Compare commits
77 Commits
v0.195.4
...
linux/keys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8276554222 | ||
|
|
2400ed34e0 | ||
|
|
ce23637dc1 | ||
|
|
7bcd6d839f | ||
|
|
20b9989b79 | ||
|
|
3269029a3e | ||
|
|
ea0e908714 | ||
|
|
8b69ccb488 | ||
|
|
2d6d53219b | ||
|
|
de615870cc | ||
|
|
f1f9470a14 | ||
|
|
9433c381eb | ||
|
|
6ede6a1573 | ||
|
|
7e0d74db8a | ||
|
|
e019ce8260 | ||
|
|
7ab7eab54e | ||
|
|
7da2e2af50 | ||
|
|
92a986cffd | ||
|
|
8b6658c650 | ||
|
|
9bc4c0f7f5 | ||
|
|
f829a178f6 | ||
|
|
2df80de880 | ||
|
|
2976b99b14 | ||
|
|
ad3d44119a | ||
|
|
0b5a264f88 | ||
|
|
f22d06cf6e | ||
|
|
26718da9bb | ||
|
|
671a91e69d | ||
|
|
bbda5d4f78 | ||
|
|
2fb674088f | ||
|
|
5c4ea49793 | ||
|
|
21e7cc3fed | ||
|
|
f53168c56c | ||
|
|
3182f14972 | ||
|
|
eca211e0f1 | ||
|
|
d6add799dc | ||
|
|
e9649dc25c | ||
|
|
71daf47ad5 | ||
|
|
66c6f5066e | ||
|
|
be79ccde07 | ||
|
|
061ba9b6d2 | ||
|
|
1ba3c2f589 | ||
|
|
bbee877ca8 | ||
|
|
c6e697fc7f | ||
|
|
b7b83105fc | ||
|
|
7d901c5e47 | ||
|
|
1a8d1944b0 | ||
|
|
35db644beb | ||
|
|
76e52ea374 | ||
|
|
ca0f0cc8d1 | ||
|
|
a133c1311d | ||
|
|
08ffd9884a | ||
|
|
dc591fe7c7 | ||
|
|
95784d53ca | ||
|
|
9b63ba6205 | ||
|
|
862e733ef5 | ||
|
|
66dda8e368 | ||
|
|
16d02cfdb3 | ||
|
|
2c41e10c98 | ||
|
|
9ab5e78b79 | ||
|
|
e30e4381de | ||
|
|
de627ba04d | ||
|
|
80eed63255 | ||
|
|
974bc4096a | ||
|
|
642d8bb8f5 | ||
|
|
41fe2a2ab4 | ||
|
|
96ff6d86a3 | ||
|
|
6e5763215f | ||
|
|
de0e6f716c | ||
|
|
93bfae71dc | ||
|
|
171be7e009 | ||
|
|
d5cc1cbaa9 | ||
|
|
6d26f107dd | ||
|
|
e2b9dfa89c | ||
|
|
495ec7a109 | ||
|
|
b9b42bee99 | ||
|
|
a9b82e1e57 |
13
.github/actions/run_tests/action.yml
vendored
@@ -1,6 +1,12 @@
|
||||
name: "Run tests"
|
||||
description: "Runs the tests"
|
||||
|
||||
inputs:
|
||||
use-xvfb:
|
||||
description: "Whether to run tests with xvfb"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -20,4 +26,9 @@ runs:
|
||||
|
||||
- name: Run tests
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
run: |
|
||||
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
|
||||
else
|
||||
cargo nextest run --workspace --no-fail-fast
|
||||
fi
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
@@ -334,6 +334,8 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
with:
|
||||
use-xvfb: true
|
||||
|
||||
- name: Build other binaries and features
|
||||
run: |
|
||||
@@ -390,7 +392,7 @@ jobs:
|
||||
|
||||
windows_tests:
|
||||
timeout-minutes: 60
|
||||
name: (Windows) Run Tests
|
||||
name: (Windows) Run Clippy and tests
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
@@ -686,10 +688,8 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
runs-on: github-8vcpu-ubuntu-2404
|
||||
if: |
|
||||
false && (
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
)
|
||||
needs: [linux_tests]
|
||||
name: Build Zed on FreeBSD
|
||||
# env:
|
||||
@@ -816,7 +816,7 @@ jobs:
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64]
|
||||
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64, freebsd]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
@@ -195,7 +195,7 @@ jobs:
|
||||
|
||||
freebsd:
|
||||
timeout-minutes: 60
|
||||
if: false && github.repository_owner == 'zed-industries'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: github-8vcpu-ubuntu-2404
|
||||
needs: tests
|
||||
env:
|
||||
|
||||
83
Cargo.lock
generated
@@ -2,6 +2,33 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "acp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent_servers",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"async-pipe",
|
||||
"buffer_diff",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"markdown",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "activity_indicator"
|
||||
version = "0.1.0"
|
||||
@@ -107,6 +134,24 @@ dependencies = [
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent_servers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent_settings"
|
||||
version = "0.1.0"
|
||||
@@ -130,8 +175,11 @@ dependencies = [
|
||||
name = "agent_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"acp",
|
||||
"agent",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"assistant_context",
|
||||
"assistant_slash_command",
|
||||
@@ -191,6 +239,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smol",
|
||||
"streaming_diff",
|
||||
"task",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal",
|
||||
@@ -212,6 +261,22 @@ dependencies = [
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentic-coding-protocol"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1ac0351749af7bf53c65042ef69fefb9351aa8b7efa0a813d6281377605c37d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"futures 0.3.31",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
@@ -3043,7 +3108,6 @@ dependencies = [
|
||||
"context_server",
|
||||
"ctor",
|
||||
"dap",
|
||||
"dap-types",
|
||||
"dap_adapters",
|
||||
"dashmap 6.1.0",
|
||||
"debugger_ui",
|
||||
@@ -9001,7 +9065,6 @@ dependencies = [
|
||||
"util",
|
||||
"vercel",
|
||||
"workspace-hack",
|
||||
"x_ai",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
@@ -14080,6 +14143,7 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
"ref-cast",
|
||||
@@ -19581,6 +19645,7 @@ dependencies = [
|
||||
"rustix 1.0.7",
|
||||
"rustls 0.23.26",
|
||||
"rustls-webpki 0.103.1",
|
||||
"schemars",
|
||||
"scopeguard",
|
||||
"sea-orm",
|
||||
"sea-query-binder",
|
||||
@@ -19733,17 +19798,6 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
|
||||
|
||||
[[package]]
|
||||
name = "x_ai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"schemars",
|
||||
"serde",
|
||||
"strum 0.27.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "0.2.3"
|
||||
@@ -19985,10 +20039,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.195.4"
|
||||
version = "0.196.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/acp",
|
||||
"crates/agent_ui",
|
||||
"crates/agent",
|
||||
"crates/agent_settings",
|
||||
"crates/agent_servers",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
@@ -177,7 +179,6 @@ members = [
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zeta",
|
||||
@@ -217,10 +218,12 @@ edition = "2024"
|
||||
# Workspace member crates
|
||||
#
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
acp = { path = "crates/acp" }
|
||||
agent = { path = "crates/agent" }
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent_ui = { path = "crates/agent_ui" }
|
||||
agent_settings = { path = "crates/agent_settings" }
|
||||
agent_servers = { path = "crates/agent_servers" }
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
@@ -391,7 +394,6 @@ web_search_providers = { path = "crates/web_search_providers" }
|
||||
welcome = { path = "crates/welcome" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
x_ai = { path = "crates/x_ai" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
@@ -402,6 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = "0.0.6"
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
|
||||
1
assets/icons/ai_gemini.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m12.414 5.47.27 9.641h2.157l.27-13.15zM15.11.889h-3.293L6.651 7.613l1.647 2.142zM.889 15.11H4.18l1.647-2.142-1.647-2.143zm0-9.641 7.409 9.641h3.292L4.181 5.47z" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 289 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>
|
||||
|
Before Width: | Height: | Size: 358 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.66659 6.5L6.33325 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.33325 6.5L9.66659 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 804 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-code"><path d="m13 13.5 2-2.5-2-2.5"/><path d="m21 21-4.3-4.3"/><path d="M9 8.5 7 11l2 2.5"/><circle cx="11" cy="11" r="8"/></svg>
|
||||
|
Before Width: | Height: | Size: 340 B |
3
assets/icons/tool_bulb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4174 10.2159C10.5454 9.58974 10.4174 9.57261 11.3762 8.46959C11.9337 7.82822 12.335 7.09214 12.335 6.27818C12.335 5.28184 11.9309 4.32631 11.2118 3.62179C10.4926 2.91728 9.5171 2.52148 8.50001 2.52148C7.48291 2.52148 6.50748 2.91728 5.78828 3.62179C5.06909 4.32631 4.66504 5.28184 4.66504 6.27818C4.66504 6.9043 4.79288 7.65565 5.62379 8.46959C6.58253 9.59098 6.45474 9.58974 6.58257 10.2159M10.4174 10.2159L10.4174 12.2989C10.4174 12.9504 9.87836 13.4786 9.21329 13.4786H7.78674C7.12167 13.4786 6.58253 12.9504 6.58253 12.2989L6.58257 10.2159M10.4174 10.2159H8.50001H6.58257" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 776 B |
4
assets/icons/tool_copy.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.5 2.5H6.5C6.22386 2.5 6 2.83579 6 3.25V4.75C6 5.16421 6.22386 5.5 6.5 5.5H9.5C9.77614 5.5 10 5.16421 10 4.75V3.25C10 2.83579 9.77614 2.5 9.5 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 3.5H11C11.2652 3.5 11.5196 3.61706 11.7071 3.82544C11.8946 4.03381 12 4.31643 12 4.61111V12.3889C12 12.6836 11.8946 12.9662 11.7071 13.1746C11.5196 13.3829 11.2652 13.5 11 13.5H5C4.73478 13.5 4.48043 13.3829 4.29289 13.1746C4.10536 12.9662 4 12.6836 4 12.3889V4.61111C4 4.31643 4.10536 4.03381 4.29289 3.82544C4.48043 3.61706 4.73478 3.5 5 3.5H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 788 B |
5
assets/icons/tool_delete_file.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.50002 2.5H5C4.73478 2.5 4.48043 2.6159 4.29289 2.82219C4.10535 3.02848 4 3.30826 4 3.6V12.3999C4 12.6917 4.10535 12.9715 4.29289 13.1778C4.48043 13.3841 4.73478 13.5 5 13.5H11C11.2652 13.5 11.5195 13.3841 11.7071 13.1778C11.8946 12.9715 12 12.6917 12 12.3999V5.25L9.50002 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.3427 6.82379L6.65698 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.65698 6.82379L9.3427 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 724 B |
5
assets/icons/tool_diagnostics.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.7244 11.5299L9.01711 3.2922C8.91447 3.11109 8.76562 2.96045 8.58576 2.85564C8.4059 2.75084 8.20145 2.69562 7.99328 2.69562C7.7851 2.69562 7.58066 2.75084 7.40079 2.85564C7.22093 2.96045 7.07209 3.11109 6.96945 3.2922L2.26218 11.5299C2.15844 11.7096 2.10404 11.9135 2.1045 12.121C2.10495 12.3285 2.16026 12.5321 2.2648 12.7113C2.36934 12.8905 2.5194 13.0389 2.69978 13.1415C2.88015 13.244 3.08443 13.297 3.2919 13.2951H12.7064C12.9129 13.2949 13.1157 13.2404 13.2944 13.137C13.4731 13.0336 13.6215 12.8851 13.7247 12.7062C13.8278 12.5273 13.8821 12.3245 13.882 12.118C13.882 11.9115 13.8276 11.7087 13.7244 11.5299Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.99927 6.23425V8.58788" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.99927 10.9415H8.00492" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
assets/icons/tool_folder.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.4 12.5C12.6917 12.5 12.9715 12.3884 13.1778 12.1899C13.3841 11.9913 13.5 11.722 13.5 11.4412V6.14706C13.5 5.86624 13.3841 5.59693 13.1778 5.39836C12.9715 5.19979 12.6917 5.08824 12.4 5.08824H8.055C7.87103 5.08997 7.68955 5.04726 7.52717 4.96402C7.36478 4.88078 7.22668 4.75967 7.1255 4.61176L6.68 3.97647C6.57984 3.83007 6.44349 3.7099 6.28317 3.62674C6.12286 3.54358 5.94361 3.50003 5.7615 3.5H3.6C3.30826 3.5 3.02847 3.61155 2.82218 3.81012C2.61589 4.00869 2.5 4.27801 2.5 4.55882V11.4412C2.5 11.722 2.61589 11.9913 2.82218 12.1899C3.02847 12.3884 3.30826 12.5 3.6 12.5H12.4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 778 B |
5
assets/icons/tool_hammer.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 8.5L4.94864 12.6222C4.71647 12.8544 4.40157 12.9848 4.07323 12.9848C3.74488 12.9848 3.42999 12.8544 3.19781 12.6222C2.96564 12.39 2.83521 12.0751 2.83521 11.7468C2.83521 11.4185 2.96564 11.1036 3.19781 10.8714L7.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.8352 9.98474L13.8352 6.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.8352 7.42495L11.7634 6.4298C11.5533 6.23484 11.4353 5.97039 11.4352 5.69462V5.08526L10.1696 3.91022C9.54495 3.33059 8.69961 3.00261 7.81649 2.99722L5.83521 2.98474L6.35041 3.41108C6.71634 3.71233 7.00935 4.08216 7.21013 4.4962C7.4109 4.91024 7.51488 5.35909 7.51521 5.81316L7.5 6.5L9 8.5L9.5 8C9.5 8 9.87337 7.79457 10.0834 7.98959L11.1552 8.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 988 B |
4
assets/icons/tool_notification.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.5 12C6.65203 12.304 6.87068 12.5565 7.13399 12.7321C7.39729 12.9076 7.69597 13 8 13C8.30403 13 8.60271 12.9076 8.86601 12.7321C9.12932 12.5565 9.34797 12.304 9.5 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.63088 9.21874C3.56556 9.28556 3.52246 9.36865 3.50681 9.45791C3.49116 9.54718 3.50364 9.63876 3.54273 9.72152C3.58183 9.80429 3.64585 9.87467 3.72701 9.92409C3.80817 9.97352 3.90298 9.99987 3.99989 9.99994H12.0001C12.097 9.99997 12.1918 9.97372 12.273 9.92439C12.3542 9.87505 12.4183 9.80476 12.4575 9.72205C12.4967 9.63934 12.5093 9.54778 12.4938 9.45851C12.4783 9.36924 12.4353 9.2861 12.3701 9.21921C11.705 8.57941 11 7.89947 11 5.79994C11 5.05733 10.684 4.34514 10.1213 3.82004C9.55872 3.29494 8.79564 2.99994 7.99997 2.99994C7.20431 2.99994 6.44123 3.29494 5.87861 3.82004C5.31599 4.34514 4.99991 5.05733 4.99991 5.79994C4.99991 7.89947 4.2944 8.57941 3.63088 9.21874Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
assets/icons/tool_pencil.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 5L11 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 835 B |
7
assets/icons/tool_read.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 5.66667V4.33333C3 3.97971 3.14048 3.64057 3.39052 3.39052C3.64057 3.14048 3.97971 3 4.33333 3H5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3333 3H11.6666C12.0202 3 12.3593 3.14048 12.6094 3.39052C12.8594 3.64057 12.9999 3.97971 12.9999 4.33333V5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.9999 10.3333V11.6666C12.9999 12.0203 12.8594 12.3594 12.6094 12.6095C12.3593 12.8595 12.0202 13 11.6666 13H10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.66667 13H4.33333C3.97971 13 3.64057 12.8595 3.39052 12.6095C3.14048 12.3594 3 12.0203 3 11.6666V10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.5 8H10.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
4
assets/icons/tool_regex.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.57132 13.7143C5.20251 13.7143 5.71418 13.2026 5.71418 12.5714C5.71418 11.9403 5.20251 11.4286 4.57132 11.4286C3.94014 11.4286 3.42847 11.9403 3.42847 12.5714C3.42847 13.2026 3.94014 13.7143 4.57132 13.7143Z" fill="black"/>
|
||||
<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 631 B |
4
assets/icons/tool_search.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 13L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
5
assets/icons/tool_terminal.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.99487 8.44023L7.32821 7.10689L5.99487 5.77356" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.33838 10.2264H10.005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 625 B |
5
assets/icons/tool_web.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99993 13.4804C11.0267 13.4804 13.4803 11.0267 13.4803 7.99999C13.4803 4.97325 11.0267 2.51959 7.99993 2.51959C4.97319 2.51959 2.51953 4.97325 2.51953 7.99999C2.51953 11.0267 4.97319 13.4804 7.99993 13.4804Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 3C6.71611 4.34807 6 6.13836 6 7.99999C6 9.86163 6.71611 11.6519 8 13C9.28387 11.6519 10 9.86163 10 7.99999C10 6.13836 9.28387 4.34807 8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.24121 7.04827C4.52425 8.27022 6.22817 8.95178 7.99999 8.95178C9.77182 8.95178 11.4757 8.27022 12.7588 7.04827" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 847 B |
@@ -268,6 +268,14 @@
|
||||
"ctrl-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewAcpThread",
|
||||
"ctrl-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
@@ -306,6 +314,15 @@
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
|
||||
@@ -309,6 +309,14 @@
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewAcpThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -357,6 +365,15 @@
|
||||
"ctrl--": "pane::GoBack"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"up": "agent::PreviousHistoryMessage",
|
||||
"down": "agent::NextHistoryMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
|
||||
@@ -362,7 +362,9 @@
|
||||
// Whether to show user picture in the titlebar.
|
||||
"show_user_picture": true,
|
||||
// Whether to show the sign in button in the titlebar.
|
||||
"show_sign_in": true
|
||||
"show_sign_in": true,
|
||||
// Whether to show the menus in the titlebar.
|
||||
"show_menus": false
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@@ -1855,6 +1857,8 @@
|
||||
"read_ssh_config": true,
|
||||
// Configures context servers for use by the agent.
|
||||
"context_servers": {},
|
||||
// Configures agent servers available in the agent panel.
|
||||
"agent_servers": {},
|
||||
"debugger": {
|
||||
"stepping_granularity": "line",
|
||||
"save_breakpoints": true,
|
||||
|
||||
46
crates/acp/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "acp"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/acp.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["gpui/test-support", "project/test-support"]
|
||||
gemini = []
|
||||
|
||||
[dependencies]
|
||||
agent_servers.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
markdown.workspace = true
|
||||
project.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
async-pipe.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
project = { workspace = true, "features" = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
settings.workspace = true
|
||||
1645
crates/acp/src/acp.rs
Normal file
@@ -21,7 +21,6 @@ use gpui::{
|
||||
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
use http_client::StatusCode;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest,
|
||||
@@ -52,19 +51,7 @@ use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
|
||||
const MAX_RETRY_ATTEMPTS: u8 = 3;
|
||||
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum RetryStrategy {
|
||||
ExponentialBackoff {
|
||||
initial_delay: Duration,
|
||||
max_attempts: u8,
|
||||
},
|
||||
Fixed {
|
||||
delay: Duration,
|
||||
max_attempts: u8,
|
||||
},
|
||||
}
|
||||
const BASE_RETRY_DELAY_SECS: u64 = 5;
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
|
||||
@@ -396,7 +383,6 @@ pub struct Thread {
|
||||
remaining_turns: u32,
|
||||
configured_model: Option<ConfiguredModel>,
|
||||
profile: AgentProfile,
|
||||
last_error_context: Option<(Arc<dyn LanguageModel>, CompletionIntent)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -490,11 +476,10 @@ impl Thread {
|
||||
retry_state: None,
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
last_error_context: None,
|
||||
last_received_chunk_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model: configured_model.clone(),
|
||||
configured_model,
|
||||
profile: AgentProfile::new(profile_id, tools),
|
||||
}
|
||||
}
|
||||
@@ -615,7 +600,6 @@ impl Thread {
|
||||
feedback: None,
|
||||
message_feedback: HashMap::default(),
|
||||
last_auto_capture_at: None,
|
||||
last_error_context: None,
|
||||
last_received_chunk_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
@@ -1267,58 +1251,9 @@ impl Thread {
|
||||
|
||||
self.flush_notifications(model.clone(), intent, cx);
|
||||
|
||||
let _checkpoint = self.finalize_pending_checkpoint(cx);
|
||||
self.stream_completion(
|
||||
self.to_completion_request(model.clone(), intent, cx),
|
||||
model,
|
||||
intent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
let request = self.to_completion_request(model.clone(), intent, cx);
|
||||
|
||||
pub fn retry_last_completion(
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// Clear any existing error state
|
||||
self.retry_state = None;
|
||||
|
||||
// Use the last error context if available, otherwise fall back to configured model
|
||||
let (model, intent) = if let Some((model, intent)) = self.last_error_context.take() {
|
||||
(model, intent)
|
||||
} else if let Some(configured_model) = self.configured_model.as_ref() {
|
||||
let model = configured_model.model.clone();
|
||||
let intent = if self.has_pending_tool_uses() {
|
||||
CompletionIntent::ToolResults
|
||||
} else {
|
||||
CompletionIntent::UserPrompt
|
||||
};
|
||||
(model, intent)
|
||||
} else if let Some(configured_model) = self.get_or_init_configured_model(cx) {
|
||||
let model = configured_model.model.clone();
|
||||
let intent = if self.has_pending_tool_uses() {
|
||||
CompletionIntent::ToolResults
|
||||
} else {
|
||||
CompletionIntent::UserPrompt
|
||||
};
|
||||
(model, intent)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.send_to_model(model, intent, window, cx);
|
||||
}
|
||||
|
||||
pub fn enable_burn_mode_and_retry(
|
||||
&mut self,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.completion_mode = CompletionMode::Burn;
|
||||
cx.emit(ThreadEvent::ProfileChanged);
|
||||
self.retry_last_completion(window, cx);
|
||||
self.stream_completion(request, model, intent, window, cx);
|
||||
}
|
||||
|
||||
pub fn used_tools_since_last_user_message(&self) -> bool {
|
||||
@@ -1349,6 +1284,7 @@ impl Thread {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: AgentSettings::temperature_for_model(&model, cx),
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
let available_tools = self.available_tools(cx, model.clone());
|
||||
@@ -1514,6 +1450,7 @@ impl Thread {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: AgentSettings::temperature_for_model(model, cx),
|
||||
thinking_allowed: false,
|
||||
};
|
||||
|
||||
for message in &self.messages {
|
||||
@@ -1996,6 +1933,18 @@ impl Thread {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
|
||||
fn emit_generic_error(error: &anyhow::Error, cx: &mut Context<Thread>) {
|
||||
let error_message = error
|
||||
.chain()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Error interacting with language model".into(),
|
||||
message: SharedString::from(error_message.clone()),
|
||||
}));
|
||||
}
|
||||
|
||||
if error.is::<PaymentRequiredError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
|
||||
} else if let Some(error) =
|
||||
@@ -2007,10 +1956,9 @@ impl Thread {
|
||||
} else if let Some(completion_error) =
|
||||
error.downcast_ref::<LanguageModelCompletionError>()
|
||||
{
|
||||
use LanguageModelCompletionError::*;
|
||||
match &completion_error {
|
||||
LanguageModelCompletionError::PromptTooLarge {
|
||||
tokens, ..
|
||||
} => {
|
||||
PromptTooLarge { tokens, .. } => {
|
||||
let tokens = tokens.unwrap_or_else(|| {
|
||||
// We didn't get an exact token count from the API, so fall back on our estimate.
|
||||
thread
|
||||
@@ -2031,22 +1979,63 @@ impl Thread {
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
_ => {
|
||||
if let Some(retry_strategy) =
|
||||
Thread::get_retry_strategy(completion_error)
|
||||
{
|
||||
retry_scheduled = thread
|
||||
.handle_retryable_error_with_delay(
|
||||
&completion_error,
|
||||
Some(retry_strategy),
|
||||
model.clone(),
|
||||
intent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
RateLimitExceeded {
|
||||
retry_after: Some(retry_after),
|
||||
..
|
||||
}
|
||||
| ServerOverloaded {
|
||||
retry_after: Some(retry_after),
|
||||
..
|
||||
} => {
|
||||
thread.handle_rate_limit_error(
|
||||
&completion_error,
|
||||
*retry_after,
|
||||
model.clone(),
|
||||
intent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
retry_scheduled = true;
|
||||
}
|
||||
RateLimitExceeded { .. } | ServerOverloaded { .. } => {
|
||||
retry_scheduled = thread.handle_retryable_error(
|
||||
&completion_error,
|
||||
model.clone(),
|
||||
intent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if !retry_scheduled {
|
||||
emit_generic_error(error, cx);
|
||||
}
|
||||
}
|
||||
ApiInternalServerError { .. }
|
||||
| ApiReadResponseError { .. }
|
||||
| HttpSend { .. } => {
|
||||
retry_scheduled = thread.handle_retryable_error(
|
||||
&completion_error,
|
||||
model.clone(),
|
||||
intent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if !retry_scheduled {
|
||||
emit_generic_error(error, cx);
|
||||
}
|
||||
}
|
||||
NoApiKey { .. }
|
||||
| HttpResponseError { .. }
|
||||
| BadRequestFormat { .. }
|
||||
| AuthenticationError { .. }
|
||||
| PermissionError { .. }
|
||||
| ApiEndpointNotFound { .. }
|
||||
| SerializeRequest { .. }
|
||||
| BuildRequestBody { .. }
|
||||
| DeserializeResponse { .. }
|
||||
| Other { .. } => emit_generic_error(error, cx),
|
||||
}
|
||||
} else {
|
||||
emit_generic_error(error, cx);
|
||||
}
|
||||
|
||||
if !retry_scheduled {
|
||||
@@ -2173,132 +2162,73 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
|
||||
fn get_retry_strategy(error: &LanguageModelCompletionError) -> Option<RetryStrategy> {
|
||||
use LanguageModelCompletionError::*;
|
||||
|
||||
// General strategy here:
|
||||
// - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all.
|
||||
// - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), try multiple times with exponential backoff.
|
||||
// - If it's an issue that *might* be fixed by retrying (e.g. internal server error), just retry once.
|
||||
match error {
|
||||
HttpResponseError {
|
||||
status_code: StatusCode::TOO_MANY_REQUESTS,
|
||||
..
|
||||
} => Some(RetryStrategy::ExponentialBackoff {
|
||||
initial_delay: BASE_RETRY_DELAY,
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
}),
|
||||
ServerOverloaded { retry_after, .. } | RateLimitExceeded { retry_after, .. } => {
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
})
|
||||
}
|
||||
UpstreamProviderError {
|
||||
status,
|
||||
retry_after,
|
||||
..
|
||||
} => match *status {
|
||||
StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => {
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
})
|
||||
}
|
||||
StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
// Internal Server Error could be anything, so only retry once.
|
||||
max_attempts: 1,
|
||||
}),
|
||||
status => {
|
||||
// There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"),
|
||||
// but we frequently get them in practice. See https://http.dev/529
|
||||
if status.as_u16() == 529 {
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
ApiInternalServerError { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
}),
|
||||
ApiReadResponseError { .. }
|
||||
| HttpSend { .. }
|
||||
| DeserializeResponse { .. }
|
||||
| BadRequestFormat { .. } => Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
}),
|
||||
// Retrying these errors definitely shouldn't help.
|
||||
HttpResponseError {
|
||||
status_code:
|
||||
StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED,
|
||||
..
|
||||
}
|
||||
| SerializeRequest { .. }
|
||||
| BuildRequestBody { .. }
|
||||
| PromptTooLarge { .. }
|
||||
| AuthenticationError { .. }
|
||||
| PermissionError { .. }
|
||||
| ApiEndpointNotFound { .. }
|
||||
| NoApiKey { .. } => None,
|
||||
// Retry all other 4xx and 5xx errors once.
|
||||
HttpResponseError { status_code, .. }
|
||||
if status_code.is_client_error() || status_code.is_server_error() =>
|
||||
{
|
||||
Some(RetryStrategy::Fixed {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 1,
|
||||
})
|
||||
}
|
||||
// Conservatively assume that any other errors are non-retryable
|
||||
HttpResponseError { .. } | Other(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_retryable_error_with_delay(
|
||||
fn handle_rate_limit_error(
|
||||
&mut self,
|
||||
error: &LanguageModelCompletionError,
|
||||
retry_after: Duration,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// For rate limit errors, we only retry once with the specified duration
|
||||
let retry_message = format!("{error}. Retrying in {} seconds…", retry_after.as_secs());
|
||||
log::warn!(
|
||||
"Retrying completion request in {} seconds: {error:?}",
|
||||
retry_after.as_secs(),
|
||||
);
|
||||
|
||||
// Add a UI-only message instead of a regular message
|
||||
let id = self.next_message_id.post_inc();
|
||||
self.messages.push(Message {
|
||||
id,
|
||||
role: Role::System,
|
||||
segments: vec![MessageSegment::Text(retry_message)],
|
||||
loaded_context: LoadedContext::default(),
|
||||
creases: Vec::new(),
|
||||
is_hidden: false,
|
||||
ui_only: true,
|
||||
});
|
||||
cx.emit(ThreadEvent::MessageAdded(id));
|
||||
// Schedule the retry
|
||||
let thread_handle = cx.entity().downgrade();
|
||||
|
||||
cx.spawn(async move |_thread, cx| {
|
||||
cx.background_executor().timer(retry_after).await;
|
||||
|
||||
thread_handle
|
||||
.update(cx, |thread, cx| {
|
||||
// Retry the completion
|
||||
thread.send_to_model(model, intent, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_retryable_error(
|
||||
&mut self,
|
||||
error: &LanguageModelCompletionError,
|
||||
strategy: Option<RetryStrategy>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
// Store context for the Retry button
|
||||
self.last_error_context = Some((model.clone(), intent));
|
||||
|
||||
// Only auto-retry if Burn Mode is enabled
|
||||
if self.completion_mode != CompletionMode::Burn {
|
||||
// Show error with retry options
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError {
|
||||
message: format!(
|
||||
"{}\n\nTo automatically retry when similar errors happen, enable Burn Mode.",
|
||||
error
|
||||
)
|
||||
.into(),
|
||||
can_enable_burn_mode: true,
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(strategy) = strategy.or_else(|| Self::get_retry_strategy(error)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let max_attempts = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
|
||||
RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
|
||||
};
|
||||
self.handle_retryable_error_with_delay(error, None, model, intent, window, cx)
|
||||
}
|
||||
|
||||
fn handle_retryable_error_with_delay(
|
||||
&mut self,
|
||||
error: &LanguageModelCompletionError,
|
||||
custom_delay: Option<Duration>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let retry_state = self.retry_state.get_or_insert(RetryState {
|
||||
attempt: 0,
|
||||
max_attempts,
|
||||
max_attempts: MAX_RETRY_ATTEMPTS,
|
||||
intent,
|
||||
});
|
||||
|
||||
@@ -2308,24 +2238,20 @@ impl Thread {
|
||||
let intent = retry_state.intent;
|
||||
|
||||
if attempt <= max_attempts {
|
||||
let delay = match &strategy {
|
||||
RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
|
||||
let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
|
||||
Duration::from_secs(delay_secs)
|
||||
}
|
||||
RetryStrategy::Fixed { delay, .. } => *delay,
|
||||
// Use custom delay if provided (e.g., from rate limit), otherwise exponential backoff
|
||||
let delay = if let Some(custom_delay) = custom_delay {
|
||||
custom_delay
|
||||
} else {
|
||||
let delay_secs = BASE_RETRY_DELAY_SECS * 2u64.pow((attempt - 1) as u32);
|
||||
Duration::from_secs(delay_secs)
|
||||
};
|
||||
|
||||
// Add a transient message to inform the user
|
||||
let delay_secs = delay.as_secs();
|
||||
let retry_message = if max_attempts == 1 {
|
||||
format!("{error}. Retrying in {delay_secs} seconds...")
|
||||
} else {
|
||||
format!(
|
||||
"{error}. Retrying (attempt {attempt} of {max_attempts}) \
|
||||
in {delay_secs} seconds..."
|
||||
)
|
||||
};
|
||||
let retry_message = format!(
|
||||
"{error}. Retrying (attempt {attempt} of {max_attempts}) \
|
||||
in {delay_secs} seconds..."
|
||||
);
|
||||
log::warn!(
|
||||
"Retrying completion request (attempt {attempt} of {max_attempts}) \
|
||||
in {delay_secs} seconds: {error:?}",
|
||||
@@ -2364,15 +2290,18 @@ impl Thread {
|
||||
// Max retries exceeded
|
||||
self.retry_state = None;
|
||||
|
||||
let notification_text = if max_attempts == 1 {
|
||||
"Failed after retrying.".into()
|
||||
} else {
|
||||
format!("Failed after retrying {} times.", max_attempts).into()
|
||||
};
|
||||
|
||||
// Stop generating since we're giving up on retrying.
|
||||
self.pending_completions.clear();
|
||||
|
||||
// Show error alongside a Retry button, but no
|
||||
// Enable Burn Mode button (since it's already enabled)
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError {
|
||||
message: format!("Failed after retrying: {}", error).into(),
|
||||
can_enable_burn_mode: false,
|
||||
}));
|
||||
cx.emit(ThreadEvent::RetriesFailed {
|
||||
message: notification_text,
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
@@ -3284,11 +3213,6 @@ pub enum ThreadError {
|
||||
header: SharedString,
|
||||
message: SharedString,
|
||||
},
|
||||
#[error("Retryable error: {message}")]
|
||||
RetryableError {
|
||||
message: SharedString,
|
||||
can_enable_burn_mode: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -3334,6 +3258,9 @@ pub enum ThreadEvent {
|
||||
CancelEditing,
|
||||
CompletionCanceled,
|
||||
ProfileChanged,
|
||||
RetriesFailed {
|
||||
message: SharedString,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventEmitter<ThreadEvent> for Thread {}
|
||||
@@ -4244,11 +4171,6 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
@@ -4270,7 +4192,7 @@ fn main() {{
|
||||
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
|
||||
"Should retry MAX_RETRY_ATTEMPTS times for overloaded errors"
|
||||
"Should have default max attempts"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4322,11 +4244,6 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns internal server error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::InternalServerError));
|
||||
|
||||
@@ -4348,7 +4265,7 @@ fn main() {{
|
||||
let retry_state = thread.retry_state.as_ref().unwrap();
|
||||
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, 1,
|
||||
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
|
||||
"Should have correct max attempts"
|
||||
);
|
||||
});
|
||||
@@ -4364,8 +4281,8 @@ fn main() {{
|
||||
if let MessageSegment::Text(text) = seg {
|
||||
text.contains("internal")
|
||||
&& text.contains("Fake")
|
||||
&& text.contains("Retrying in")
|
||||
&& !text.contains("attempt")
|
||||
&& text
|
||||
.contains(&format!("attempt 1 of {}", MAX_RETRY_ATTEMPTS))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -4403,13 +4320,8 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns internal server error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::InternalServerError));
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
// Insert a user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
@@ -4459,14 +4371,11 @@ fn main() {{
|
||||
assert!(thread.retry_state.is_some(), "Should have retry state");
|
||||
let retry_state = thread.retry_state.as_ref().unwrap();
|
||||
assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, 1,
|
||||
"Internal server errors should only retry once"
|
||||
);
|
||||
});
|
||||
|
||||
// Advance clock for first retry
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS));
|
||||
cx.run_until_parked();
|
||||
|
||||
// Should have scheduled second retry - count retry messages
|
||||
@@ -4486,25 +4395,93 @@ fn main() {{
|
||||
})
|
||||
.count()
|
||||
});
|
||||
assert_eq!(
|
||||
retry_count, 1,
|
||||
"Should have only one retry for internal server errors"
|
||||
);
|
||||
assert_eq!(retry_count, 2, "Should have scheduled second retry");
|
||||
|
||||
// For internal server errors, we only retry once and then give up
|
||||
// Check that retry_state is cleared after the single retry
|
||||
// Check retry state updated
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
thread.retry_state.is_none(),
|
||||
"Retry state should be cleared after single retry"
|
||||
assert!(thread.retry_state.is_some(), "Should have retry state");
|
||||
let retry_state = thread.retry_state.as_ref().unwrap();
|
||||
assert_eq!(retry_state.attempt, 2, "Should be second retry attempt");
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
|
||||
"Should have correct max attempts"
|
||||
);
|
||||
});
|
||||
|
||||
// Verify total attempts (1 initial + 1 retry)
|
||||
// Advance clock for second retry (exponential backoff)
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS * 2));
|
||||
cx.run_until_parked();
|
||||
|
||||
// Should have scheduled third retry
|
||||
// Count all retry messages now
|
||||
let retry_count = thread.update(cx, |thread, _| {
|
||||
thread
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.ui_only
|
||||
&& m.segments.iter().any(|s| {
|
||||
if let MessageSegment::Text(text) = s {
|
||||
text.contains("Retrying") && text.contains("seconds")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
})
|
||||
.count()
|
||||
});
|
||||
assert_eq!(
|
||||
retry_count, MAX_RETRY_ATTEMPTS as usize,
|
||||
"Should have scheduled third retry"
|
||||
);
|
||||
|
||||
// Check retry state updated
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(thread.retry_state.is_some(), "Should have retry state");
|
||||
let retry_state = thread.retry_state.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
retry_state.attempt, MAX_RETRY_ATTEMPTS,
|
||||
"Should be at max retry attempt"
|
||||
);
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
|
||||
"Should have correct max attempts"
|
||||
);
|
||||
});
|
||||
|
||||
// Advance clock for third retry (exponential backoff)
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS * 4));
|
||||
cx.run_until_parked();
|
||||
|
||||
// No more retries should be scheduled after clock was advanced.
|
||||
let retry_count = thread.update(cx, |thread, _| {
|
||||
thread
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.ui_only
|
||||
&& m.segments.iter().any(|s| {
|
||||
if let MessageSegment::Text(text) = s {
|
||||
text.contains("Retrying") && text.contains("seconds")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
})
|
||||
.count()
|
||||
});
|
||||
assert_eq!(
|
||||
retry_count, MAX_RETRY_ATTEMPTS as usize,
|
||||
"Should not exceed max retries"
|
||||
);
|
||||
|
||||
// Final completion count should be initial + max retries
|
||||
assert_eq!(
|
||||
*completion_count.lock(),
|
||||
2,
|
||||
"Should have attempted once plus 1 retry"
|
||||
(MAX_RETRY_ATTEMPTS + 1) as usize,
|
||||
"Should have made initial + max retry attempts"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4515,11 +4492,6 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
@@ -4529,13 +4501,13 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Track events
|
||||
let stopped_with_error = Arc::new(Mutex::new(false));
|
||||
let stopped_with_error_clone = stopped_with_error.clone();
|
||||
let retries_failed = Arc::new(Mutex::new(false));
|
||||
let retries_failed_clone = retries_failed.clone();
|
||||
|
||||
let _subscription = thread.update(cx, |_, cx| {
|
||||
cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| {
|
||||
if let ThreadEvent::Stopped(Err(_)) = event {
|
||||
*stopped_with_error_clone.lock() = true;
|
||||
if let ThreadEvent::RetriesFailed { .. } = event {
|
||||
*retries_failed_clone.lock() = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -4547,11 +4519,23 @@ fn main() {{
|
||||
cx.run_until_parked();
|
||||
|
||||
// Advance through all retries
|
||||
for _ in 0..MAX_RETRY_ATTEMPTS {
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
for i in 0..MAX_RETRY_ATTEMPTS {
|
||||
let delay = if i == 0 {
|
||||
BASE_RETRY_DELAY_SECS
|
||||
} else {
|
||||
BASE_RETRY_DELAY_SECS * 2u64.pow(i as u32 - 1)
|
||||
};
|
||||
cx.executor().advance_clock(Duration::from_secs(delay));
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
// After the 3rd retry is scheduled, we need to wait for it to execute and fail
|
||||
// The 3rd retry has a delay of BASE_RETRY_DELAY_SECS * 4 (20 seconds)
|
||||
let final_delay = BASE_RETRY_DELAY_SECS * 2u64.pow((MAX_RETRY_ATTEMPTS - 1) as u32);
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_secs(final_delay));
|
||||
cx.run_until_parked();
|
||||
|
||||
let retry_count = thread.update(cx, |thread, _| {
|
||||
thread
|
||||
.messages
|
||||
@@ -4569,14 +4553,14 @@ fn main() {{
|
||||
.count()
|
||||
});
|
||||
|
||||
// After max retries, should emit Stopped(Err(...)) event
|
||||
// After max retries, should emit RetriesFailed event
|
||||
assert_eq!(
|
||||
retry_count, MAX_RETRY_ATTEMPTS as usize,
|
||||
"Should have attempted MAX_RETRY_ATTEMPTS retries for overloaded errors"
|
||||
"Should have attempted max retries"
|
||||
);
|
||||
assert!(
|
||||
*stopped_with_error.lock(),
|
||||
"Should emit Stopped(Err(...)) event after max retries exceeded"
|
||||
*retries_failed.lock(),
|
||||
"Should emit RetriesFailed event after max retries exceeded"
|
||||
);
|
||||
|
||||
// Retry state should be cleared
|
||||
@@ -4594,7 +4578,7 @@ fn main() {{
|
||||
.count();
|
||||
assert_eq!(
|
||||
retry_messages, MAX_RETRY_ATTEMPTS as usize,
|
||||
"Should have MAX_RETRY_ATTEMPTS retry messages for overloaded errors"
|
||||
"Should have one retry message per attempt"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -4606,11 +4590,6 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// We'll use a wrapper to switch behavior after first failure
|
||||
struct RetryTestModel {
|
||||
inner: Arc<FakeLanguageModel>,
|
||||
@@ -4737,7 +4716,8 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Wait for retry
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS));
|
||||
cx.run_until_parked();
|
||||
|
||||
// Stream some successful content
|
||||
@@ -4779,11 +4759,6 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create a model that fails once then succeeds
|
||||
struct FailOnceModel {
|
||||
inner: Arc<FakeLanguageModel>,
|
||||
@@ -4904,7 +4879,8 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Wait for retry delay
|
||||
cx.executor().advance_clock(BASE_RETRY_DELAY);
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS));
|
||||
cx.run_until_parked();
|
||||
|
||||
// The retry should now use our FailOnceModel which should succeed
|
||||
@@ -4945,11 +4921,6 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create a model that returns rate limit error with retry_after
|
||||
struct RateLimitModel {
|
||||
inner: Arc<FakeLanguageModel>,
|
||||
@@ -5068,15 +5039,9 @@ fn main() {{
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
thread.retry_state.is_some(),
|
||||
"Rate limit errors should set retry_state"
|
||||
thread.retry_state.is_none(),
|
||||
"Rate limit errors should not set retry_state"
|
||||
);
|
||||
if let Some(retry_state) = &thread.retry_state {
|
||||
assert_eq!(
|
||||
retry_state.max_attempts, MAX_RETRY_ATTEMPTS,
|
||||
"Rate limit errors should use MAX_RETRY_ATTEMPTS"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify we have one retry message
|
||||
@@ -5109,15 +5074,18 @@ fn main() {{
|
||||
.find(|msg| msg.role == Role::System && msg.ui_only)
|
||||
.expect("Should have a retry message");
|
||||
|
||||
// Check that the message contains attempt count since we use retry_state
|
||||
// Check that the message doesn't contain attempt count
|
||||
if let Some(MessageSegment::Text(text)) = retry_message.segments.first() {
|
||||
assert!(
|
||||
text.contains(&format!("attempt 1 of {}", MAX_RETRY_ATTEMPTS)),
|
||||
"Rate limit retry message should contain attempt count with MAX_RETRY_ATTEMPTS"
|
||||
!text.contains("attempt"),
|
||||
"Rate limit retry message should not contain attempt count"
|
||||
);
|
||||
assert!(
|
||||
text.contains("Retrying"),
|
||||
"Rate limit retry message should contain retry text"
|
||||
text.contains(&format!(
|
||||
"Retrying in {} seconds",
|
||||
TEST_RATE_LIMIT_RETRY_SECS
|
||||
)),
|
||||
"Rate limit retry message should contain retry delay"
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -5223,79 +5191,6 @@ fn main() {{
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Ensure we're in Normal mode (not Burn mode)
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Normal);
|
||||
});
|
||||
|
||||
// Track error events
|
||||
let error_events = Arc::new(Mutex::new(Vec::new()));
|
||||
let error_events_clone = error_events.clone();
|
||||
|
||||
let _subscription = thread.update(cx, |_, cx| {
|
||||
cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| {
|
||||
if let ThreadEvent::ShowError(error) = event {
|
||||
error_events_clone.lock().push(error.clone());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
// Insert a user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx);
|
||||
});
|
||||
|
||||
// Start completion
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify no retry state was created
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
thread.retry_state.is_none(),
|
||||
"Should not have retry state in Normal mode"
|
||||
);
|
||||
});
|
||||
|
||||
// Check that a retryable error was reported
|
||||
let errors = error_events.lock();
|
||||
assert!(!errors.is_empty(), "Should have received an error event");
|
||||
|
||||
if let ThreadError::RetryableError {
|
||||
message: _,
|
||||
can_enable_burn_mode,
|
||||
} = &errors[0]
|
||||
{
|
||||
assert!(
|
||||
*can_enable_burn_mode,
|
||||
"Error should indicate burn mode can be enabled"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected RetryableError, got {:?}", errors[0]);
|
||||
}
|
||||
|
||||
// Verify the thread is no longer generating
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(
|
||||
!thread.is_generating(),
|
||||
"Should not be generating after error without retry"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
@@ -5303,11 +5198,6 @@ fn main() {{
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Enable Burn Mode to allow retries
|
||||
thread.update(cx, |thread, _| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
|
||||
// Create model that returns overloaded error
|
||||
let model = Arc::new(ErrorInjector::new(TestError::Overloaded));
|
||||
|
||||
|
||||
27
crates/agent_servers/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "agent_servers"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/agent_servers.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
1
crates/agent_servers/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
231
crates/agent_servers/src/agent_servers.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AsyncApp, Entity, SharedString};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use util::{ResultExt, paths};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
pub struct AllAgentServersSettings {
|
||||
gemini: Option<AgentServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
pub struct AgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
command: AgentServerCommand,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
|
||||
pub struct AgentServerCommand {
|
||||
#[serde(rename = "command")]
|
||||
pub path: PathBuf,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
pub struct Gemini;
|
||||
|
||||
pub struct AgentServerVersion {
|
||||
pub current_version: SharedString,
|
||||
pub supported: bool,
|
||||
}
|
||||
|
||||
pub trait AgentServer: Send {
|
||||
fn command(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> impl Future<Output = Result<AgentServerCommand>>;
|
||||
|
||||
fn version(
|
||||
&self,
|
||||
command: &AgentServerCommand,
|
||||
) -> impl Future<Output = Result<AgentServerVersion>> + Send;
|
||||
}
|
||||
|
||||
const GEMINI_ACP_ARG: &str = "--acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
async fn command(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<AgentServerCommand> {
|
||||
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
|
||||
let settings = settings.get::<AllAgentServersSettings>(None);
|
||||
settings
|
||||
.gemini
|
||||
.as_ref()
|
||||
.map(|gemini_settings| AgentServerCommand {
|
||||
path: gemini_settings.command.path.clone(),
|
||||
args: gemini_settings
|
||||
.command
|
||||
.args
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(GEMINI_ACP_ARG.into()))
|
||||
.collect(),
|
||||
env: gemini_settings.command.env.clone(),
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Some(custom_command) = custom_command {
|
||||
return Ok(custom_command);
|
||||
}
|
||||
|
||||
if let Some(path) = find_bin_in_path("gemini", project, cx).await {
|
||||
return Ok(AgentServerCommand {
|
||||
path,
|
||||
args: vec![GEMINI_ACP_ARG.into()],
|
||||
env: None,
|
||||
});
|
||||
}
|
||||
|
||||
let (fs, node_runtime) = project.update(cx, |project, _| {
|
||||
(project.fs().clone(), project.node_runtime().cloned())
|
||||
})?;
|
||||
let node_runtime = node_runtime.context("gemini not found on path")?;
|
||||
|
||||
let directory = ::paths::agent_servers_dir().join("gemini");
|
||||
fs.create_dir(&directory).await?;
|
||||
node_runtime
|
||||
.npm_install_packages(&directory, &[("@google/gemini-cli", "latest")])
|
||||
.await?;
|
||||
let path = directory.join("node_modules/.bin/gemini");
|
||||
|
||||
Ok(AgentServerCommand {
|
||||
path,
|
||||
args: vec![GEMINI_ACP_ARG.into()],
|
||||
env: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
|
||||
|
||||
let current_version = String::from_utf8(version_output?.stdout)?.into();
|
||||
let supported = String::from_utf8(help_output?.stdout)?.contains(GEMINI_ACP_ARG);
|
||||
|
||||
Ok(AgentServerVersion {
|
||||
current_version,
|
||||
supported,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_bin_in_path(
|
||||
bin_name: &'static str,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<PathBuf> {
|
||||
let (env_task, root_dir) = project
|
||||
.update(cx, |project, cx| {
|
||||
let worktree = project.visible_worktrees(cx).next();
|
||||
match worktree {
|
||||
Some(worktree) => {
|
||||
let env_task = project.environment().update(cx, |env, cx| {
|
||||
env.get_worktree_environment(worktree.clone(), cx)
|
||||
});
|
||||
|
||||
let path = worktree.read(cx).abs_path();
|
||||
(env_task, path)
|
||||
}
|
||||
None => {
|
||||
let path: Arc<Path> = paths::home_dir().as_path().into();
|
||||
let env_task = project.environment().update(cx, |env, cx| {
|
||||
env.get_directory_environment(path.clone(), cx)
|
||||
});
|
||||
(env_task, path)
|
||||
}
|
||||
}
|
||||
})
|
||||
.log_err()?;
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let which_result = if cfg!(windows) {
|
||||
which::which(bin_name)
|
||||
} else {
|
||||
let env = env_task.await.unwrap_or_default();
|
||||
let shell_path = env.get("PATH").cloned();
|
||||
which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
|
||||
};
|
||||
|
||||
if let Err(which::Error::CannotFindBinaryPath) = which_result {
|
||||
return None;
|
||||
}
|
||||
|
||||
which_result.log_err()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AgentServerCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let filtered_env = self.env.as_ref().map(|env| {
|
||||
env.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
if util::redact::should_redact(k) {
|
||||
"[REDACTED]"
|
||||
} else {
|
||||
v
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
f.debug_struct("AgentServerCommand")
|
||||
.field("path", &self.path)
|
||||
.field("args", &self.args)
|
||||
.field("env", &filtered_env)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
const KEY: Option<&'static str> = Some("agent_servers");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.gemini.is_some() {
|
||||
settings.gemini = value.gemini.clone();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
@@ -13,14 +13,14 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
"language/test-support",
|
||||
]
|
||||
test-support = ["gpui/test-support", "language/test-support"]
|
||||
|
||||
[dependencies]
|
||||
acp.workspace = true
|
||||
agent.workspace = true
|
||||
agentic-coding-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_servers.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_context.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
@@ -76,6 +76,7 @@ serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
task.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
|
||||
5
crates/agent_ui/src/acp.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod completion_provider;
|
||||
mod message_history;
|
||||
mod thread_view;
|
||||
|
||||
pub use thread_view::AcpThreadView;
|
||||
574
crates/agent_ui/src/acp/completion_provider.rs
Normal file
@@ -0,0 +1,574 @@
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use editor::display_map::CreaseId;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId};
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::MentionLink;
|
||||
use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MentionSet {
|
||||
paths_by_crease_id: HashMap<CreaseId, ProjectPath>,
|
||||
}
|
||||
|
||||
impl MentionSet {
|
||||
pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) {
|
||||
self.paths_by_crease_id.insert(crease_id, path);
|
||||
}
|
||||
|
||||
pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option<ProjectPath> {
|
||||
self.paths_by_crease_id.get(&crease_id).cloned()
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
|
||||
self.paths_by_crease_id.drain().map(|(id, _)| id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextPickerCompletionProvider {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: WeakEntity<Editor>,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
}
|
||||
|
||||
impl ContextPickerCompletionProvider {
|
||||
pub fn new(
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
editor: WeakEntity<Editor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
mention_set,
|
||||
workspace,
|
||||
editor,
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_for_path(
|
||||
project_path: ProjectPath,
|
||||
path_prefix: &str,
|
||||
is_recent: bool,
|
||||
is_directory: bool,
|
||||
excerpt_id: ExcerptId,
|
||||
source_range: Range<Anchor>,
|
||||
editor: Entity<Editor>,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
cx: &App,
|
||||
) -> Completion {
|
||||
let (file_name, directory) =
|
||||
extract_file_name_and_directory(&project_path.path, path_prefix);
|
||||
|
||||
let label =
|
||||
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
|
||||
let full_path = if let Some(directory) = directory {
|
||||
format!("{}{}", directory, file_name)
|
||||
} else {
|
||||
file_name.to_string()
|
||||
};
|
||||
|
||||
let crease_icon_path = if is_directory {
|
||||
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
|
||||
} else {
|
||||
FileIcons::get_icon(Path::new(&full_path), cx)
|
||||
.unwrap_or_else(|| IconName::File.path().into())
|
||||
};
|
||||
let completion_icon_path = if is_recent {
|
||||
IconName::HistoryRerun.path().into()
|
||||
} else {
|
||||
crease_icon_path.clone()
|
||||
};
|
||||
|
||||
let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path));
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label,
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(completion_icon_path),
|
||||
insert_text_mode: None,
|
||||
confirm: Some(confirm_completion_callback(
|
||||
crease_icon_path,
|
||||
file_name,
|
||||
project_path,
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len - 1,
|
||||
editor,
|
||||
mention_set,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
let mut label = CodeLabel::default();
|
||||
|
||||
label.push_str(&file_name, None);
|
||||
label.push_str(" ", None);
|
||||
|
||||
if let Some(directory) = directory {
|
||||
label.push_str(&directory, comment_id);
|
||||
}
|
||||
|
||||
label.filter_range = 0..label.text().len();
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
excerpt_id: ExcerptId,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_trigger: CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let state = buffer.update(cx, |buffer, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let offset_to_line = buffer.point_to_offset(line_start);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
let line = lines.next()?;
|
||||
MentionCompletion::try_parse(line, offset_to_line)
|
||||
});
|
||||
let Some(state) = state else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let source_range = snapshot.anchor_before(state.source_range.start)
|
||||
..snapshot.anchor_after(state.source_range.end);
|
||||
|
||||
let editor = self.editor.clone();
|
||||
let mention_set = self.mention_set.clone();
|
||||
let MentionCompletion { argument, .. } = state;
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
|
||||
let search_task = search_files(query.clone(), Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let matches = search_task.await;
|
||||
let Some(editor) = editor.upgrade() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let completions = cx.update(|cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| {
|
||||
let path_match = &mat.mat;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(path_match.worktree_id),
|
||||
path: path_match.path.clone(),
|
||||
};
|
||||
|
||||
Self::completion_for_path(
|
||||
project_path,
|
||||
&path_match.path_prefix,
|
||||
mat.is_recent,
|
||||
path_match.is_dir,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
mention_set.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})?;
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
// Since this does its own filtering (see `filter_completions()` returns false),
|
||||
// there is no benefit to computing whether this set of completions is incomplete.
|
||||
is_incomplete: true,
|
||||
}])
|
||||
})
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Entity<language::Buffer>,
|
||||
position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
_menu_is_open: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
let buffer = buffer.read(cx);
|
||||
let position = position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let offset_to_line = buffer.point_to_offset(line_start);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
if let Some(line) = lines.next() {
|
||||
MentionCompletion::try_parse(line, offset_to_line)
|
||||
.map(|completion| {
|
||||
completion.source_range.start <= offset_to_line + position.column as usize
|
||||
&& completion.source_range.end >= offset_to_line + position.column as usize
|
||||
})
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_completions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn filter_completions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_completion_callback(
|
||||
crease_icon_path: SharedString,
|
||||
crease_text: SharedString,
|
||||
project_path: ProjectPath,
|
||||
excerpt_id: ExcerptId,
|
||||
start: Anchor,
|
||||
content_len: usize,
|
||||
editor: Entity<Editor>,
|
||||
mention_set: Arc<Mutex<MentionSet>>,
|
||||
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
|
||||
Arc::new(move |_, window, cx| {
|
||||
let crease_text = crease_text.clone();
|
||||
let crease_icon_path = crease_icon_path.clone();
|
||||
let editor = editor.clone();
|
||||
let project_path = project_path.clone();
|
||||
let mention_set = mention_set.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
let crease_id = crate::context_picker::insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
start,
|
||||
content_len,
|
||||
crease_text.clone(),
|
||||
crease_icon_path,
|
||||
editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(crease_id) = crease_id {
|
||||
mention_set.lock().insert(crease_id, project_path);
|
||||
}
|
||||
});
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct MentionCompletion {
|
||||
source_range: Range<usize>,
|
||||
argument: Option<String>,
|
||||
}
|
||||
|
||||
impl MentionCompletion {
|
||||
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
|
||||
let last_mention_start = line.rfind('@')?;
|
||||
if last_mention_start >= line.len() {
|
||||
return Some(Self::default());
|
||||
}
|
||||
if last_mention_start > 0
|
||||
&& line
|
||||
.chars()
|
||||
.nth(last_mention_start - 1)
|
||||
.map_or(false, |c| !c.is_whitespace())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let rest_of_line = &line[last_mention_start + 1..];
|
||||
let mut argument = None;
|
||||
|
||||
let mut parts = rest_of_line.split_whitespace();
|
||||
let mut end = last_mention_start + 1;
|
||||
if let Some(argument_text) = parts.next() {
|
||||
end += argument_text.len();
|
||||
argument = Some(argument_text.to_string());
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
source_range: last_mention_start + offset_to_line..end + offset_to_line,
|
||||
argument,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
|
||||
use project::{Project, ProjectPath};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{ops::Deref, rc::Rc};
|
||||
use util::path;
|
||||
use workspace::{AppState, Item};
|
||||
|
||||
#[test]
|
||||
fn test_mention_completion_parse() {
|
||||
assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse("Lorem @", 0),
|
||||
Some(MentionCompletion {
|
||||
source_range: 6..7,
|
||||
argument: None,
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse("Lorem @main", 0),
|
||||
Some(MentionCompletion {
|
||||
source_range: 6..11,
|
||||
argument: Some("main".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
|
||||
}
|
||||
|
||||
struct AtMentionEditor(Entity<Editor>);
|
||||
|
||||
impl Item for AtMentionEditor {
|
||||
type Event = ();
|
||||
|
||||
fn include_in_nav_history() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Test".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for AtMentionEditor {}
|
||||
|
||||
impl Focusable for AtMentionEditor {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.0.read(cx).focus_handle(cx).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AtMentionEditor {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.0.clone().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_context_completion_provider(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let app_state = cx.update(AppState::test);
|
||||
|
||||
cx.update(|cx| {
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"editor": "",
|
||||
"a": {
|
||||
"one.txt": "",
|
||||
"two.txt": "",
|
||||
"three.txt": "",
|
||||
"four.txt": ""
|
||||
},
|
||||
"b": {
|
||||
"five.txt": "",
|
||||
"six.txt": "",
|
||||
"seven.txt": "",
|
||||
"eight.txt": "",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
let worktree = project.update(cx, |project, cx| {
|
||||
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
worktrees.pop().unwrap()
|
||||
});
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
|
||||
|
||||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||
|
||||
let paths = vec![
|
||||
path!("a/one.txt"),
|
||||
path!("a/two.txt"),
|
||||
path!("a/three.txt"),
|
||||
path!("a/four.txt"),
|
||||
path!("b/five.txt"),
|
||||
path!("b/six.txt"),
|
||||
path!("b/seven.txt"),
|
||||
path!("b/eight.txt"),
|
||||
];
|
||||
|
||||
let mut opened_editors = Vec::new();
|
||||
for path in paths {
|
||||
let buffer = workspace
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new(path).into(),
|
||||
},
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
opened_editors.push(buffer);
|
||||
}
|
||||
|
||||
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let editor = cx.new(|cx| {
|
||||
Editor::new(
|
||||
editor::EditorMode::full(),
|
||||
multi_buffer::MultiBuffer::build_simple("", cx),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
editor
|
||||
});
|
||||
|
||||
let mention_set = Arc::new(Mutex::new(MentionSet::default()));
|
||||
|
||||
let editor_entity = editor.downgrade();
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||
mention_set.clone(),
|
||||
workspace.downgrade(),
|
||||
editor_entity,
|
||||
))));
|
||||
});
|
||||
|
||||
cx.simulate_input("Lorem ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
});
|
||||
|
||||
cx.simulate_input("@");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem @");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
"eight.txt dir/b/",
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
"four.txt dir/a/",
|
||||
"three.txt dir/a/",
|
||||
"two.txt dir/a/",
|
||||
"one.txt dir/a/",
|
||||
"dir ",
|
||||
"a dir/",
|
||||
"four.txt dir/a/",
|
||||
"one.txt dir/a/",
|
||||
"three.txt dir/a/",
|
||||
"two.txt dir/a/",
|
||||
"b dir/",
|
||||
"eight.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"editor dir/"
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Select and confirm "File"
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
|
||||
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
|
||||
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
|
||||
editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
|
||||
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem [@four.txt](@file:dir/a/four.txt) ");
|
||||
});
|
||||
}
|
||||
|
||||
fn current_completion_labels(editor: &Editor) -> Vec<String> {
|
||||
let completions = editor.current_completions().expect("Missing completions");
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|completion| completion.label.text.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub(crate) fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
client::init_settings(cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
editor::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
81
crates/agent_ui/src/acp/message_history.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
pub struct MessageHistory<T> {
|
||||
items: Vec<T>,
|
||||
current: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T> MessageHistory<T> {
|
||||
pub fn new() -> Self {
|
||||
MessageHistory {
|
||||
items: Vec::new(),
|
||||
current: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, message: T) {
|
||||
self.current.take();
|
||||
self.items.push(message);
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) -> Option<&T> {
|
||||
if self.items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_ix = self
|
||||
.current
|
||||
.get_or_insert(self.items.len())
|
||||
.saturating_sub(1);
|
||||
|
||||
self.current = Some(new_ix);
|
||||
self.items.get(new_ix)
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<&T> {
|
||||
let current = self.current.as_mut()?;
|
||||
*current += 1;
|
||||
|
||||
self.items.get(*current).or_else(|| {
|
||||
self.current.take();
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_prev_next() {
|
||||
let mut history = MessageHistory::new();
|
||||
|
||||
// Test empty history
|
||||
assert_eq!(history.prev(), None);
|
||||
assert_eq!(history.next(), None);
|
||||
|
||||
// Add some messages
|
||||
history.push("first");
|
||||
history.push("second");
|
||||
history.push("third");
|
||||
|
||||
// Test prev navigation
|
||||
assert_eq!(history.prev(), Some(&"third"));
|
||||
assert_eq!(history.prev(), Some(&"second"));
|
||||
assert_eq!(history.prev(), Some(&"first"));
|
||||
assert_eq!(history.prev(), Some(&"first"));
|
||||
|
||||
assert_eq!(history.next(), Some(&"second"));
|
||||
|
||||
// Test mixed navigation
|
||||
history.push("fourth");
|
||||
assert_eq!(history.prev(), Some(&"fourth"));
|
||||
assert_eq!(history.prev(), Some(&"third"));
|
||||
assert_eq!(history.next(), Some(&"fourth"));
|
||||
assert_eq!(history.next(), None);
|
||||
|
||||
// Test that push resets navigation
|
||||
history.prev();
|
||||
history.prev();
|
||||
history.push("fifth");
|
||||
assert_eq!(history.prev(), Some(&"fifth"));
|
||||
}
|
||||
}
|
||||
1972
crates/agent_ui/src/acp/thread_view.rs
Normal file
@@ -983,57 +983,30 @@ impl ActiveThread {
|
||||
| ThreadEvent::SummaryChanged => {
|
||||
self.save_thread(cx);
|
||||
}
|
||||
ThreadEvent::Stopped(reason) => {
|
||||
match reason {
|
||||
Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
|
||||
let used_tools = self.thread.read(cx).used_tools_since_last_user_message();
|
||||
self.notify_with_sound(
|
||||
if used_tools {
|
||||
"Finished running tools"
|
||||
} else {
|
||||
"New message"
|
||||
},
|
||||
IconName::ZedAssistant,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
Ok(StopReason::ToolUse) => {
|
||||
// Don't notify for intermediate tool use
|
||||
}
|
||||
Ok(StopReason::Refusal) => {
|
||||
self.notify_with_sound(
|
||||
"Language model refused to respond",
|
||||
IconName::Warning,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
self.notify_with_sound(
|
||||
"Agent stopped due to an error",
|
||||
IconName::Warning,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let error_message = error
|
||||
.chain()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
self.last_error = Some(ThreadError::Message {
|
||||
header: "Error".into(),
|
||||
message: error_message.into(),
|
||||
});
|
||||
}
|
||||
ThreadEvent::Stopped(reason) => match reason {
|
||||
Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
|
||||
let used_tools = self.thread.read(cx).used_tools_since_last_user_message();
|
||||
self.play_notification_sound(window, cx);
|
||||
self.show_notification(
|
||||
if used_tools {
|
||||
"Finished running tools"
|
||||
} else {
|
||||
"New message"
|
||||
},
|
||||
IconName::ZedAssistant,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ThreadEvent::ToolConfirmationNeeded => {
|
||||
self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
|
||||
self.play_notification_sound(window, cx);
|
||||
self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
|
||||
}
|
||||
ThreadEvent::ToolUseLimitReached => {
|
||||
self.notify_with_sound(
|
||||
self.play_notification_sound(window, cx);
|
||||
self.show_notification(
|
||||
"Consecutive tool use limit reached.",
|
||||
IconName::Warning,
|
||||
window,
|
||||
@@ -1176,6 +1149,9 @@ impl ActiveThread {
|
||||
self.save_thread(cx);
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::RetriesFailed { message } => {
|
||||
self.show_notification(message, ui::IconName::Warning, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1230,17 +1206,6 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_with_sound(
|
||||
&mut self,
|
||||
caption: impl Into<SharedString>,
|
||||
icon: IconName,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ActiveThread>,
|
||||
) {
|
||||
self.play_notification_sound(window, cx);
|
||||
self.show_notification(caption, icon, window, cx);
|
||||
}
|
||||
|
||||
fn pop_up(
|
||||
&mut self,
|
||||
icon: IconName,
|
||||
@@ -1496,6 +1461,7 @@ impl ActiveThread {
|
||||
&configured_model.model,
|
||||
cx,
|
||||
),
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
Some(configured_model.model.count_tokens(request, cx))
|
||||
@@ -2615,8 +2581,8 @@ impl ActiveThread {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::LightBulb)
|
||||
.size(IconSize::XSmall)
|
||||
Icon::new(IconName::ToolBulb)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(LoadingLabel::new("Thinking").size(LabelSize::Small)),
|
||||
@@ -3029,7 +2995,7 @@ impl ActiveThread {
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Icon::new(tool_use.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -491,7 +491,6 @@ impl AgentConfiguration {
|
||||
category_filter: Some(
|
||||
ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
id: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
|
||||
@@ -1375,6 +1375,7 @@ impl AgentDiff {
|
||||
| ThreadEvent::ToolConfirmationNeeded
|
||||
| ThreadEvent::ToolUseLimitReached
|
||||
| ThreadEvent::CancelEditing
|
||||
| ThreadEvent::RetriesFailed { .. }
|
||||
| ThreadEvent::ProfileChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ use std::time::Duration;
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::NewAcpThread;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
|
||||
ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
acp::AcpThreadView,
|
||||
active_thread::{self, ActiveThread, ActiveThreadEvent},
|
||||
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
|
||||
agent_diff::AgentDiff,
|
||||
@@ -38,6 +40,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{UserStore, zed_urls};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
||||
@@ -59,9 +62,8 @@ use theme::ThemeSettings;
|
||||
use time::UtcOffset;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
Banner, Button, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, IconPosition,
|
||||
KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName,
|
||||
prelude::*,
|
||||
Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
@@ -110,6 +112,12 @@ pub fn init(cx: &mut App) {
|
||||
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &NewAcpThread, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| panel.new_gemini_thread(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
@@ -126,7 +134,8 @@ pub fn init(cx: &mut App) {
|
||||
let thread = thread.read(cx).thread().clone();
|
||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
|
||||
}
|
||||
ActiveView::TextThread { .. }
|
||||
ActiveView::AcpThread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
}
|
||||
@@ -189,6 +198,9 @@ enum ActiveView {
|
||||
message_editor: Entity<MessageEditor>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
},
|
||||
AcpThread {
|
||||
thread_view: Entity<AcpThreadView>,
|
||||
},
|
||||
TextThread {
|
||||
context_editor: Entity<TextThreadEditor>,
|
||||
title_editor: Entity<Editor>,
|
||||
@@ -208,7 +220,9 @@ enum WhichFontSize {
|
||||
impl ActiveView {
|
||||
pub fn which_font_size_used(&self) -> WhichFontSize {
|
||||
match self {
|
||||
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
|
||||
ActiveView::Thread { .. } | ActiveView::AcpThread { .. } | ActiveView::History => {
|
||||
WhichFontSize::AgentFont
|
||||
}
|
||||
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
|
||||
ActiveView::Configuration => WhichFontSize::None,
|
||||
}
|
||||
@@ -239,6 +253,7 @@ impl ActiveView {
|
||||
thread.scroll_to_bottom(cx);
|
||||
});
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {}
|
||||
ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
@@ -421,7 +436,8 @@ pub struct AgentPanel {
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
hovered_recent_history_item: Option<usize>,
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu: Option<Entity<ContextMenu>>,
|
||||
width: Option<Pixels>,
|
||||
@@ -654,7 +670,8 @@ impl AgentPanel {
|
||||
.clone()
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
|
||||
}
|
||||
ActiveView::TextThread { .. }
|
||||
ActiveView::AcpThread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
},
|
||||
@@ -684,7 +701,8 @@ impl AgentPanel {
|
||||
history_store: history_store.clone(),
|
||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
||||
hovered_recent_history_item: None,
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
|
||||
new_thread_menu_handle: PopoverMenuHandle::default(),
|
||||
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu: None,
|
||||
width: None,
|
||||
@@ -734,6 +752,9 @@ impl AgentPanel {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
|
||||
}
|
||||
ActiveView::AcpThread { thread_view, .. } => {
|
||||
thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
}
|
||||
@@ -741,7 +762,10 @@ impl AgentPanel {
|
||||
fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { message_editor, .. } => Some(message_editor),
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
|
||||
ActiveView::AcpThread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,6 +887,21 @@ impl AgentPanel {
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let thread_view = cx.new_window_entity(|window, cx| {
|
||||
crate::acp::AcpThreadView::new(workspace, project, window, cx)
|
||||
})?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn deploy_rules_library(
|
||||
&mut self,
|
||||
action: &OpenRulesLibrary,
|
||||
@@ -995,6 +1034,7 @@ impl AgentPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
@@ -1026,6 +1066,9 @@ impl AgentPanel {
|
||||
ActiveView::Thread { message_editor, .. } => {
|
||||
message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::AcpThread { thread_view } => {
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
@@ -1053,7 +1096,7 @@ impl AgentPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.assistant_dropdown_menu_handle.toggle(window, cx);
|
||||
self.agent_panel_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn increase_font_size(
|
||||
@@ -1145,7 +1188,10 @@ impl AgentPanel {
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||
ActiveView::AcpThread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1198,6 +1244,13 @@ impl AgentPanel {
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
ActiveView::AcpThread { thread_view } => {
|
||||
thread_view
|
||||
.update(cx, |thread_view, cx| {
|
||||
thread_view.open_thread_as_markdown(workspace, window, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
}
|
||||
@@ -1352,7 +1405,8 @@ impl AgentPanel {
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
ActiveView::AcpThread { .. } => {}
|
||||
ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
|
||||
if current_is_special && !new_is_special {
|
||||
@@ -1438,6 +1492,7 @@ impl Focusable for AgentPanel {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
|
||||
ActiveView::AcpThread { thread_view, .. } => thread_view.focus_handle(cx),
|
||||
ActiveView::History => self.history.focus_handle(cx),
|
||||
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
|
||||
ActiveView::Configuration => {
|
||||
@@ -1594,6 +1649,9 @@ impl AgentPanel {
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
ActiveView::AcpThread { thread_view } => Label::new(thread_view.read(cx).title(cx))
|
||||
.truncate()
|
||||
.into_any_element(),
|
||||
ActiveView::TextThread {
|
||||
title_editor,
|
||||
context_editor,
|
||||
@@ -1728,10 +1786,51 @@ impl AgentPanel {
|
||||
|
||||
let active_thread = match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
|
||||
ActiveView::AcpThread { .. }
|
||||
| ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => None,
|
||||
};
|
||||
|
||||
let agent_extra_menu = PopoverMenu::new("agent-options-menu")
|
||||
let new_thread_menu = PopoverMenu::new("new_thread_menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
|
||||
Tooltip::text("New Thread…"),
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
menu = menu
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.header("Zed Agent")
|
||||
})
|
||||
.action("New Thread", NewThread::default().boxed_clone())
|
||||
.action("New Text Thread", NewTextThread.boxed_clone())
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
if !thread.is_empty() {
|
||||
this.action(
|
||||
"New From Summary",
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread.id().clone()),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.separator()
|
||||
.header("External Agents")
|
||||
.action("New Gemini Thread", NewAcpThread.boxed_clone())
|
||||
});
|
||||
menu
|
||||
}))
|
||||
});
|
||||
|
||||
let agent_panel_menu = PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
.icon_size(IconSize::Small),
|
||||
@@ -1749,42 +1848,9 @@ impl AgentPanel {
|
||||
},
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.assistant_dropdown_menu_handle.clone())
|
||||
.with_handle(self.agent_panel_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
menu = menu
|
||||
.action("New Thread", NewThread::default().boxed_clone())
|
||||
.action("New Text Thread", NewTextThread.boxed_clone())
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
if !thread.is_empty() {
|
||||
this.action(
|
||||
"New From Summary",
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread.id().clone()),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.separator();
|
||||
|
||||
menu = menu
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
id: None,
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server…", Box::new(AddContextServer))
|
||||
.separator();
|
||||
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
|
||||
if let Some(usage) = usage {
|
||||
menu = menu
|
||||
.header_with_link("Prompt Usage", "Manage", account_url.clone())
|
||||
@@ -1822,6 +1888,19 @@ impl AgentPanel {
|
||||
.separator()
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server…", Box::new(AddContextServer))
|
||||
.separator();
|
||||
|
||||
menu = menu
|
||||
.action("Rules…", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
@@ -1863,71 +1942,55 @@ impl AgentPanel {
|
||||
.px(DynamicSpacing::Base08.rems(cx))
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
IconButton::new("new", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"New Thread",
|
||||
&NewThread::default(),
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
NewThread::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.child(agent_extra_menu),
|
||||
.child(new_thread_menu)
|
||||
.child(agent_panel_menu),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
|
||||
match &self.active_view {
|
||||
let (active_thread, message_editor) = match &self.active_view {
|
||||
ActiveView::Thread {
|
||||
thread,
|
||||
message_editor,
|
||||
..
|
||||
} => {
|
||||
let active_thread = thread.read(cx);
|
||||
let message_editor = message_editor.read(cx);
|
||||
} => (thread.read(cx), message_editor.read(cx)),
|
||||
ActiveView::AcpThread { .. } => {
|
||||
return None;
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let editor_empty = message_editor.is_editor_fully_empty(cx);
|
||||
let editor_empty = message_editor.is_editor_fully_empty(cx);
|
||||
|
||||
if active_thread.is_empty() && editor_empty {
|
||||
return None;
|
||||
}
|
||||
if active_thread.is_empty() && editor_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
let thread = active_thread.thread().read(cx);
|
||||
let is_generating = thread.is_generating();
|
||||
let conversation_token_usage = thread.total_token_usage()?;
|
||||
let thread = active_thread.thread().read(cx);
|
||||
let is_generating = thread.is_generating();
|
||||
let conversation_token_usage = thread.total_token_usage()?;
|
||||
|
||||
let (total_token_usage, is_estimating) =
|
||||
if let Some((editing_message_id, unsent_tokens)) =
|
||||
active_thread.editing_message_id()
|
||||
{
|
||||
let combined = thread
|
||||
.token_usage_up_to_message(editing_message_id)
|
||||
.add(unsent_tokens);
|
||||
let (total_token_usage, is_estimating) =
|
||||
if let Some((editing_message_id, unsent_tokens)) = active_thread.editing_message_id() {
|
||||
let combined = thread
|
||||
.token_usage_up_to_message(editing_message_id)
|
||||
.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
} else {
|
||||
let unsent_tokens =
|
||||
message_editor.last_estimated_token_count().unwrap_or(0);
|
||||
let combined = conversation_token_usage.add(unsent_tokens);
|
||||
(combined, unsent_tokens > 0)
|
||||
} else {
|
||||
let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
|
||||
let combined = conversation_token_usage.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
};
|
||||
(combined, unsent_tokens > 0)
|
||||
};
|
||||
|
||||
let is_waiting_to_update_token_count =
|
||||
message_editor.is_waiting_to_update_token_count();
|
||||
let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } => {
|
||||
if total_token_usage.total == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -2033,6 +2096,9 @@ impl AgentPanel {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
return false;
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||
return false;
|
||||
}
|
||||
@@ -2617,6 +2683,9 @@ impl AgentPanel {
|
||||
) -> Option<AnyElement> {
|
||||
let active_thread = match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => thread,
|
||||
ActiveView::AcpThread { .. } => {
|
||||
return None;
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||
return None;
|
||||
}
|
||||
@@ -2821,21 +2890,6 @@ impl AgentPanel {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
let retry_button = Button::new("retry", "Retry")
|
||||
.icon(IconName::RotateCw)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click({
|
||||
let thread = thread.clone();
|
||||
move |_, window, cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.clear_last_error();
|
||||
thread.thread().update(cx, |thread, cx| {
|
||||
thread.retry_last_completion(Some(window.window_handle()), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
@@ -2844,72 +2898,13 @@ impl AgentPanel {
|
||||
.icon(icon)
|
||||
.title(header)
|
||||
.description(message.clone())
|
||||
.primary_action(retry_button)
|
||||
.secondary_action(self.dismiss_error_button(thread, cx))
|
||||
.tertiary_action(self.create_copy_button(message_with_header))
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(message_with_header))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_retryable_error(
|
||||
&self,
|
||||
message: SharedString,
|
||||
can_enable_burn_mode: bool,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
let retry_button = Button::new("retry", "Retry")
|
||||
.icon(IconName::RotateCw)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click({
|
||||
let thread = thread.clone();
|
||||
move |_, window, cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.clear_last_error();
|
||||
thread.thread().update(cx, |thread, cx| {
|
||||
thread.retry_last_completion(Some(window.window_handle()), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let mut callout = Callout::new()
|
||||
.icon(icon)
|
||||
.title("Error")
|
||||
.description(message.clone())
|
||||
.bg_color(self.error_callout_bg(cx))
|
||||
.primary_action(retry_button);
|
||||
|
||||
if can_enable_burn_mode {
|
||||
let burn_mode_button = Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
|
||||
.icon(IconName::ZedBurnMode)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click({
|
||||
let thread = thread.clone();
|
||||
move |_, window, cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.clear_last_error();
|
||||
thread.thread().update(cx, |thread, cx| {
|
||||
thread.enable_burn_mode_and_retry(Some(window.window_handle()), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
callout = callout.secondary_action(burn_mode_button);
|
||||
}
|
||||
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(callout)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_prompt_editor(
|
||||
&self,
|
||||
context_editor: &Entity<TextThreadEditor>,
|
||||
@@ -3037,6 +3032,9 @@ impl AgentPanel {
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
unimplemented!()
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
TextThreadEditor::insert_dragged_files(
|
||||
@@ -3055,8 +3053,10 @@ impl AgentPanel {
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
if matches!(self.active_view, ActiveView::TextThread { .. }) {
|
||||
key_context.add("prompt_editor");
|
||||
match &self.active_view {
|
||||
ActiveView::AcpThread { .. } => key_context.add("acp_thread"),
|
||||
ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
|
||||
ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
key_context
|
||||
}
|
||||
@@ -3110,6 +3110,7 @@ impl Render for AgentPanel {
|
||||
});
|
||||
this.continue_conversation(window, cx);
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {}
|
||||
ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
@@ -3145,21 +3146,16 @@ impl Render for AgentPanel {
|
||||
ThreadError::Message { header, message } => {
|
||||
self.render_error_message(header, message, thread, cx)
|
||||
}
|
||||
ThreadError::RetryableError {
|
||||
message,
|
||||
can_enable_burn_mode,
|
||||
} => self.render_retryable_error(
|
||||
message,
|
||||
can_enable_burn_mode,
|
||||
thread,
|
||||
cx,
|
||||
),
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
})
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::AcpThread { thread_view, .. } => parent
|
||||
.relative()
|
||||
.child(thread_view.clone())
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::TextThread {
|
||||
context_editor,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod acp;
|
||||
mod active_thread;
|
||||
mod agent_configuration;
|
||||
mod agent_diff;
|
||||
@@ -56,6 +57,8 @@ actions!(
|
||||
[
|
||||
/// Creates a new text-based conversation thread.
|
||||
NewTextThread,
|
||||
/// Creates a new external agent conversation thread.
|
||||
NewAcpThread,
|
||||
/// Toggles the context picker interface for adding files, symbols, or other context.
|
||||
ToggleContextPicker,
|
||||
/// Toggles the navigation menu for switching between threads and views.
|
||||
@@ -76,8 +79,6 @@ actions!(
|
||||
AddContextServer,
|
||||
/// Removes the currently selected thread.
|
||||
RemoveSelectedThread,
|
||||
/// Starts a chat conversation with the agent.
|
||||
Chat,
|
||||
/// Starts a chat conversation with follow-up enabled.
|
||||
ChatWithFollow,
|
||||
/// Cycles to the next inline assist suggestion.
|
||||
|
||||
@@ -475,6 +475,7 @@ impl CodegenAlternative {
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
messages: vec![request_message],
|
||||
thinking_allowed: false,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod completion_provider;
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
pub(crate) mod file_context_picker;
|
||||
mod rules_context_picker;
|
||||
mod symbol_context_picker;
|
||||
mod thread_context_picker;
|
||||
|
||||
@@ -47,13 +47,14 @@ use ui::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::{
|
||||
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
|
||||
ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
|
||||
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
|
||||
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
|
||||
};
|
||||
@@ -1453,6 +1454,7 @@ impl MessageEditor {
|
||||
tool_choice: None,
|
||||
stop: vec![],
|
||||
temperature: AgentSettings::temperature_for_model(&model.model, cx),
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
Some(model.model.count_tokens(request, cx))
|
||||
@@ -1620,6 +1622,7 @@ impl Render for MessageEditor {
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.when(changed_buffers.len() > 0, |parent| {
|
||||
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
|
||||
})
|
||||
|
||||
@@ -297,6 +297,7 @@ impl TerminalInlineAssistant {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
thinking_allowed: false,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -2293,6 +2293,7 @@ impl AssistantContext {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: model.and_then(|model| AgentSettings::temperature_for_model(model, cx)),
|
||||
thinking_allowed: true,
|
||||
};
|
||||
for message in self.messages(cx) {
|
||||
if message.status != MessageStatus::Done {
|
||||
|
||||
@@ -57,7 +57,7 @@ impl Tool for CopyPathTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Clipboard
|
||||
IconName::ToolCopy
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -46,7 +46,7 @@ impl Tool for CreateDirectoryTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Folder
|
||||
IconName::ToolFolder
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -46,7 +46,7 @@ impl Tool for DeletePathTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FileDelete
|
||||
IconName::ToolDeleteFile
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -59,7 +59,7 @@ impl Tool for DiagnosticsTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::XCircle
|
||||
IconName::ToolDiagnostics
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -719,6 +719,7 @@ impl EditAgent {
|
||||
tools,
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
Ok(self.model.stream_completion_text(request, cx).await?.stream)
|
||||
|
||||
@@ -12,7 +12,6 @@ use collections::HashMap;
|
||||
use fs::FakeFs;
|
||||
use futures::{FutureExt, future::LocalBoxFuture};
|
||||
use gpui::{AppContext, TestAppContext, Timer};
|
||||
use http_client::StatusCode;
|
||||
use indoc::{formatdoc, indoc};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
@@ -1264,6 +1263,7 @@ impl EvalAssertion {
|
||||
content: vec![prompt.into()],
|
||||
cache: false,
|
||||
}],
|
||||
thinking_allowed: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut response = retry_on_rate_limit(async || {
|
||||
@@ -1600,6 +1600,7 @@ impl EditAgentTest {
|
||||
let conversation = LanguageModelRequest {
|
||||
messages,
|
||||
tools,
|
||||
thinking_allowed: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1672,30 +1673,6 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
|
||||
Timer::after(retry_after + jitter).await;
|
||||
continue;
|
||||
}
|
||||
LanguageModelCompletionError::UpstreamProviderError {
|
||||
status,
|
||||
retry_after,
|
||||
..
|
||||
} => {
|
||||
// Only retry for specific status codes
|
||||
let should_retry = matches!(
|
||||
*status,
|
||||
StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE
|
||||
) || status.as_u16() == 529;
|
||||
|
||||
if !should_retry {
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
// Use server-provided retry_after if available, otherwise use default
|
||||
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
|
||||
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
eprintln!(
|
||||
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
|
||||
);
|
||||
Timer::after(retry_after + jitter).await;
|
||||
continue;
|
||||
}
|
||||
_ => return Err(err.into()),
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
|
||||
@@ -139,7 +139,7 @@ impl Tool for EditFileTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
IconName::ToolPencil
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
@@ -783,8 +783,8 @@ impl ToolCard for EditFileToolCard {
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Icon::new(IconName::Pencil)
|
||||
.size(IconSize::XSmall)
|
||||
Icon::new(IconName::ToolPencil)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -130,7 +130,7 @@ impl Tool for FetchTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Globe
|
||||
IconName::ToolWeb
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Tool for FindPathTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::SearchCode
|
||||
IconName::ToolSearch
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
@@ -313,7 +313,7 @@ impl ToolCard for FindPathToolCard {
|
||||
.mb_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
ToolCallCardHeader::new(IconName::SearchCode, matches_label)
|
||||
ToolCallCardHeader::new(IconName::ToolSearch, matches_label)
|
||||
.with_code_path(&self.glob)
|
||||
.disclosure_slot(
|
||||
Disclosure::new("path-search-disclosure", self.expanded)
|
||||
|
||||
@@ -70,7 +70,7 @@ impl Tool for GrepTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Regex
|
||||
IconName::ToolRegex
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -58,7 +58,7 @@ impl Tool for ListDirectoryTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Folder
|
||||
IconName::ToolFolder
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -31,7 +31,7 @@ impl Tool for ProjectNotificationsTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Envelope
|
||||
IconName::ToolNotification
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FileSearch
|
||||
IconName::ToolRead
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -90,7 +90,7 @@ impl Tool for TerminalTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Terminal
|
||||
IconName::ToolTerminal
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -37,7 +37,7 @@ impl Tool for ThinkingTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::LightBulb
|
||||
IconName::ToolBulb
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -82,7 +82,7 @@ impl RenderOnce for ToolCallCardHeader {
|
||||
.child(
|
||||
h_flex().h(line_height).justify_center().child(
|
||||
Icon::new(self.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -143,6 +143,8 @@ impl ToolCard for WebSearchToolCard {
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let icon = IconName::ToolWeb;
|
||||
|
||||
let header = match self.response.as_ref() {
|
||||
Some(Ok(response)) => {
|
||||
let text: SharedString = if response.results.len() == 1 {
|
||||
@@ -150,13 +152,12 @@ impl ToolCard for WebSearchToolCard {
|
||||
} else {
|
||||
format!("{} results", response.results.len()).into()
|
||||
};
|
||||
ToolCallCardHeader::new(IconName::Globe, "Searched the Web")
|
||||
.with_secondary_text(text)
|
||||
ToolCallCardHeader::new(icon, "Searched the Web").with_secondary_text(text)
|
||||
}
|
||||
Some(Err(error)) => {
|
||||
ToolCallCardHeader::new(IconName::Globe, "Web Search").with_error(error.to_string())
|
||||
ToolCallCardHeader::new(icon, "Web Search").with_error(error.to_string())
|
||||
}
|
||||
None => ToolCallCardHeader::new(IconName::Globe, "Searching the Web").loading(),
|
||||
None => ToolCallCardHeader::new(icon, "Searching the Web").loading(),
|
||||
};
|
||||
|
||||
let content = self.response.as_ref().and_then(|response| match response {
|
||||
|
||||
@@ -94,7 +94,6 @@ context_server.workspace = true
|
||||
ctor.workspace = true
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
dap_adapters = { workspace = true, features = ["test-support"] }
|
||||
dap-types.workspace = true
|
||||
debugger_ui = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
extension.workspace = true
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::tests::TestServer;
|
||||
use call::ActiveCall;
|
||||
use collections::{HashMap, HashSet};
|
||||
|
||||
use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
@@ -23,7 +22,6 @@ use language::{
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::session::ThreadId,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
};
|
||||
use remote::SshRemoteClient;
|
||||
@@ -31,11 +29,7 @@ use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::path;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -694,162 +688,3 @@ async fn test_remote_server_debugger(
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slow_adapter_startup_retries(
|
||||
cx_a: &mut TestAppContext,
|
||||
server_cx: &mut TestAppContext,
|
||||
executor: BackgroundExecutor,
|
||||
) {
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
command_palette_hooks::init(cx);
|
||||
zlog::init_test();
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
server_cx.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
path!("/code"),
|
||||
json!({
|
||||
"lib.rs": "fn one() -> usize { 1 }"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// User A connects to the remote project via SSH.
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let remote_http_client = Arc::new(BlockedHttpClient);
|
||||
let node = NodeRuntime::unavailable();
|
||||
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
||||
let _headless_project = server_cx.new(|cx| {
|
||||
client::init_settings(cx);
|
||||
HeadlessProject::new(
|
||||
HeadlessAppState {
|
||||
session: server_ssh,
|
||||
fs: remote_fs.clone(),
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let mut server = TestServer::start(server_cx.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
cx_a.update(|cx| {
|
||||
debugger_ui::init(cx);
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
|
||||
let debugger_panel = workspace
|
||||
.update_in(cx_a, |_workspace, window, cx| {
|
||||
cx.spawn_in(window, DebugPanel::load)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
workspace.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.add_panel(debugger_panel, window, cx);
|
||||
});
|
||||
|
||||
cx_a.run_until_parked();
|
||||
let debug_panel = workspace
|
||||
.update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.unwrap();
|
||||
|
||||
let workspace_window = cx_a
|
||||
.window_handle()
|
||||
.downcast::<workspace::Workspace>()
|
||||
.unwrap();
|
||||
|
||||
let count = Arc::new(AtomicUsize::new(0));
|
||||
let session = debugger_ui::tests::start_debug_session_with(
|
||||
&workspace_window,
|
||||
cx_a,
|
||||
DebugTaskDefinition {
|
||||
adapter: "fake-adapter".into(),
|
||||
label: "test".into(),
|
||||
config: json!({
|
||||
"request": "launch"
|
||||
}),
|
||||
tcp_connection: Some(TcpArgumentsTemplate {
|
||||
port: None,
|
||||
host: None,
|
||||
timeout: None,
|
||||
}),
|
||||
},
|
||||
move |client| {
|
||||
let count = count.clone();
|
||||
client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
|
||||
if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
|
||||
return RequestHandling::Exit;
|
||||
}
|
||||
RequestHandling::Respond(Ok(Capabilities::default()))
|
||||
});
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx_a.run_until_parked();
|
||||
|
||||
let active_session = debug_panel
|
||||
.update(cx_a, |this, _| this.active_session())
|
||||
.unwrap();
|
||||
|
||||
let running_state = active_session.update(cx_a, |active_session, _| {
|
||||
active_session.running_state().clone()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
client.id(),
|
||||
running_state.read_with(cx_a, |running_state, _| running_state.session_id())
|
||||
);
|
||||
assert_eq!(
|
||||
ThreadId(1),
|
||||
running_state.read_with(cx_a, |running_state, _| running_state
|
||||
.selected_thread_id()
|
||||
.unwrap())
|
||||
);
|
||||
|
||||
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(session.read(cx).session_id(), cx)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
client_ssh.update(cx_a, |a, _| {
|
||||
a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
@@ -442,18 +442,10 @@ impl DebugAdapter for FakeAdapter {
|
||||
_: Option<Vec<String>>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let connection = task_definition
|
||||
.tcp_connection
|
||||
.as_ref()
|
||||
.map(|connection| TcpArguments {
|
||||
host: connection.host(),
|
||||
port: connection.port.unwrap_or(17),
|
||||
timeout: connection.timeout,
|
||||
});
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some("command".into()),
|
||||
arguments: vec![],
|
||||
connection,
|
||||
connection: None,
|
||||
envs: HashMap::default(),
|
||||
cwd: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
adapters::DebugAdapterBinary,
|
||||
transport::{IoKind, LogKind, TransportDelegate},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use dap_types::{
|
||||
messages::{Message, Response},
|
||||
requests::Request,
|
||||
@@ -110,7 +110,9 @@ impl DebugAdapterClient {
|
||||
self.transport_delegate
|
||||
.pending_requests
|
||||
.lock()
|
||||
.insert(sequence_id, callback_tx)?;
|
||||
.as_mut()
|
||||
.context("client is closed")?
|
||||
.insert(sequence_id, callback_tx);
|
||||
|
||||
log::debug!(
|
||||
"Client {} send `{}` request with sequence_id: {}",
|
||||
@@ -168,7 +170,6 @@ impl DebugAdapterClient {
|
||||
pub fn kill(&self) {
|
||||
log::debug!("Killing DAP process");
|
||||
self.transport_delegate.transport.lock().kill();
|
||||
self.transport_delegate.pending_requests.lock().shutdown();
|
||||
}
|
||||
|
||||
pub fn has_adapter_logs(&self) -> bool {
|
||||
@@ -183,34 +184,11 @@ impl DebugAdapterClient {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
|
||||
pub fn on_request<R: dap_types::requests::Request, F>(&self, handler: F)
|
||||
where
|
||||
F: 'static
|
||||
+ Send
|
||||
+ FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
|
||||
{
|
||||
use crate::transport::RequestHandling;
|
||||
|
||||
self.transport_delegate
|
||||
.transport
|
||||
.lock()
|
||||
.as_fake()
|
||||
.on_request::<R, _>(move |seq, request| {
|
||||
RequestHandling::Respond(handler(seq, request))
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn on_request_ext<R: dap_types::requests::Request, F>(&self, handler: F)
|
||||
where
|
||||
F: 'static
|
||||
+ Send
|
||||
+ FnMut(
|
||||
u64,
|
||||
R::Arguments,
|
||||
) -> crate::transport::RequestHandling<
|
||||
Result<R::Response, dap_types::ErrorResponse>,
|
||||
>,
|
||||
{
|
||||
self.transport_delegate
|
||||
.transport
|
||||
|
||||
@@ -49,12 +49,6 @@ pub enum IoKind {
|
||||
StdErr,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub enum RequestHandling<T> {
|
||||
Respond(T),
|
||||
Exit,
|
||||
}
|
||||
|
||||
type LogHandlers = Arc<Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
|
||||
|
||||
pub trait Transport: Send + Sync {
|
||||
@@ -82,11 +76,7 @@ async fn start(
|
||||
) -> Result<Box<dyn Transport>> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if cfg!(any(test, feature = "test-support")) {
|
||||
if let Some(connection) = binary.connection.clone() {
|
||||
return Ok(Box::new(FakeTransport::start_tcp(connection, cx).await?));
|
||||
} else {
|
||||
return Ok(Box::new(FakeTransport::start_stdio(cx).await?));
|
||||
}
|
||||
return Ok(Box::new(FakeTransport::start(cx).await?));
|
||||
}
|
||||
|
||||
if binary.connection.is_some() {
|
||||
@@ -100,57 +90,11 @@ async fn start(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PendingRequests {
|
||||
inner: Option<HashMap<u64, oneshot::Sender<Result<Response>>>>,
|
||||
}
|
||||
|
||||
impl PendingRequests {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: Some(HashMap::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, e: anyhow::Error) {
|
||||
let Some(inner) = self.inner.as_mut() else {
|
||||
return;
|
||||
};
|
||||
for (_, sender) in inner.drain() {
|
||||
sender.send(Err(e.cloned())).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn insert(
|
||||
&mut self,
|
||||
sequence_id: u64,
|
||||
callback_tx: oneshot::Sender<Result<Response>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(inner) = self.inner.as_mut() else {
|
||||
bail!("client is closed")
|
||||
};
|
||||
inner.insert(sequence_id, callback_tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove(
|
||||
&mut self,
|
||||
sequence_id: u64,
|
||||
) -> anyhow::Result<Option<oneshot::Sender<Result<Response>>>> {
|
||||
let Some(inner) = self.inner.as_mut() else {
|
||||
bail!("client is closed");
|
||||
};
|
||||
Ok(inner.remove(&sequence_id))
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&mut self) {
|
||||
self.flush(anyhow!("transport shutdown"));
|
||||
self.inner = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TransportDelegate {
|
||||
log_handlers: LogHandlers,
|
||||
pub(crate) pending_requests: Arc<Mutex<PendingRequests>>,
|
||||
// TODO this should really be some kind of associative channel
|
||||
pub(crate) pending_requests:
|
||||
Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
|
||||
pub(crate) transport: Mutex<Box<dyn Transport>>,
|
||||
pub(crate) server_tx: smol::lock::Mutex<Option<Sender<Message>>>,
|
||||
tasks: Mutex<Vec<Task<()>>>,
|
||||
@@ -164,7 +108,7 @@ impl TransportDelegate {
|
||||
transport: Mutex::new(transport),
|
||||
log_handlers,
|
||||
server_tx: Default::default(),
|
||||
pending_requests: Arc::new(Mutex::new(PendingRequests::new())),
|
||||
pending_requests: Arc::new(Mutex::new(Some(HashMap::default()))),
|
||||
tasks: Default::default(),
|
||||
})
|
||||
}
|
||||
@@ -207,10 +151,24 @@ impl TransportDelegate {
|
||||
Ok(()) => {
|
||||
pending_requests
|
||||
.lock()
|
||||
.flush(anyhow!("debugger shutdown unexpectedly"));
|
||||
.take()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|(_, request)| {
|
||||
request
|
||||
.send(Err(anyhow!("debugger shutdown unexpectedly")))
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
pending_requests.lock().flush(e);
|
||||
pending_requests
|
||||
.lock()
|
||||
.take()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|(_, request)| {
|
||||
request.send(Err(e.cloned())).ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -328,7 +286,7 @@ impl TransportDelegate {
|
||||
async fn recv_from_server<Stdout>(
|
||||
server_stdout: Stdout,
|
||||
mut message_handler: DapMessageHandler,
|
||||
pending_requests: Arc<Mutex<PendingRequests>>,
|
||||
pending_requests: Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
|
||||
log_handlers: Option<LogHandlers>,
|
||||
) -> Result<()>
|
||||
where
|
||||
@@ -345,10 +303,14 @@ impl TransportDelegate {
|
||||
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
return Ok(());
|
||||
break Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Ok(Message::Response(res))) => {
|
||||
let tx = pending_requests.lock().remove(res.request_seq)?;
|
||||
let tx = pending_requests
|
||||
.lock()
|
||||
.as_mut()
|
||||
.context("client is closed")?
|
||||
.remove(&res.request_seq);
|
||||
if let Some(tx) = tx {
|
||||
if let Err(e) = tx.send(Self::process_response(res)) {
|
||||
log::trace!("Did not send response `{:?}` for a cancelled", e);
|
||||
@@ -742,7 +704,8 @@ impl Drop for StdioTransport {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
type RequestHandler = Box<dyn Send + FnMut(u64, serde_json::Value) -> RequestHandling<Response>>;
|
||||
type RequestHandler =
|
||||
Box<dyn Send + FnMut(u64, serde_json::Value) -> dap_types::messages::Response>;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
type ResponseHandler = Box<dyn Send + Fn(Response)>;
|
||||
@@ -753,38 +716,23 @@ pub struct FakeTransport {
|
||||
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
|
||||
// for reverse request responses
|
||||
response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
|
||||
message_handler: Option<Task<Result<()>>>,
|
||||
kind: FakeTransportKind,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub enum FakeTransportKind {
|
||||
Stdio {
|
||||
stdin_writer: Option<PipeWriter>,
|
||||
stdout_reader: Option<PipeReader>,
|
||||
},
|
||||
Tcp {
|
||||
connection: TcpArguments,
|
||||
executor: BackgroundExecutor,
|
||||
},
|
||||
stdin_writer: Option<PipeWriter>,
|
||||
stdout_reader: Option<PipeReader>,
|
||||
message_handler: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeTransport {
|
||||
pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
|
||||
where
|
||||
F: 'static
|
||||
+ Send
|
||||
+ FnMut(u64, R::Arguments) -> RequestHandling<Result<R::Response, ErrorResponse>>,
|
||||
F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, ErrorResponse>,
|
||||
{
|
||||
self.request_handlers.lock().insert(
|
||||
R::COMMAND,
|
||||
Box::new(move |seq, args| {
|
||||
let result = handler(seq, serde_json::from_value(args).unwrap());
|
||||
let RequestHandling::Respond(response) = result else {
|
||||
return RequestHandling::Exit;
|
||||
};
|
||||
let response = match response {
|
||||
let response = match result {
|
||||
Ok(response) => Response {
|
||||
seq: seq + 1,
|
||||
request_seq: seq,
|
||||
@@ -802,7 +750,7 @@ impl FakeTransport {
|
||||
message: None,
|
||||
},
|
||||
};
|
||||
RequestHandling::Respond(response)
|
||||
response
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -816,75 +764,86 @@ impl FakeTransport {
|
||||
.insert(R::COMMAND, Box::new(handler));
|
||||
}
|
||||
|
||||
async fn start_tcp(connection: TcpArguments, cx: &mut AsyncApp) -> Result<Self> {
|
||||
Ok(Self {
|
||||
request_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
response_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
message_handler: None,
|
||||
kind: FakeTransportKind::Tcp {
|
||||
connection,
|
||||
executor: cx.background_executor().clone(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_messages(
|
||||
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
|
||||
response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
|
||||
stdin_reader: PipeReader,
|
||||
stdout_writer: PipeWriter,
|
||||
) -> Result<()> {
|
||||
async fn start(cx: &mut AsyncApp) -> Result<Self> {
|
||||
use dap_types::requests::{Request, RunInTerminal, StartDebugging};
|
||||
use serde_json::json;
|
||||
|
||||
let mut reader = BufReader::new(stdin_reader);
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
|
||||
let mut this = Self {
|
||||
request_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
response_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
stdin_writer: Some(stdin_writer),
|
||||
stdout_reader: Some(stdout_reader),
|
||||
message_handler: None,
|
||||
};
|
||||
|
||||
let request_handlers = this.request_handlers.clone();
|
||||
let response_handlers = this.response_handlers.clone();
|
||||
let stdout_writer = Arc::new(smol::lock::Mutex::new(stdout_writer));
|
||||
let mut buffer = String::new();
|
||||
|
||||
loop {
|
||||
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None).await {
|
||||
ConnectionResult::Timeout => {
|
||||
anyhow::bail!("Timed out when connecting to debugger");
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
break Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Err(e)) => break Err(e),
|
||||
ConnectionResult::Result(Ok(message)) => {
|
||||
match message {
|
||||
Message::Request(request) => {
|
||||
// redirect reverse requests to stdout writer/reader
|
||||
if request.command == RunInTerminal::COMMAND
|
||||
|| request.command == StartDebugging::COMMAND
|
||||
{
|
||||
let message =
|
||||
serde_json::to_string(&Message::Request(request)).unwrap();
|
||||
this.message_handler = Some(cx.background_spawn(async move {
|
||||
let mut reader = BufReader::new(stdin_reader);
|
||||
let mut buffer = String::new();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message).as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
} else {
|
||||
let response = if let Some(handle) =
|
||||
request_handlers.lock().get_mut(request.command.as_str())
|
||||
loop {
|
||||
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
|
||||
.await
|
||||
{
|
||||
ConnectionResult::Timeout => {
|
||||
anyhow::bail!("Timed out when connecting to debugger");
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::info!("Debugger closed the connection");
|
||||
break Ok(());
|
||||
}
|
||||
ConnectionResult::Result(Err(e)) => break Err(e),
|
||||
ConnectionResult::Result(Ok(message)) => {
|
||||
match message {
|
||||
Message::Request(request) => {
|
||||
// redirect reverse requests to stdout writer/reader
|
||||
if request.command == RunInTerminal::COMMAND
|
||||
|| request.command == StartDebugging::COMMAND
|
||||
{
|
||||
handle(request.seq, request.arguments.unwrap_or(json!({})))
|
||||
let message =
|
||||
serde_json::to_string(&Message::Request(request)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
} else {
|
||||
panic!("No request handler for {}", request.command);
|
||||
};
|
||||
let response = match response {
|
||||
RequestHandling::Respond(response) => response,
|
||||
RequestHandling::Exit => {
|
||||
break Err(anyhow!("exit in response to request"));
|
||||
}
|
||||
};
|
||||
let response = if let Some(handle) =
|
||||
request_handlers.lock().get_mut(request.command.as_str())
|
||||
{
|
||||
handle(request.seq, request.arguments.unwrap_or(json!({})))
|
||||
} else {
|
||||
panic!("No request handler for {}", request.command);
|
||||
};
|
||||
let message =
|
||||
serde_json::to_string(&Message::Response(response))
|
||||
.unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
}
|
||||
Message::Event(event) => {
|
||||
let message =
|
||||
serde_json::to_string(&Message::Response(response)).unwrap();
|
||||
serde_json::to_string(&Message::Event(event)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
@@ -895,56 +854,20 @@ impl FakeTransport {
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
}
|
||||
Message::Event(event) => {
|
||||
let message = serde_json::to_string(&Message::Event(event)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(TransportDelegate::build_rpc_message(message).as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
Message::Response(response) => {
|
||||
if let Some(handle) =
|
||||
response_handlers.lock().get(response.command.as_str())
|
||||
{
|
||||
handle(response);
|
||||
} else {
|
||||
log::error!("No response handler for {}", response.command);
|
||||
Message::Response(response) => {
|
||||
if let Some(handle) =
|
||||
response_handlers.lock().get(response.command.as_str())
|
||||
{
|
||||
handle(response);
|
||||
} else {
|
||||
log::error!("No response handler for {}", response.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_stdio(cx: &mut AsyncApp) -> Result<Self> {
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
let kind = FakeTransportKind::Stdio {
|
||||
stdin_writer: Some(stdin_writer),
|
||||
stdout_reader: Some(stdout_reader),
|
||||
};
|
||||
|
||||
let mut this = Self {
|
||||
request_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
response_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
message_handler: None,
|
||||
kind,
|
||||
};
|
||||
|
||||
let request_handlers = this.request_handlers.clone();
|
||||
let response_handlers = this.response_handlers.clone();
|
||||
|
||||
this.message_handler = Some(cx.background_spawn(Self::handle_messages(
|
||||
request_handlers,
|
||||
response_handlers,
|
||||
stdin_reader,
|
||||
stdout_writer,
|
||||
)));
|
||||
}));
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
@@ -953,10 +876,7 @@ impl FakeTransport {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Transport for FakeTransport {
|
||||
fn tcp_arguments(&self) -> Option<TcpArguments> {
|
||||
match &self.kind {
|
||||
FakeTransportKind::Stdio { .. } => None,
|
||||
FakeTransportKind::Tcp { connection, .. } => Some(connection.clone()),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn connect(
|
||||
@@ -967,33 +887,12 @@ impl Transport for FakeTransport {
|
||||
Box<dyn AsyncRead + Unpin + Send + 'static>,
|
||||
)>,
|
||||
> {
|
||||
let result = match &mut self.kind {
|
||||
FakeTransportKind::Stdio {
|
||||
stdin_writer,
|
||||
stdout_reader,
|
||||
} => util::maybe!({
|
||||
Ok((
|
||||
Box::new(stdin_writer.take().context("Cannot reconnect")?) as _,
|
||||
Box::new(stdout_reader.take().context("Cannot reconnect")?) as _,
|
||||
))
|
||||
}),
|
||||
FakeTransportKind::Tcp { executor, .. } => {
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
|
||||
let request_handlers = self.request_handlers.clone();
|
||||
let response_handlers = self.response_handlers.clone();
|
||||
|
||||
self.message_handler = Some(executor.spawn(Self::handle_messages(
|
||||
request_handlers,
|
||||
response_handlers,
|
||||
stdin_reader,
|
||||
stdout_writer,
|
||||
)));
|
||||
|
||||
Ok((Box::new(stdin_writer) as _, Box::new(stdout_reader) as _))
|
||||
}
|
||||
};
|
||||
let result = util::maybe!({
|
||||
Ok((
|
||||
Box::new(self.stdin_writer.take().context("Cannot reconnect")?) as _,
|
||||
Box::new(self.stdout_reader.take().context("Cannot reconnect")?) as _,
|
||||
))
|
||||
});
|
||||
Task::ready(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ doctest = false
|
||||
test-support = [
|
||||
"dap/test-support",
|
||||
"dap_adapters/test-support",
|
||||
"debugger_tools/test-support",
|
||||
"editor/test-support",
|
||||
"gpui/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"unindent",
|
||||
"debugger_tools"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -69,7 +69,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
debugger_tools = { workspace = true, optional = true }
|
||||
debugger_tools.workspace = true
|
||||
unindent = { workspace = true, optional = true }
|
||||
zed_actions.workspace = true
|
||||
|
||||
|
||||
@@ -622,6 +622,14 @@ impl DebugPanel {
|
||||
.on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
|
||||
.tooltip(Tooltip::text("Open Documentation"))
|
||||
};
|
||||
let logs_button = || {
|
||||
IconButton::new("debug-open-logs", IconName::ScrollText)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(debugger_tools::OpenDebugAdapterLogs.boxed_clone(), cx)
|
||||
})
|
||||
.tooltip(Tooltip::text("Open Debug Adapter Logs"))
|
||||
};
|
||||
|
||||
Some(
|
||||
div.border_b_1()
|
||||
@@ -873,6 +881,7 @@ impl DebugPanel {
|
||||
.justify_around()
|
||||
.when(is_side, |this| {
|
||||
this.child(new_session_button())
|
||||
.child(logs_button())
|
||||
.child(documentation_button())
|
||||
}),
|
||||
)
|
||||
@@ -922,6 +931,7 @@ impl DebugPanel {
|
||||
))
|
||||
.when(!is_side, |this| {
|
||||
this.child(new_session_button())
|
||||
.child(logs_button())
|
||||
.child(documentation_button())
|
||||
}),
|
||||
),
|
||||
@@ -1694,7 +1704,6 @@ impl Render for DebugPanel {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::DebugAdapters,
|
||||
),
|
||||
id: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
|
||||
@@ -122,7 +122,7 @@ impl DebugSession {
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn running_state(&self) -> &Entity<RunningState> {
|
||||
pub(crate) fn running_state(&self) -> &Entity<RunningState> {
|
||||
&self.running_state
|
||||
}
|
||||
|
||||
|
||||
@@ -1459,7 +1459,7 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_thread_id(&self) -> Option<ThreadId> {
|
||||
pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
|
||||
self.thread_id
|
||||
}
|
||||
|
||||
|
||||
@@ -482,7 +482,9 @@ pub enum SelectMode {
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum EditorMode {
|
||||
SingleLine,
|
||||
SingleLine {
|
||||
auto_width: bool,
|
||||
},
|
||||
AutoHeight {
|
||||
min_lines: usize,
|
||||
max_lines: Option<usize>,
|
||||
@@ -1660,7 +1662,13 @@ impl Editor {
|
||||
pub fn single_line(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(EditorMode::SingleLine, buffer, None, window, cx)
|
||||
Self::new(
|
||||
EditorMode::SingleLine { auto_width: false },
|
||||
buffer,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn multi_line(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
@@ -1669,6 +1677,18 @@ impl Editor {
|
||||
Self::new(EditorMode::full(), buffer, None, window, cx)
|
||||
}
|
||||
|
||||
pub fn auto_width(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
Self::new(
|
||||
EditorMode::SingleLine { auto_width: true },
|
||||
buffer,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn auto_height(
|
||||
min_lines: usize,
|
||||
max_lines: usize,
|
||||
@@ -4361,7 +4381,7 @@ impl Editor {
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
let comment_candidate = snapshot
|
||||
.chars_for_range(range)
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_len_of_delimiter)
|
||||
.collect::<String>();
|
||||
@@ -4377,6 +4397,22 @@ impl Editor {
|
||||
})
|
||||
.max_by_key(|(_, len)| *len)?;
|
||||
|
||||
if let Some((block_start, _)) = language.block_comment_delimiters()
|
||||
{
|
||||
let block_start_trimmed = block_start.trim_end();
|
||||
if block_start_trimmed.starts_with(delimiter.trim_end()) {
|
||||
let line_content = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(num_of_whitespaces)
|
||||
.take(block_start_trimmed.len())
|
||||
.collect::<String>();
|
||||
|
||||
if line_content.starts_with(block_start_trimmed) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
num_of_whitespaces + trimmed_len <= start_point.column as usize;
|
||||
if cursor_is_placed_after_comment_marker {
|
||||
@@ -20467,7 +20503,6 @@ impl Editor {
|
||||
if event.blurred != self.focus_handle {
|
||||
self.last_focused_descendant = Some(event.blurred);
|
||||
}
|
||||
self.selection_drag_state = SelectionDragState::None;
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -3080,6 +3080,45 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||||
});
|
||||
|
||||
let lua_language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comments: vec!["--".into()],
|
||||
block_comment: Some(("--[[".into(), "]]".into())),
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
));
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
|
||||
|
||||
// Line with line comment should extend
|
||||
cx.set_state(indoc! {"
|
||||
--ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
--
|
||||
--ˇ
|
||||
"});
|
||||
|
||||
// Line with block comment that matches line comment should not extend
|
||||
cx.set_state(indoc! {"
|
||||
--[[ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
--[[
|
||||
ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -7777,13 +7777,46 @@ impl Element for EditorElement {
|
||||
editor.set_style(self.style.clone(), window, cx);
|
||||
|
||||
let layout_id = match editor.mode {
|
||||
EditorMode::SingleLine => {
|
||||
EditorMode::SingleLine { auto_width } => {
|
||||
let rem_size = window.rem_size();
|
||||
|
||||
let height = self.style.text.line_height_in_pixels(rem_size);
|
||||
let mut style = Style::default();
|
||||
style.size.height = height.into();
|
||||
style.size.width = relative(1.).into();
|
||||
window.request_layout(style, None, cx)
|
||||
if auto_width {
|
||||
let editor_handle = cx.entity().clone();
|
||||
let style = self.style.clone();
|
||||
window.request_measured_layout(
|
||||
Style::default(),
|
||||
move |_, _, window, cx| {
|
||||
let editor_snapshot = editor_handle
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let line = Self::layout_lines(
|
||||
DisplayRow(0)..DisplayRow(1),
|
||||
&editor_snapshot,
|
||||
&style,
|
||||
px(f32::MAX),
|
||||
|_| false, // Single lines never soft wrap
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.pop()
|
||||
.unwrap();
|
||||
|
||||
let font_id =
|
||||
window.text_system().resolve_font(&style.text.font());
|
||||
let font_size =
|
||||
style.text.font_size.to_pixels(window.rem_size());
|
||||
let em_width =
|
||||
window.text_system().em_width(font_id, font_size).unwrap();
|
||||
|
||||
size(line.width + em_width, height)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let mut style = Style::default();
|
||||
style.size.height = height.into();
|
||||
style.size.width = relative(1.).into();
|
||||
window.request_layout(style, None, cx)
|
||||
}
|
||||
}
|
||||
EditorMode::AutoHeight {
|
||||
min_lines,
|
||||
@@ -10355,7 +10388,7 @@ mod tests {
|
||||
});
|
||||
|
||||
for editor_mode_without_invisibles in [
|
||||
EditorMode::SingleLine,
|
||||
EditorMode::SingleLine { auto_width: false },
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: Some(100),
|
||||
|
||||
@@ -221,6 +221,9 @@ impl ExampleContext {
|
||||
ThreadEvent::ShowError(thread_error) => {
|
||||
tx.try_send(Err(anyhow!(thread_error.clone()))).ok();
|
||||
}
|
||||
ThreadEvent::RetriesFailed { .. } => {
|
||||
// Ignore retries failed events
|
||||
}
|
||||
ThreadEvent::Stopped(reason) => match reason {
|
||||
Ok(StopReason::EndTurn) => {
|
||||
tx.close_channel();
|
||||
|
||||
@@ -324,20 +324,8 @@
|
||||
<body>
|
||||
<h1 id="current-filename">Thread Explorer</h1>
|
||||
<div class="view-switcher">
|
||||
<button
|
||||
id="full-view"
|
||||
class="view-button active"
|
||||
onclick="switchView('full')"
|
||||
>
|
||||
Full View
|
||||
</button>
|
||||
<button
|
||||
id="compact-view"
|
||||
class="view-button"
|
||||
onclick="switchView('compact')"
|
||||
>
|
||||
Compact View
|
||||
</button>
|
||||
<button id="full-view" class="view-button active" onclick="switchView('full')">Full View</button>
|
||||
<button id="compact-view" class="view-button" onclick="switchView('compact')">Compact View</button>
|
||||
<button
|
||||
id="export-button"
|
||||
class="view-button"
|
||||
@@ -347,11 +335,7 @@
|
||||
Export
|
||||
</button>
|
||||
<div class="theme-switcher">
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="theme-button"
|
||||
onclick="toggleTheme()"
|
||||
>
|
||||
<button id="theme-toggle" class="theme-button" onclick="toggleTheme()">
|
||||
<span id="theme-icon" class="theme-icon">☀️</span>
|
||||
<span id="theme-text">Light</span>
|
||||
</button>
|
||||
@@ -368,8 +352,7 @@
|
||||
← Previous
|
||||
</button>
|
||||
<div class="thread-indicator">
|
||||
Thread <span id="current-thread-index">1</span> of
|
||||
<span id="total-threads">1</span>:
|
||||
Thread <span id="current-thread-index">1</span> of <span id="total-threads">1</span>:
|
||||
<span id="thread-id">Default Thread</span>
|
||||
</div>
|
||||
<button
|
||||
@@ -423,9 +406,7 @@
|
||||
function toggleTheme() {
|
||||
// If currently system or light, switch to dark
|
||||
if (themeMode === "system") {
|
||||
const systemDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
themeMode = systemDark ? "light" : "dark";
|
||||
} else {
|
||||
themeMode = themeMode === "light" ? "dark" : "light";
|
||||
@@ -442,19 +423,15 @@
|
||||
function initTheme() {
|
||||
if (themeMode === "system") {
|
||||
// Use system preference
|
||||
const systemDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
applyTheme(systemDark ? "dark" : "light");
|
||||
|
||||
// Listen for system theme changes
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", (e) => {
|
||||
if (themeMode === "system") {
|
||||
applyTheme(e.matches ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||||
if (themeMode === "system") {
|
||||
applyTheme(e.matches ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Use saved preference
|
||||
applyTheme(themeMode);
|
||||
@@ -466,49 +443,38 @@
|
||||
viewMode = mode;
|
||||
|
||||
// Update button states
|
||||
document
|
||||
.getElementById("full-view")
|
||||
.classList.toggle("active", mode === "full");
|
||||
document
|
||||
.getElementById("compact-view")
|
||||
.classList.toggle("active", mode === "compact");
|
||||
document.getElementById("full-view").classList.toggle("active", mode === "full");
|
||||
document.getElementById("compact-view").classList.toggle("active", mode === "compact");
|
||||
|
||||
// Add or remove compact-mode class on the body
|
||||
document.body.classList.toggle(
|
||||
"compact-mode",
|
||||
mode === "compact",
|
||||
);
|
||||
document.body.classList.toggle("compact-mode", mode === "compact");
|
||||
|
||||
// Re-render the thread with the new view mode
|
||||
renderThread();
|
||||
}
|
||||
|
||||
|
||||
// Function to export the current thread as a JSON file
|
||||
function exportThreadAsJson() {
|
||||
// Clone the thread to avoid modifying the original
|
||||
const threadToExport = JSON.parse(JSON.stringify(thread));
|
||||
|
||||
|
||||
// Create a Blob with the JSON data
|
||||
const blob = new Blob(
|
||||
[JSON.stringify(threadToExport, null, 2)],
|
||||
{ type: "application/json" }
|
||||
);
|
||||
|
||||
const blob = new Blob([JSON.stringify(threadToExport, null, 2)], { type: "application/json" });
|
||||
|
||||
// Create a download link
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
|
||||
|
||||
// Generate filename based on thread ID or index
|
||||
const filename = threadToExport.thread_id ||
|
||||
threadToExport.filename ||
|
||||
`thread-${currentThreadIndex + 1}.json`;
|
||||
const filename =
|
||||
threadToExport.thread_id || threadToExport.filename || `thread-${currentThreadIndex + 1}.json`;
|
||||
a.download = filename.endsWith(".json") ? filename : `${filename}.json`;
|
||||
|
||||
|
||||
// Trigger the download
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
|
||||
// Clean up
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
@@ -524,9 +490,7 @@
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ Text: "Fix the bug: kwargs not passed..." },
|
||||
],
|
||||
content: [{ Text: "Fix the bug: kwargs not passed..." }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
@@ -593,12 +557,9 @@
|
||||
name: "edit_file",
|
||||
input: {
|
||||
path: "fastmcp/core.py",
|
||||
old_string:
|
||||
"def start_server(app):\n anyio.run(app)",
|
||||
new_string:
|
||||
"def start_server(app, **kwargs):\n anyio.run(app, **kwargs)",
|
||||
display_description:
|
||||
"Fix kwargs passing to anyio.run",
|
||||
old_string: "def start_server(app):\n anyio.run(app)",
|
||||
new_string: "def start_server(app, **kwargs):\n anyio.run(app, **kwargs)",
|
||||
display_description: "Fix kwargs passing to anyio.run",
|
||||
},
|
||||
is_input_complete: true,
|
||||
},
|
||||
@@ -681,14 +642,10 @@
|
||||
|
||||
// Function to update the navigation buttons state
|
||||
function updateNavigationButtons() {
|
||||
document.getElementById("prev-thread").disabled =
|
||||
currentThreadIndex <= 0;
|
||||
document.getElementById("next-thread").disabled =
|
||||
currentThreadIndex >= threads.length - 1;
|
||||
document.getElementById("current-thread-index").textContent =
|
||||
currentThreadIndex + 1;
|
||||
document.getElementById("total-threads").textContent =
|
||||
threads.length;
|
||||
document.getElementById("prev-thread").disabled = currentThreadIndex <= 0;
|
||||
document.getElementById("next-thread").disabled = currentThreadIndex >= threads.length - 1;
|
||||
document.getElementById("current-thread-index").textContent = currentThreadIndex + 1;
|
||||
document.getElementById("total-threads").textContent = threads.length;
|
||||
}
|
||||
|
||||
function renderThread() {
|
||||
@@ -696,20 +653,15 @@
|
||||
tbody.innerHTML = ""; // Clear existing content
|
||||
|
||||
// Set thread name if available
|
||||
const threadId =
|
||||
thread.thread_id || `Thread ${currentThreadIndex + 1}`;
|
||||
const threadId = thread.thread_id || `Thread ${currentThreadIndex + 1}`;
|
||||
document.getElementById("thread-id").textContent = threadId;
|
||||
|
||||
// Set filename in the header if available
|
||||
const filename =
|
||||
thread.filename || `Thread ${currentThreadIndex + 1}`;
|
||||
document.getElementById("current-filename").textContent =
|
||||
filename;
|
||||
const filename = thread.filename || `Thread ${currentThreadIndex + 1}`;
|
||||
document.getElementById("current-filename").textContent = filename;
|
||||
|
||||
// Skip system message
|
||||
const nonSystemMessages = thread.messages.filter(
|
||||
(msg) => msg.role !== "system",
|
||||
);
|
||||
const nonSystemMessages = thread.messages.filter((msg) => msg.role !== "system");
|
||||
|
||||
let turnNumber = 0;
|
||||
processMessages(nonSystemMessages, tbody, turnNumber);
|
||||
@@ -737,9 +689,7 @@
|
||||
for (const content of msg.content) {
|
||||
if (content.hasOwnProperty("Text")) {
|
||||
if (assistantText) {
|
||||
assistantText +=
|
||||
"<br><br>" +
|
||||
formatContent(content.Text);
|
||||
assistantText += "<br><br>" + formatContent(content.Text);
|
||||
} else {
|
||||
assistantText = formatContent(content.Text);
|
||||
}
|
||||
@@ -763,49 +713,33 @@
|
||||
tbody.appendChild(row);
|
||||
|
||||
// Add all tool calls to the tools cell
|
||||
const toolsCell = document.getElementById(
|
||||
`tools-${turnNumber}`,
|
||||
);
|
||||
const resultsCell = document.getElementById(
|
||||
`results-${turnNumber}`,
|
||||
);
|
||||
const toolsCell = document.getElementById(`tools-${turnNumber}`);
|
||||
const resultsCell = document.getElementById(`results-${turnNumber}`);
|
||||
|
||||
// Process all tools and their results
|
||||
for (let j = 0; j < toolUses.length; j++) {
|
||||
const toolUse = toolUses[j];
|
||||
const toolCall = formatToolCall(
|
||||
toolUse.name,
|
||||
toolUse.input,
|
||||
);
|
||||
const toolCall = formatToolCall(toolUse.name, toolUse.input);
|
||||
|
||||
// Add the tool call to the tools cell
|
||||
if (j > 0) toolsCell.innerHTML += "<hr>";
|
||||
toolsCell.innerHTML += toolCall;
|
||||
|
||||
// Look for corresponding tool result
|
||||
if (
|
||||
hasMatchingToolResult(messages, i, toolUse.name)
|
||||
) {
|
||||
if (hasMatchingToolResult(messages, i, toolUse.name)) {
|
||||
const resultMsg = messages[i + 1];
|
||||
const toolResult = findToolResult(
|
||||
resultMsg,
|
||||
toolUse.name,
|
||||
);
|
||||
const toolResult = findToolResult(resultMsg, toolUse.name);
|
||||
|
||||
if (toolResult) {
|
||||
// Add the result to the results cell
|
||||
if (j > 0) resultsCell.innerHTML += "<hr>";
|
||||
|
||||
// Create a container for the result
|
||||
const resultDiv =
|
||||
document.createElement("div");
|
||||
const resultDiv = document.createElement("div");
|
||||
resultDiv.className = "tool-result";
|
||||
|
||||
// Format and display the tool result
|
||||
formatToolResultInline(
|
||||
toolResult.content,
|
||||
resultDiv,
|
||||
);
|
||||
formatToolResultInline(toolResult.content.Text, resultDiv);
|
||||
resultsCell.appendChild(resultDiv);
|
||||
|
||||
// Skip the result message in the next iteration
|
||||
@@ -815,10 +749,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
msg.role === "user" &&
|
||||
msg.content.some((c) => c.hasOwnProperty("ToolResult"))
|
||||
) {
|
||||
} else if (msg.role === "user" && msg.content.some((c) => c.hasOwnProperty("ToolResult"))) {
|
||||
// Skip tool result messages as they are handled with their corresponding tool use
|
||||
continue;
|
||||
}
|
||||
@@ -826,10 +757,7 @@
|
||||
}
|
||||
|
||||
function isUserQuery(message) {
|
||||
return (
|
||||
message.role === "user" &&
|
||||
!message.content.some((c) => c.hasOwnProperty("ToolResult"))
|
||||
);
|
||||
return message.role === "user" && !message.content.some((c) => c.hasOwnProperty("ToolResult"));
|
||||
}
|
||||
|
||||
function renderUserMessage(message, turnNumber, tbody) {
|
||||
@@ -848,18 +776,14 @@
|
||||
currentIndex + 1 < messages.length &&
|
||||
messages[currentIndex + 1].role === "user" &&
|
||||
messages[currentIndex + 1].content.some(
|
||||
(c) =>
|
||||
c.hasOwnProperty("ToolResult") &&
|
||||
c.ToolResult.tool_name === toolName,
|
||||
(c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function findToolResult(resultMessage, toolName) {
|
||||
const toolResultContent = resultMessage.content.find(
|
||||
(c) =>
|
||||
c.hasOwnProperty("ToolResult") &&
|
||||
c.ToolResult.tool_name === toolName,
|
||||
(c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
|
||||
);
|
||||
|
||||
return toolResultContent ? toolResultContent.ToolResult : null;
|
||||
@@ -874,18 +798,12 @@
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value !== null && value !== undefined) {
|
||||
// Store full parameter for expanded view
|
||||
let fullValue =
|
||||
typeof value === "string"
|
||||
? `"${value}"`
|
||||
: value;
|
||||
let fullValue = typeof value === "string" ? `"${value}"` : value;
|
||||
fullParams.push([key, fullValue]);
|
||||
|
||||
// Abbreviated value for compact view
|
||||
let displayValue = fullValue;
|
||||
if (
|
||||
typeof value === "string" &&
|
||||
value.length > 30
|
||||
) {
|
||||
if (typeof value === "string" && value.length > 30) {
|
||||
displayValue = `"${value.substring(0, 30)}..."`;
|
||||
}
|
||||
params.push(`${key}=${displayValue}`);
|
||||
@@ -903,10 +821,7 @@
|
||||
// For the full view, use the original untruncated values
|
||||
let result = `<span class="tool-name">${name}</span>(`;
|
||||
const formattedParams = fullParams
|
||||
.map(
|
||||
(p) =>
|
||||
` ${p[0]}=${p[1]}`,
|
||||
)
|
||||
.map((p) => ` ${p[0]}=${p[1]}`)
|
||||
.join(",<br/>");
|
||||
const fullView = `${result}<br/>${formattedParams}<br/>)`;
|
||||
|
||||
@@ -925,8 +840,7 @@
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value !== null && value !== undefined) {
|
||||
// Format different types of values
|
||||
let formattedValue =
|
||||
typeof value === "string" ? `"${value}"` : value;
|
||||
let formattedValue = typeof value === "string" ? `"${value}"` : value;
|
||||
params.push([key, formattedValue]);
|
||||
}
|
||||
}
|
||||
@@ -938,9 +852,7 @@
|
||||
return `${result}${params[0][1]})`;
|
||||
} else {
|
||||
// Format parameters
|
||||
const formattedParams = params
|
||||
.map((p) => ` ${p[0]}=${p[1]}`)
|
||||
.join(",<br/>");
|
||||
const formattedParams = params.map((p) => ` ${p[0]}=${p[1]}`).join(",<br/>");
|
||||
return `${result}<br/>${formattedParams}<br/>)`;
|
||||
}
|
||||
}
|
||||
@@ -1013,21 +925,13 @@
|
||||
// Keyboard navigation handler
|
||||
document.addEventListener("keydown", function (event) {
|
||||
// previous thread
|
||||
if (
|
||||
(event.ctrlKey && event.key === "ArrowLeft") ||
|
||||
event.key === "h" ||
|
||||
event.key === "k"
|
||||
) {
|
||||
if ((event.ctrlKey && event.key === "ArrowLeft") || event.key === "h" || event.key === "k") {
|
||||
if (!document.getElementById("prev-thread").disabled) {
|
||||
previousThread();
|
||||
}
|
||||
}
|
||||
// next thread
|
||||
else if (
|
||||
(event.ctrlKey && event.key === "ArrowRight") ||
|
||||
event.key === "j" ||
|
||||
event.key === "l"
|
||||
) {
|
||||
else if ((event.ctrlKey && event.key === "ArrowRight") || event.key === "j" || event.key === "l") {
|
||||
if (!document.getElementById("next-thread").disabled) {
|
||||
nextThread();
|
||||
}
|
||||
|
||||
@@ -594,6 +594,7 @@ impl ExampleInstance {
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
let model = model.clone();
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use client::{ExtensionMetadata, ExtensionProvides};
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
@@ -81,24 +80,16 @@ pub fn init(cx: &mut App) {
|
||||
.find_map(|item| item.downcast::<ExtensionsPage>());
|
||||
|
||||
if let Some(existing) = existing {
|
||||
existing.update(cx, |extensions_page, cx| {
|
||||
if provides_filter.is_some() {
|
||||
if provides_filter.is_some() {
|
||||
existing.update(cx, |extensions_page, cx| {
|
||||
extensions_page.change_provides_filter(provides_filter, cx);
|
||||
}
|
||||
if let Some(id) = action.id.as_ref() {
|
||||
extensions_page.focus_extension(id, window, cx);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let extensions_page = ExtensionsPage::new(
|
||||
workspace,
|
||||
provides_filter,
|
||||
action.id.as_deref(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let extensions_page =
|
||||
ExtensionsPage::new(workspace, provides_filter, window, cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(extensions_page),
|
||||
None,
|
||||
@@ -296,7 +287,6 @@ impl ExtensionsPage {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
provides_filter: Option<ExtensionProvides>,
|
||||
focus_extension_id: Option<&str>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
@@ -327,9 +317,6 @@ impl ExtensionsPage {
|
||||
let query_editor = cx.new(|cx| {
|
||||
let mut input = Editor::single_line(window, cx);
|
||||
input.set_placeholder_text("Search extensions...", cx);
|
||||
if let Some(id) = focus_extension_id {
|
||||
input.set_text(format!("id:{id}"), window, cx);
|
||||
}
|
||||
input
|
||||
});
|
||||
cx.subscribe(&query_editor, Self::on_query_change).detach();
|
||||
@@ -353,7 +340,7 @@ impl ExtensionsPage {
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle),
|
||||
};
|
||||
this.fetch_extensions(
|
||||
this.search_query(cx),
|
||||
None,
|
||||
Some(BTreeSet::from_iter(this.provides_filter)),
|
||||
None,
|
||||
cx,
|
||||
@@ -477,23 +464,9 @@ impl ExtensionsPage {
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_extensions =
|
||||
if let Some(id) = search.as_ref().and_then(|s| s.strip_prefix("id:")) {
|
||||
let versions =
|
||||
extension_store.update(cx, |store, cx| store.fetch_extension_versions(id, cx));
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let versions = versions.await?;
|
||||
let latest = versions
|
||||
.into_iter()
|
||||
.max_by_key(|v| v.published_at)
|
||||
.context("no extension found")?;
|
||||
Ok(vec![latest])
|
||||
})
|
||||
} else {
|
||||
extension_store.update(cx, |store, cx| {
|
||||
store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx)
|
||||
})
|
||||
};
|
||||
let remote_extensions = extension_store.update(cx, |store, cx| {
|
||||
store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx)
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let dev_extensions = if let Some(search) = search {
|
||||
@@ -745,24 +718,34 @@ impl ExtensionsPage {
|
||||
}
|
||||
|
||||
parent.child(
|
||||
h_flex().gap_2().children(
|
||||
h_flex().gap_1().children(
|
||||
extension
|
||||
.manifest
|
||||
.provides
|
||||
.iter()
|
||||
.map(|provides| {
|
||||
div()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.px_0p5()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new(extension_provides_label(
|
||||
*provides,
|
||||
))
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.filter_map(|provides| {
|
||||
match provides {
|
||||
ExtensionProvides::SlashCommands
|
||||
| ExtensionProvides::IndexedDocsProviders => {
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(
|
||||
div()
|
||||
.px_1()
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(
|
||||
Label::new(extension_provides_label(
|
||||
*provides,
|
||||
))
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
@@ -771,8 +754,7 @@ impl ExtensionsPage {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.children(buttons.upgrade)
|
||||
.children(buttons.configure)
|
||||
.child(buttons.install_or_uninstall),
|
||||
@@ -1183,13 +1165,6 @@ impl ExtensionsPage {
|
||||
self.refresh_feature_upsells(cx);
|
||||
}
|
||||
|
||||
pub fn focus_extension(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.query_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(format!("id:{id}"), window, cx)
|
||||
});
|
||||
self.refresh_search(cx);
|
||||
}
|
||||
|
||||
pub fn change_provides_filter(
|
||||
&mut self,
|
||||
provides_filter: Option<ExtensionProvides>,
|
||||
@@ -1486,23 +1461,30 @@ impl Render for ExtensionsPage {
|
||||
this.change_provides_filter(None, cx);
|
||||
})),
|
||||
)
|
||||
.children(ExtensionProvides::iter().map(|provides| {
|
||||
.children(ExtensionProvides::iter().filter_map(|provides| {
|
||||
match provides {
|
||||
ExtensionProvides::SlashCommands
|
||||
| ExtensionProvides::IndexedDocsProviders => return None,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let label = extension_provides_label(provides);
|
||||
Button::new(
|
||||
SharedString::from(format!("filter-category-{}", label)),
|
||||
label,
|
||||
let button_id = SharedString::from(format!("filter-category-{}", label));
|
||||
|
||||
Some(
|
||||
Button::new(button_id, label)
|
||||
.style(if self.provides_filter == Some(provides) {
|
||||
ButtonStyle::Filled
|
||||
} else {
|
||||
ButtonStyle::Subtle
|
||||
})
|
||||
.toggle_state(self.provides_filter == Some(provides))
|
||||
.on_click({
|
||||
cx.listener(move |this, _event, _, cx| {
|
||||
this.change_provides_filter(Some(provides), cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.style(if self.provides_filter == Some(provides) {
|
||||
ButtonStyle::Filled
|
||||
} else {
|
||||
ButtonStyle::Subtle
|
||||
})
|
||||
.toggle_state(self.provides_filter == Some(provides))
|
||||
.on_click({
|
||||
cx.listener(move |this, _event, _, cx| {
|
||||
this.change_provides_filter(Some(provides), cx);
|
||||
})
|
||||
})
|
||||
})),
|
||||
)
|
||||
.child(self.render_feature_upsells(cx))
|
||||
|
||||
@@ -92,6 +92,12 @@ impl FeatureFlag for JjUiFeatureFlag {
|
||||
const NAME: &'static str = "jj-ui";
|
||||
}
|
||||
|
||||
pub struct AcpFeatureFlag;
|
||||
|
||||
impl FeatureFlag for AcpFeatureFlag {
|
||||
const NAME: &'static str = "acp";
|
||||
}
|
||||
|
||||
pub struct ZedCloudFeatureFlag {}
|
||||
|
||||
impl FeatureFlag for ZedCloudFeatureFlag {
|
||||
|
||||
@@ -1830,6 +1830,7 @@ impl GitPanel {
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
thinking_allowed: false,
|
||||
};
|
||||
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
|
||||
@@ -120,7 +120,18 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
|
||||
}
|
||||
RemoteAction::Push(branch_name, remote_ref) => {
|
||||
if output.stderr.contains("* [new branch]") {
|
||||
let style = if output.stderr.contains("Create a pull request") {
|
||||
let pr_hints = [
|
||||
// GitHub
|
||||
"Create a pull request",
|
||||
// Bitbucket
|
||||
"Create pull request",
|
||||
// GitLab
|
||||
"create a merge request",
|
||||
];
|
||||
let style = if pr_hints
|
||||
.iter()
|
||||
.any(|indicator| output.stderr.contains(indicator))
|
||||
{
|
||||
let finder = LinkFinder::new();
|
||||
let first_link = finder
|
||||
.links(&output.stderr)
|
||||
@@ -154,3 +165,109 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_push_new_branch_pull_request() {
|
||||
let action = RemoteAction::Push(
|
||||
SharedString::new("test_branch"),
|
||||
Remote {
|
||||
name: SharedString::new("test_remote"),
|
||||
},
|
||||
);
|
||||
|
||||
let output = RemoteCommandOutput {
|
||||
stdout: String::new(),
|
||||
stderr: String::from(
|
||||
"
|
||||
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
|
||||
remote:
|
||||
remote: Create a pull request for 'test' on GitHub by visiting:
|
||||
remote: https://example.com/test/test/pull/new/test
|
||||
remote:
|
||||
To example.com:test/test.git
|
||||
* [new branch] test -> test
|
||||
",
|
||||
),
|
||||
};
|
||||
|
||||
let msg = format_output(&action, output);
|
||||
|
||||
if let SuccessStyle::PushPrLink { link } = &msg.style {
|
||||
assert_eq!(link, "https://example.com/test/test/pull/new/test");
|
||||
} else {
|
||||
panic!("Expected PushPrLink variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_new_branch_merge_request() {
|
||||
let action = RemoteAction::Push(
|
||||
SharedString::new("test_branch"),
|
||||
Remote {
|
||||
name: SharedString::new("test_remote"),
|
||||
},
|
||||
);
|
||||
|
||||
let output = RemoteCommandOutput {
|
||||
stdout: String::new(),
|
||||
stderr: String::from("
|
||||
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
|
||||
remote:
|
||||
remote: To create a merge request for test, visit:
|
||||
remote: https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test
|
||||
remote:
|
||||
To example.com:test/test.git
|
||||
* [new branch] test -> test
|
||||
"),
|
||||
};
|
||||
|
||||
let msg = format_output(&action, output);
|
||||
|
||||
if let SuccessStyle::PushPrLink { link } = &msg.style {
|
||||
assert_eq!(
|
||||
link,
|
||||
"https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected PushPrLink variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_new_branch_no_link() {
|
||||
let action = RemoteAction::Push(
|
||||
SharedString::new("test_branch"),
|
||||
Remote {
|
||||
name: SharedString::new("test_remote"),
|
||||
},
|
||||
);
|
||||
|
||||
let output = RemoteCommandOutput {
|
||||
stdout: String::new(),
|
||||
stderr: String::from(
|
||||
"
|
||||
To http://example.com/test/test.git
|
||||
* [new branch] test -> test
|
||||
",
|
||||
),
|
||||
};
|
||||
|
||||
let msg = format_output(&action, output);
|
||||
|
||||
if let SuccessStyle::ToastWithLog { output } = &msg.style {
|
||||
assert_eq!(
|
||||
output.stderr,
|
||||
"
|
||||
To http://example.com/test/test.git
|
||||
* [new branch] test -> test
|
||||
"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected ToastWithLog variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,7 @@ pub struct App {
|
||||
// TypeId is the type of the event that the listener callback expects
|
||||
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
|
||||
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
|
||||
pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>,
|
||||
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||
@@ -344,6 +345,7 @@ impl App {
|
||||
event_listeners: SubscriberSet::new(),
|
||||
release_listeners: SubscriberSet::new(),
|
||||
keystroke_observers: SubscriberSet::new(),
|
||||
keystroke_interceptors: SubscriberSet::new(),
|
||||
keyboard_layout_observers: SubscriberSet::new(),
|
||||
global_observers: SubscriberSet::new(),
|
||||
quit_observers: SubscriberSet::new(),
|
||||
@@ -1322,6 +1324,32 @@ impl App {
|
||||
)
|
||||
}
|
||||
|
||||
/// Register a callback to be invoked when a keystroke is received by the application
|
||||
/// in any window. Note that this fires _before_ all other action and event mechanisms have resolved
|
||||
/// unlike [`App::observe_keystrokes`] which fires after. This means that `cx.stop_propagation` calls
|
||||
/// within interceptors will prevent action dispatch
|
||||
pub fn intercept_keystrokes(
|
||||
&mut self,
|
||||
mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Subscription {
|
||||
fn inner(
|
||||
keystroke_interceptors: &SubscriberSet<(), KeystrokeObserver>,
|
||||
handler: KeystrokeObserver,
|
||||
) -> Subscription {
|
||||
let (subscription, activate) = keystroke_interceptors.insert((), handler);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
inner(
|
||||
&mut self.keystroke_interceptors,
|
||||
Box::new(move |event, window, cx| {
|
||||
f(event, window, cx);
|
||||
true
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Register key bindings.
|
||||
pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
|
||||
self.keymap.borrow_mut().add_bindings(bindings);
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use collections::{HashMap, HashSet};
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use strum::{EnumIter, IntoEnumIterator as _};
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use xkbcommon::xkb::{Keycode, Keymap, Keysym, MOD_NAME_SHIFT, State};
|
||||
|
||||
use crate::{PlatformKeyboardLayout, SharedString};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LinuxKeyboardLayout {
|
||||
name: SharedString,
|
||||
}
|
||||
@@ -20,3 +27,449 @@ impl LinuxKeyboardLayout {
|
||||
Self { name }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
pub(crate) struct LinuxKeyboardMapper {
|
||||
letters: HashMap<Keycode, String>,
|
||||
code_to_key: HashMap<Keycode, String>,
|
||||
code_to_shifted_key: HashMap<Keycode, String>,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
impl LinuxKeyboardMapper {
|
||||
pub(crate) fn new(
|
||||
keymap: &Keymap,
|
||||
base_group: u32,
|
||||
latched_group: u32,
|
||||
locked_group: u32,
|
||||
) -> Self {
|
||||
let mut xkb_state = State::new(keymap);
|
||||
xkb_state.update_mask(0, 0, 0, base_group, latched_group, locked_group);
|
||||
|
||||
let mut shifted_state = State::new(&keymap);
|
||||
let shift_mod = keymap.mod_get_index(MOD_NAME_SHIFT);
|
||||
let shift_mask = 1 << shift_mod;
|
||||
shifted_state.update_mask(shift_mask, 0, 0, base_group, latched_group, locked_group);
|
||||
|
||||
let mut letters = HashMap::default();
|
||||
let mut code_to_key = HashMap::default();
|
||||
let mut code_to_shifted_key = HashMap::default();
|
||||
let mut inserted_letters = HashSet::default();
|
||||
|
||||
for scan_code in LinuxScanCodes::iter() {
|
||||
let keycode = Keycode::new(scan_code as u32);
|
||||
|
||||
let key = xkb_state.key_get_utf8(keycode);
|
||||
if !key.is_empty() {
|
||||
if key_is_a_letter(&key) {
|
||||
letters.insert(keycode, key.clone());
|
||||
inserted_letters.insert(key);
|
||||
} else {
|
||||
code_to_key.insert(keycode, key.clone());
|
||||
}
|
||||
} else {
|
||||
// keycode might be a dead key
|
||||
let keysym = xkb_state.key_get_one_sym(keycode);
|
||||
if let Some(key) = underlying_dead_key(keysym) {
|
||||
code_to_key.insert(keycode, key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let shifted_key = shifted_state.key_get_utf8(keycode);
|
||||
if !shifted_key.is_empty() {
|
||||
code_to_shifted_key.insert(keycode, shifted_key);
|
||||
} else {
|
||||
// keycode might be a dead key
|
||||
let shifted_keysym = shifted_state.key_get_one_sym(keycode);
|
||||
if let Some(shifted_key) = underlying_dead_key(shifted_keysym) {
|
||||
code_to_shifted_key.insert(keycode, shifted_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
insert_letters_if_missing(&inserted_letters, &mut letters);
|
||||
|
||||
Self {
|
||||
letters,
|
||||
code_to_key,
|
||||
code_to_shifted_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_key(
|
||||
&self,
|
||||
keycode: Keycode,
|
||||
modifiers: &mut crate::Modifiers,
|
||||
) -> Option<String> {
|
||||
if let Some(key) = self.letters.get(&keycode) {
|
||||
return Some(key.clone());
|
||||
}
|
||||
if modifiers.shift {
|
||||
modifiers.shift = false;
|
||||
self.code_to_shifted_key.get(&keycode).cloned()
|
||||
} else {
|
||||
self.code_to_key.get(&keycode).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
fn key_is_a_letter(key: &str) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
"a" | "b"
|
||||
| "c"
|
||||
| "d"
|
||||
| "e"
|
||||
| "f"
|
||||
| "g"
|
||||
| "h"
|
||||
| "i"
|
||||
| "j"
|
||||
| "k"
|
||||
| "l"
|
||||
| "m"
|
||||
| "n"
|
||||
| "o"
|
||||
| "p"
|
||||
| "q"
|
||||
| "r"
|
||||
| "s"
|
||||
| "t"
|
||||
| "u"
|
||||
| "v"
|
||||
| "w"
|
||||
| "x"
|
||||
| "y"
|
||||
| "z"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which symbol the dead key represents
|
||||
* <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
|
||||
*/
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
pub(crate) fn underlying_dead_key(keysym: Keysym) -> Option<String> {
|
||||
match keysym {
|
||||
Keysym::dead_grave => Some("`".to_owned()),
|
||||
Keysym::dead_acute => Some("´".to_owned()),
|
||||
Keysym::dead_circumflex => Some("^".to_owned()),
|
||||
Keysym::dead_tilde => Some("~".to_owned()),
|
||||
Keysym::dead_macron => Some("¯".to_owned()),
|
||||
Keysym::dead_breve => Some("˘".to_owned()),
|
||||
Keysym::dead_abovedot => Some("˙".to_owned()),
|
||||
Keysym::dead_diaeresis => Some("¨".to_owned()),
|
||||
Keysym::dead_abovering => Some("˚".to_owned()),
|
||||
Keysym::dead_doubleacute => Some("˝".to_owned()),
|
||||
Keysym::dead_caron => Some("ˇ".to_owned()),
|
||||
Keysym::dead_cedilla => Some("¸".to_owned()),
|
||||
Keysym::dead_ogonek => Some("˛".to_owned()),
|
||||
Keysym::dead_iota => Some("ͅ".to_owned()),
|
||||
Keysym::dead_voiced_sound => Some("゙".to_owned()),
|
||||
Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
|
||||
Keysym::dead_belowdot => Some("̣̣".to_owned()),
|
||||
Keysym::dead_hook => Some("̡".to_owned()),
|
||||
Keysym::dead_horn => Some("̛".to_owned()),
|
||||
Keysym::dead_stroke => Some("̶̶".to_owned()),
|
||||
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
|
||||
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
|
||||
Keysym::dead_doublegrave => Some("̏".to_owned()),
|
||||
Keysym::dead_belowring => Some("˳".to_owned()),
|
||||
Keysym::dead_belowmacron => Some("̱".to_owned()),
|
||||
Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
|
||||
Keysym::dead_belowtilde => Some("̰".to_owned()),
|
||||
Keysym::dead_belowbreve => Some("̮".to_owned()),
|
||||
Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
|
||||
Keysym::dead_invertedbreve => Some("̯".to_owned()),
|
||||
Keysym::dead_belowcomma => Some("̦".to_owned()),
|
||||
Keysym::dead_currency => None,
|
||||
Keysym::dead_lowline => None,
|
||||
Keysym::dead_aboveverticalline => None,
|
||||
Keysym::dead_belowverticalline => None,
|
||||
Keysym::dead_longsolidusoverlay => None,
|
||||
Keysym::dead_a => None,
|
||||
Keysym::dead_A => None,
|
||||
Keysym::dead_e => None,
|
||||
Keysym::dead_E => None,
|
||||
Keysym::dead_i => None,
|
||||
Keysym::dead_I => None,
|
||||
Keysym::dead_o => None,
|
||||
Keysym::dead_O => None,
|
||||
Keysym::dead_u => None,
|
||||
Keysym::dead_U => None,
|
||||
Keysym::dead_small_schwa => Some("ə".to_owned()),
|
||||
Keysym::dead_capital_schwa => Some("Ə".to_owned()),
|
||||
Keysym::dead_greek => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
fn insert_letters_if_missing(inserted: &HashSet<String>, letters: &mut HashMap<Keycode, String>) {
|
||||
for scan_code in LinuxScanCodes::LETTERS.iter() {
|
||||
let keycode = Keycode::new(*scan_code as u32);
|
||||
let key = scan_code.to_str();
|
||||
if !inserted.contains(key) {
|
||||
letters.insert(keycode, key.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)]
|
||||
enum LinuxScanCodes {
|
||||
A = 0x0026,
|
||||
B = 0x0038,
|
||||
C = 0x0036,
|
||||
D = 0x0028,
|
||||
E = 0x001a,
|
||||
F = 0x0029,
|
||||
G = 0x002a,
|
||||
H = 0x002b,
|
||||
I = 0x001f,
|
||||
J = 0x002c,
|
||||
K = 0x002d,
|
||||
L = 0x002e,
|
||||
M = 0x003a,
|
||||
N = 0x0039,
|
||||
O = 0x0020,
|
||||
P = 0x0021,
|
||||
Q = 0x0018,
|
||||
R = 0x001b,
|
||||
S = 0x0027,
|
||||
T = 0x001c,
|
||||
U = 0x001e,
|
||||
V = 0x0037,
|
||||
W = 0x0019,
|
||||
X = 0x0035,
|
||||
Y = 0x001d,
|
||||
Z = 0x0034,
|
||||
Digit0 = 0x0013,
|
||||
Digit1 = 0x000a,
|
||||
Digit2 = 0x000b,
|
||||
Digit3 = 0x000c,
|
||||
Digit4 = 0x000d,
|
||||
Digit5 = 0x000e,
|
||||
Digit6 = 0x000f,
|
||||
Digit7 = 0x0010,
|
||||
Digit8 = 0x0011,
|
||||
Digit9 = 0x0012,
|
||||
Backquote = 0x0031,
|
||||
Minus = 0x0014,
|
||||
Equal = 0x0015,
|
||||
LeftBracket = 0x0022,
|
||||
RightBracket = 0x0023,
|
||||
Backslash = 0x0033,
|
||||
Semicolon = 0x002f,
|
||||
Quote = 0x0030,
|
||||
Comma = 0x003b,
|
||||
Period = 0x003c,
|
||||
Slash = 0x003d,
|
||||
// This key is typically located near LeftShift key, varies on international keyboards: Dan: <> Dutch: ][ Ger: <> UK: \|
|
||||
IntlBackslash = 0x005e,
|
||||
// Used for Brazilian /? and Japanese _ 'ro'.
|
||||
IntlRo = 0x0061,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
impl LinuxScanCodes {
|
||||
const LETTERS: &'static [LinuxScanCodes] = &[
|
||||
LinuxScanCodes::A,
|
||||
LinuxScanCodes::B,
|
||||
LinuxScanCodes::C,
|
||||
LinuxScanCodes::D,
|
||||
LinuxScanCodes::E,
|
||||
LinuxScanCodes::F,
|
||||
LinuxScanCodes::G,
|
||||
LinuxScanCodes::H,
|
||||
LinuxScanCodes::I,
|
||||
LinuxScanCodes::J,
|
||||
LinuxScanCodes::K,
|
||||
LinuxScanCodes::L,
|
||||
LinuxScanCodes::M,
|
||||
LinuxScanCodes::N,
|
||||
LinuxScanCodes::O,
|
||||
LinuxScanCodes::P,
|
||||
LinuxScanCodes::Q,
|
||||
LinuxScanCodes::R,
|
||||
LinuxScanCodes::S,
|
||||
LinuxScanCodes::T,
|
||||
LinuxScanCodes::U,
|
||||
LinuxScanCodes::V,
|
||||
LinuxScanCodes::W,
|
||||
LinuxScanCodes::X,
|
||||
LinuxScanCodes::Y,
|
||||
LinuxScanCodes::Z,
|
||||
];
|
||||
|
||||
fn to_str(&self) -> &str {
|
||||
match self {
|
||||
LinuxScanCodes::A => "a",
|
||||
LinuxScanCodes::B => "b",
|
||||
LinuxScanCodes::C => "c",
|
||||
LinuxScanCodes::D => "d",
|
||||
LinuxScanCodes::E => "e",
|
||||
LinuxScanCodes::F => "f",
|
||||
LinuxScanCodes::G => "g",
|
||||
LinuxScanCodes::H => "h",
|
||||
LinuxScanCodes::I => "i",
|
||||
LinuxScanCodes::J => "j",
|
||||
LinuxScanCodes::K => "k",
|
||||
LinuxScanCodes::L => "l",
|
||||
LinuxScanCodes::M => "m",
|
||||
LinuxScanCodes::N => "n",
|
||||
LinuxScanCodes::O => "o",
|
||||
LinuxScanCodes::P => "p",
|
||||
LinuxScanCodes::Q => "q",
|
||||
LinuxScanCodes::R => "r",
|
||||
LinuxScanCodes::S => "s",
|
||||
LinuxScanCodes::T => "t",
|
||||
LinuxScanCodes::U => "u",
|
||||
LinuxScanCodes::V => "v",
|
||||
LinuxScanCodes::W => "w",
|
||||
LinuxScanCodes::X => "x",
|
||||
LinuxScanCodes::Y => "y",
|
||||
LinuxScanCodes::Z => "z",
|
||||
LinuxScanCodes::Digit0 => "0",
|
||||
LinuxScanCodes::Digit1 => "1",
|
||||
LinuxScanCodes::Digit2 => "2",
|
||||
LinuxScanCodes::Digit3 => "3",
|
||||
LinuxScanCodes::Digit4 => "4",
|
||||
LinuxScanCodes::Digit5 => "5",
|
||||
LinuxScanCodes::Digit6 => "6",
|
||||
LinuxScanCodes::Digit7 => "7",
|
||||
LinuxScanCodes::Digit8 => "8",
|
||||
LinuxScanCodes::Digit9 => "9",
|
||||
LinuxScanCodes::Backquote => "`",
|
||||
LinuxScanCodes::Minus => "-",
|
||||
LinuxScanCodes::Equal => "=",
|
||||
LinuxScanCodes::LeftBracket => "[",
|
||||
LinuxScanCodes::RightBracket => "]",
|
||||
LinuxScanCodes::Backslash => "\\",
|
||||
LinuxScanCodes::Semicolon => ";",
|
||||
LinuxScanCodes::Quote => "'",
|
||||
LinuxScanCodes::Comma => ",",
|
||||
LinuxScanCodes::Period => ".",
|
||||
LinuxScanCodes::Slash => "/",
|
||||
LinuxScanCodes::IntlBackslash => "unknown",
|
||||
LinuxScanCodes::IntlRo => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn to_shifted(&self) -> &str {
|
||||
match self {
|
||||
LinuxScanCodes::A => "a",
|
||||
LinuxScanCodes::B => "b",
|
||||
LinuxScanCodes::C => "c",
|
||||
LinuxScanCodes::D => "d",
|
||||
LinuxScanCodes::E => "e",
|
||||
LinuxScanCodes::F => "f",
|
||||
LinuxScanCodes::G => "g",
|
||||
LinuxScanCodes::H => "h",
|
||||
LinuxScanCodes::I => "i",
|
||||
LinuxScanCodes::J => "j",
|
||||
LinuxScanCodes::K => "k",
|
||||
LinuxScanCodes::L => "l",
|
||||
LinuxScanCodes::M => "m",
|
||||
LinuxScanCodes::N => "n",
|
||||
LinuxScanCodes::O => "o",
|
||||
LinuxScanCodes::P => "p",
|
||||
LinuxScanCodes::Q => "q",
|
||||
LinuxScanCodes::R => "r",
|
||||
LinuxScanCodes::S => "s",
|
||||
LinuxScanCodes::T => "t",
|
||||
LinuxScanCodes::U => "u",
|
||||
LinuxScanCodes::V => "v",
|
||||
LinuxScanCodes::W => "w",
|
||||
LinuxScanCodes::X => "x",
|
||||
LinuxScanCodes::Y => "y",
|
||||
LinuxScanCodes::Z => "z",
|
||||
LinuxScanCodes::Digit0 => ")",
|
||||
LinuxScanCodes::Digit1 => "!",
|
||||
LinuxScanCodes::Digit2 => "@",
|
||||
LinuxScanCodes::Digit3 => "#",
|
||||
LinuxScanCodes::Digit4 => "$",
|
||||
LinuxScanCodes::Digit5 => "%",
|
||||
LinuxScanCodes::Digit6 => "^",
|
||||
LinuxScanCodes::Digit7 => "&",
|
||||
LinuxScanCodes::Digit8 => "*",
|
||||
LinuxScanCodes::Digit9 => "(",
|
||||
LinuxScanCodes::Backquote => "~",
|
||||
LinuxScanCodes::Minus => "_",
|
||||
LinuxScanCodes::Equal => "+",
|
||||
LinuxScanCodes::LeftBracket => "{",
|
||||
LinuxScanCodes::RightBracket => "}",
|
||||
LinuxScanCodes::Backslash => "|",
|
||||
LinuxScanCodes::Semicolon => ":",
|
||||
LinuxScanCodes::Quote => "\"",
|
||||
LinuxScanCodes::Comma => "<",
|
||||
LinuxScanCodes::Period => ">",
|
||||
LinuxScanCodes::Slash => "?",
|
||||
LinuxScanCodes::IntlBackslash => "unknown",
|
||||
LinuxScanCodes::IntlRo => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, any(feature = "wayland", feature = "x11")))]
|
||||
mod tests {
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
use x11rb::{protocol::xkb::ConnectionExt, xcb_ffi::XCBConnection};
|
||||
use xkbcommon::xkb::{
|
||||
CONTEXT_NO_FLAGS, KEYMAP_COMPILE_NO_FLAGS, Keymap,
|
||||
x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION},
|
||||
};
|
||||
|
||||
use crate::platform::linux::keyboard::LinuxScanCodes;
|
||||
|
||||
use super::LinuxKeyboardMapper;
|
||||
|
||||
fn get_keymap() -> Keymap {
|
||||
static XCB_CONNECTION: LazyLock<XCBConnection> =
|
||||
LazyLock::new(|| XCBConnection::connect(None).unwrap().0);
|
||||
|
||||
let _ = XCB_CONNECTION
|
||||
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
let xkb_context = xkbcommon::xkb::Context::new(CONTEXT_NO_FLAGS);
|
||||
let xkb_device_id = xkbcommon::xkb::x11::get_core_keyboard_device_id(&*XCB_CONNECTION);
|
||||
xkbcommon::xkb::x11::keymap_new_from_device(
|
||||
&xkb_context,
|
||||
&*XCB_CONNECTION,
|
||||
xkb_device_id,
|
||||
KEYMAP_COMPILE_NO_FLAGS,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_us_layout_mapper() {
|
||||
let keymap = get_keymap();
|
||||
let mapper = LinuxKeyboardMapper::new(&keymap, 0, 0, 0);
|
||||
for scan_code in super::LinuxScanCodes::iter() {
|
||||
if scan_code == LinuxScanCodes::IntlBackslash || scan_code == LinuxScanCodes::IntlRo {
|
||||
continue;
|
||||
}
|
||||
let keycode = xkbcommon::xkb::Keycode::new(scan_code as u32);
|
||||
let key = mapper
|
||||
.get_key(keycode, &mut crate::Modifiers::default())
|
||||
.unwrap();
|
||||
assert_eq!(key.as_str(), scan_code.to_str());
|
||||
|
||||
let shifted_key = mapper
|
||||
.get_key(
|
||||
keycode,
|
||||
&mut crate::Modifiers {
|
||||
shift: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(shifted_key.as_str(), scan_code.to_shifted());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ use crate::{
|
||||
Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use super::LinuxKeyboardMapper;
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
pub(crate) const SCROLL_LINES: f32 = 3.0;
|
||||
|
||||
@@ -710,6 +713,7 @@ pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
|
||||
impl crate::Keystroke {
|
||||
pub(super) fn from_xkb(
|
||||
state: &State,
|
||||
keyboard_mapper: &LinuxKeyboardMapper,
|
||||
mut modifiers: crate::Modifiers,
|
||||
keycode: Keycode,
|
||||
) -> Self {
|
||||
@@ -718,76 +722,67 @@ impl crate::Keystroke {
|
||||
let key_sym = state.key_get_one_sym(keycode);
|
||||
|
||||
let key = match key_sym {
|
||||
Keysym::space => "space".to_owned(),
|
||||
Keysym::BackSpace => "backspace".to_owned(),
|
||||
Keysym::Return => "enter".to_owned(),
|
||||
Keysym::Prior => "pageup".to_owned(),
|
||||
Keysym::Next => "pagedown".to_owned(),
|
||||
// Keysym::Tab => "tab".to_owned(),
|
||||
Keysym::ISO_Left_Tab => "tab".to_owned(),
|
||||
Keysym::KP_Prior => "pageup".to_owned(),
|
||||
Keysym::KP_Next => "pagedown".to_owned(),
|
||||
Keysym::uparrow => "up".to_owned(),
|
||||
Keysym::downarrow => "down".to_owned(),
|
||||
Keysym::leftarrow => "left".to_owned(),
|
||||
Keysym::rightarrow => "right".to_owned(),
|
||||
Keysym::Home | Keysym::KP_Home => "home".to_owned(),
|
||||
Keysym::End | Keysym::KP_End => "end".to_owned(),
|
||||
Keysym::Prior | Keysym::KP_Prior => "pageup".to_owned(),
|
||||
Keysym::Next | Keysym::KP_Next => "pagedown".to_owned(),
|
||||
Keysym::XF86_Back => "back".to_owned(),
|
||||
Keysym::XF86_Forward => "forward".to_owned(),
|
||||
Keysym::Escape => "escape".to_owned(),
|
||||
Keysym::Insert | Keysym::KP_Insert => "insert".to_owned(),
|
||||
Keysym::Delete | Keysym::KP_Delete => "delete".to_owned(),
|
||||
Keysym::Menu => "menu".to_owned(),
|
||||
Keysym::XF86_Cut => "cut".to_owned(),
|
||||
Keysym::XF86_Copy => "copy".to_owned(),
|
||||
Keysym::XF86_Paste => "paste".to_owned(),
|
||||
Keysym::XF86_New => "new".to_owned(),
|
||||
Keysym::XF86_Open => "open".to_owned(),
|
||||
Keysym::XF86_Save => "save".to_owned(),
|
||||
|
||||
Keysym::comma => ",".to_owned(),
|
||||
Keysym::period => ".".to_owned(),
|
||||
Keysym::less => "<".to_owned(),
|
||||
Keysym::greater => ">".to_owned(),
|
||||
Keysym::slash => "/".to_owned(),
|
||||
Keysym::question => "?".to_owned(),
|
||||
|
||||
Keysym::semicolon => ";".to_owned(),
|
||||
Keysym::colon => ":".to_owned(),
|
||||
Keysym::apostrophe => "'".to_owned(),
|
||||
Keysym::quotedbl => "\"".to_owned(),
|
||||
|
||||
Keysym::bracketleft => "[".to_owned(),
|
||||
Keysym::braceleft => "{".to_owned(),
|
||||
Keysym::bracketright => "]".to_owned(),
|
||||
Keysym::braceright => "}".to_owned(),
|
||||
Keysym::backslash => "\\".to_owned(),
|
||||
Keysym::bar => "|".to_owned(),
|
||||
|
||||
Keysym::grave => "`".to_owned(),
|
||||
Keysym::asciitilde => "~".to_owned(),
|
||||
Keysym::exclam => "!".to_owned(),
|
||||
Keysym::at => "@".to_owned(),
|
||||
Keysym::numbersign => "#".to_owned(),
|
||||
Keysym::dollar => "$".to_owned(),
|
||||
Keysym::percent => "%".to_owned(),
|
||||
Keysym::asciicircum => "^".to_owned(),
|
||||
Keysym::ampersand => "&".to_owned(),
|
||||
Keysym::asterisk => "*".to_owned(),
|
||||
Keysym::parenleft => "(".to_owned(),
|
||||
Keysym::parenright => ")".to_owned(),
|
||||
Keysym::minus => "-".to_owned(),
|
||||
Keysym::underscore => "_".to_owned(),
|
||||
Keysym::equal => "=".to_owned(),
|
||||
Keysym::plus => "+".to_owned(),
|
||||
|
||||
_ => {
|
||||
let name = xkb::keysym_get_name(key_sym).to_lowercase();
|
||||
if key_sym.is_keypad_key() {
|
||||
name.replace("kp_", "")
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
Keysym::F1 => "f1".to_owned(),
|
||||
Keysym::F2 => "f2".to_owned(),
|
||||
Keysym::F3 => "f3".to_owned(),
|
||||
Keysym::F4 => "f4".to_owned(),
|
||||
Keysym::F5 => "f5".to_owned(),
|
||||
Keysym::F6 => "f6".to_owned(),
|
||||
Keysym::F7 => "f7".to_owned(),
|
||||
Keysym::F8 => "f8".to_owned(),
|
||||
Keysym::F9 => "f9".to_owned(),
|
||||
Keysym::F10 => "f10".to_owned(),
|
||||
Keysym::F11 => "f11".to_owned(),
|
||||
Keysym::F12 => "f12".to_owned(),
|
||||
Keysym::F13 => "f13".to_owned(),
|
||||
Keysym::F14 => "f14".to_owned(),
|
||||
Keysym::F15 => "f15".to_owned(),
|
||||
Keysym::F16 => "f16".to_owned(),
|
||||
Keysym::F17 => "f17".to_owned(),
|
||||
Keysym::F18 => "f18".to_owned(),
|
||||
Keysym::F19 => "f19".to_owned(),
|
||||
Keysym::F20 => "f20".to_owned(),
|
||||
Keysym::F21 => "f21".to_owned(),
|
||||
Keysym::F22 => "f22".to_owned(),
|
||||
Keysym::F23 => "f23".to_owned(),
|
||||
Keysym::F24 => "f24".to_owned(),
|
||||
_ => keyboard_mapper
|
||||
.get_key(keycode, &mut modifiers)
|
||||
.unwrap_or_else(|| {
|
||||
let name = xkb::keysym_get_name(key_sym).to_lowercase();
|
||||
if key_sym.is_keypad_key() {
|
||||
name.replace("kp_", "")
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
if modifiers.shift {
|
||||
// we only include the shift for upper-case letters by convention,
|
||||
// so don't include for numbers and symbols, but do include for
|
||||
// tab/enter, etc.
|
||||
if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
|
||||
modifiers.shift = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore control characters (and DEL) for the purposes of key_char
|
||||
let key_char =
|
||||
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
|
||||
@@ -798,65 +793,6 @@ impl crate::Keystroke {
|
||||
key_char,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which symbol the dead key represents
|
||||
* <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
|
||||
*/
|
||||
pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
|
||||
match keysym {
|
||||
Keysym::dead_grave => Some("`".to_owned()),
|
||||
Keysym::dead_acute => Some("´".to_owned()),
|
||||
Keysym::dead_circumflex => Some("^".to_owned()),
|
||||
Keysym::dead_tilde => Some("~".to_owned()),
|
||||
Keysym::dead_macron => Some("¯".to_owned()),
|
||||
Keysym::dead_breve => Some("˘".to_owned()),
|
||||
Keysym::dead_abovedot => Some("˙".to_owned()),
|
||||
Keysym::dead_diaeresis => Some("¨".to_owned()),
|
||||
Keysym::dead_abovering => Some("˚".to_owned()),
|
||||
Keysym::dead_doubleacute => Some("˝".to_owned()),
|
||||
Keysym::dead_caron => Some("ˇ".to_owned()),
|
||||
Keysym::dead_cedilla => Some("¸".to_owned()),
|
||||
Keysym::dead_ogonek => Some("˛".to_owned()),
|
||||
Keysym::dead_iota => Some("ͅ".to_owned()),
|
||||
Keysym::dead_voiced_sound => Some("゙".to_owned()),
|
||||
Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
|
||||
Keysym::dead_belowdot => Some("̣̣".to_owned()),
|
||||
Keysym::dead_hook => Some("̡".to_owned()),
|
||||
Keysym::dead_horn => Some("̛".to_owned()),
|
||||
Keysym::dead_stroke => Some("̶̶".to_owned()),
|
||||
Keysym::dead_abovecomma => Some("̓̓".to_owned()),
|
||||
Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
|
||||
Keysym::dead_doublegrave => Some("̏".to_owned()),
|
||||
Keysym::dead_belowring => Some("˳".to_owned()),
|
||||
Keysym::dead_belowmacron => Some("̱".to_owned()),
|
||||
Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
|
||||
Keysym::dead_belowtilde => Some("̰".to_owned()),
|
||||
Keysym::dead_belowbreve => Some("̮".to_owned()),
|
||||
Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
|
||||
Keysym::dead_invertedbreve => Some("̯".to_owned()),
|
||||
Keysym::dead_belowcomma => Some("̦".to_owned()),
|
||||
Keysym::dead_currency => None,
|
||||
Keysym::dead_lowline => None,
|
||||
Keysym::dead_aboveverticalline => None,
|
||||
Keysym::dead_belowverticalline => None,
|
||||
Keysym::dead_longsolidusoverlay => None,
|
||||
Keysym::dead_a => None,
|
||||
Keysym::dead_A => None,
|
||||
Keysym::dead_e => None,
|
||||
Keysym::dead_E => None,
|
||||
Keysym::dead_i => None,
|
||||
Keysym::dead_I => None,
|
||||
Keysym::dead_o => None,
|
||||
Keysym::dead_O => None,
|
||||
Keysym::dead_u => None,
|
||||
Keysym::dead_U => None,
|
||||
Keysym::dead_small_schwa => Some("ə".to_owned()),
|
||||
Keysym::dead_capital_schwa => Some("Ə".to_owned()),
|
||||
Keysym::dead_greek => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
|
||||
@@ -61,22 +61,20 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
||||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
|
||||
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, ffi::XKB_KEYMAP_FORMAT_TEXT_V1};
|
||||
|
||||
use super::{
|
||||
display::WaylandDisplay,
|
||||
window::{ImeInput, WaylandWindowStatePtr},
|
||||
};
|
||||
|
||||
use crate::platform::{PlatformWindow, blade::BladeContext};
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
|
||||
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
|
||||
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||
MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
|
||||
PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScrollDelta,
|
||||
ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
|
||||
LinuxKeyboardLayout, LinuxKeyboardMapper, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
||||
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels,
|
||||
ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
|
||||
};
|
||||
use crate::{
|
||||
SharedString,
|
||||
@@ -92,6 +90,10 @@ use crate::{
|
||||
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
platform::{PlatformWindow, blade::BladeContext},
|
||||
underlying_dead_key,
|
||||
};
|
||||
|
||||
/// Used to convert evdev scancode to xkb scancode
|
||||
const MIN_KEYCODE: u32 = 8;
|
||||
@@ -208,9 +210,11 @@ pub(crate) struct WaylandClientState {
|
||||
// Output to scale mapping
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
|
||||
keyboard_layout: LinuxKeyboardLayout,
|
||||
keymap_state: Option<xkb::State>,
|
||||
compose_state: Option<xkb::compose::State>,
|
||||
keyboard_layout: LinuxKeyboardLayout,
|
||||
keyboard_mapper: Option<Rc<LinuxKeyboardMapper>>,
|
||||
keyboard_mapper_cache: HashMap<String, Rc<LinuxKeyboardMapper>>,
|
||||
drag: DragState,
|
||||
click: ClickState,
|
||||
repeat: KeyRepeat,
|
||||
@@ -340,7 +344,7 @@ impl WaylandClientStatePtr {
|
||||
text_input.commit();
|
||||
}
|
||||
|
||||
pub fn handle_keyboard_layout_change(&self) {
|
||||
pub fn handle_keyboard_layout_change(&self, locked_group: u32) {
|
||||
let client = self.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let changed = if let Some(keymap_state) = &state.keymap_state {
|
||||
@@ -350,6 +354,17 @@ impl WaylandClientStatePtr {
|
||||
let changed = layout_name != state.keyboard_layout.name();
|
||||
if changed {
|
||||
state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into());
|
||||
let mapper = state
|
||||
.keyboard_mapper_cache
|
||||
.entry(layout_name.to_string())
|
||||
.or_insert(Rc::new(LinuxKeyboardMapper::new(
|
||||
&keymap,
|
||||
0,
|
||||
0,
|
||||
locked_group,
|
||||
)))
|
||||
.clone();
|
||||
state.keyboard_mapper = Some(mapper);
|
||||
}
|
||||
changed
|
||||
} else {
|
||||
@@ -447,6 +462,7 @@ impl WaylandClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let conn = Connection::connect_to_env().unwrap();
|
||||
|
||||
let keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME);
|
||||
let (globals, mut event_queue) =
|
||||
registry_queue_init::<WaylandClientStatePtr>(&conn).unwrap();
|
||||
let qh = event_queue.handle();
|
||||
@@ -567,9 +583,11 @@ impl WaylandClient {
|
||||
in_progress_outputs,
|
||||
windows: HashMap::default(),
|
||||
common,
|
||||
keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME),
|
||||
keymap_state: None,
|
||||
compose_state: None,
|
||||
keyboard_layout,
|
||||
keyboard_mapper: None,
|
||||
keyboard_mapper_cache: HashMap::default(),
|
||||
drag: DragState {
|
||||
data_offer: None,
|
||||
window: None,
|
||||
@@ -1214,7 +1232,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
state.compose_state = get_xkb_compose_state(&xkb_context);
|
||||
drop(state);
|
||||
|
||||
this.handle_keyboard_layout_change();
|
||||
this.handle_keyboard_layout_change(0);
|
||||
}
|
||||
wl_keyboard::Event::Enter { surface, .. } => {
|
||||
state.keyboard_focused_window = get_window(&mut state, &surface.id());
|
||||
@@ -1270,7 +1288,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
|
||||
if group != old_layout {
|
||||
this.handle_keyboard_layout_change();
|
||||
this.handle_keyboard_layout_change(group);
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::Key {
|
||||
@@ -1288,20 +1306,25 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
let focused_window = focused_window.clone();
|
||||
|
||||
let keymap_state = state.keymap_state.as_ref().unwrap();
|
||||
let keycode = Keycode::from(key + MIN_KEYCODE);
|
||||
let keyboard_mapper = state.keyboard_mapper.as_ref().unwrap();
|
||||
let keycode = xkb::Keycode::from(key + MIN_KEYCODE);
|
||||
let keysym = keymap_state.key_get_one_sym(keycode);
|
||||
|
||||
match key_state {
|
||||
wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
|
||||
let mut keystroke =
|
||||
Keystroke::from_xkb(&keymap_state, state.modifiers, keycode);
|
||||
let mut keystroke = Keystroke::from_xkb(
|
||||
keymap_state,
|
||||
keyboard_mapper,
|
||||
state.modifiers,
|
||||
keycode,
|
||||
);
|
||||
if let Some(mut compose) = state.compose_state.take() {
|
||||
compose.feed(keysym);
|
||||
match compose.status() {
|
||||
xkb::Status::Composing => {
|
||||
keystroke.key_char = None;
|
||||
state.pre_edit_text =
|
||||
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
|
||||
compose.utf8().or(underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
state.pre_edit_text.clone().unwrap_or(String::default());
|
||||
drop(state);
|
||||
@@ -1318,7 +1341,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
xkb::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
let new_pre_edit = Keystroke::underlying_dead_key(keysym);
|
||||
let new_pre_edit = underlying_dead_key(keysym);
|
||||
state.pre_edit_text = new_pre_edit.clone();
|
||||
drop(state);
|
||||
if let Some(pre_edit) = pre_edit {
|
||||
@@ -1379,7 +1402,12 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => {
|
||||
let input = PlatformInput::KeyUp(KeyUpEvent {
|
||||
keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode),
|
||||
keystroke: Keystroke::from_xkb(
|
||||
keymap_state,
|
||||
keyboard_mapper,
|
||||
state.modifiers,
|
||||
keycode,
|
||||
),
|
||||
});
|
||||
|
||||
if state.repeat.current_keycode == Some(keycode) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::{Capslock, xcb_flush};
|
||||
use core::str;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -49,7 +48,7 @@ use super::{
|
||||
};
|
||||
|
||||
use crate::platform::{
|
||||
LinuxCommon, PlatformWindow,
|
||||
Capslock, LinuxCommon, PlatformWindow,
|
||||
blade::BladeContext,
|
||||
linux::{
|
||||
DEFAULT_CURSOR_ICON_NAME, LinuxClient, get_xkb_compose_state, is_within_click_distance,
|
||||
@@ -58,13 +57,14 @@ use crate::platform::{
|
||||
reveal_path_internal,
|
||||
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
|
||||
},
|
||||
xcb_flush,
|
||||
};
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
|
||||
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform,
|
||||
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions,
|
||||
ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
modifiers_from_xinput_info, point, px,
|
||||
LinuxKeyboardLayout, LinuxKeyboardMapper, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
Pixels, Platform, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point,
|
||||
RequestFrameOptions, ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
modifiers_from_xinput_info, point, px, underlying_dead_key,
|
||||
};
|
||||
|
||||
/// Value for DeviceId parameters which selects all devices.
|
||||
@@ -200,6 +200,8 @@ pub struct X11ClientState {
|
||||
pub(crate) xkb: xkbc::State,
|
||||
previous_xkb_state: XKBStateNotiy,
|
||||
keyboard_layout: LinuxKeyboardLayout,
|
||||
keyboard_mapper: Rc<LinuxKeyboardMapper>,
|
||||
keyboard_mapper_cache: HashMap<String, Rc<LinuxKeyboardMapper>>,
|
||||
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
pub modifiers: Modifiers,
|
||||
@@ -403,22 +405,24 @@ impl X11Client {
|
||||
|
||||
let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS);
|
||||
let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection);
|
||||
let xkb_state = {
|
||||
let xkb_keymap = xkbc::x11::keymap_new_from_device(
|
||||
&xkb_context,
|
||||
&xcb_connection,
|
||||
xkb_device_id,
|
||||
xkbc::KEYMAP_COMPILE_NO_FLAGS,
|
||||
);
|
||||
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
|
||||
};
|
||||
let xkb_keymap = xkbc::x11::keymap_new_from_device(
|
||||
&xkb_context,
|
||||
&xcb_connection,
|
||||
xkb_device_id,
|
||||
xkbc::KEYMAP_COMPILE_NO_FLAGS,
|
||||
);
|
||||
let xkb_state =
|
||||
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id);
|
||||
let compose_state = get_xkb_compose_state(&xkb_context);
|
||||
let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE);
|
||||
let layout_name = xkb_state
|
||||
.get_keymap()
|
||||
.layout_get_name(layout_idx)
|
||||
.to_string();
|
||||
let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into());
|
||||
let keyboard_layout = LinuxKeyboardLayout::new(layout_name.clone().into());
|
||||
let keyboard_mapper = Rc::new(LinuxKeyboardMapper::new(&xkb_keymap, 0, 0, 0));
|
||||
let mut keyboard_mapper_cache = HashMap::default();
|
||||
keyboard_mapper_cache.insert(layout_name, keyboard_mapper.clone());
|
||||
|
||||
let gpu_context = BladeContext::new().context("Unable to init GPU context")?;
|
||||
|
||||
@@ -512,6 +516,8 @@ impl X11Client {
|
||||
xkb: xkb_state,
|
||||
previous_xkb_state: XKBStateNotiy::default(),
|
||||
keyboard_layout,
|
||||
keyboard_mapper,
|
||||
keyboard_mapper_cache,
|
||||
ximc,
|
||||
xim_handler,
|
||||
|
||||
@@ -972,24 +978,27 @@ impl X11Client {
|
||||
};
|
||||
state.xkb = xkb_state;
|
||||
drop(state);
|
||||
self.handle_keyboard_layout_change();
|
||||
self.handle_keyboard_layout_change(depressed_layout, latched_layout, locked_layout);
|
||||
}
|
||||
Event::XkbStateNotify(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let old_layout = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
|
||||
let new_layout = u32::from(event.group);
|
||||
let base_group = event.base_group as u32;
|
||||
let latched_group = event.latched_group as u32;
|
||||
let locked_group = event.locked_group.into();
|
||||
state.xkb.update_mask(
|
||||
event.base_mods.into(),
|
||||
event.latched_mods.into(),
|
||||
event.locked_mods.into(),
|
||||
event.base_group as u32,
|
||||
event.latched_group as u32,
|
||||
event.locked_group.into(),
|
||||
base_group,
|
||||
latched_group,
|
||||
locked_group,
|
||||
);
|
||||
state.previous_xkb_state = XKBStateNotiy {
|
||||
depressed_layout: event.base_group as u32,
|
||||
latched_layout: event.latched_group as u32,
|
||||
locked_layout: event.locked_group.into(),
|
||||
depressed_layout: base_group,
|
||||
latched_layout: latched_group,
|
||||
locked_layout: locked_group,
|
||||
};
|
||||
|
||||
let modifiers = Modifiers::from_xkb(&state.xkb);
|
||||
@@ -1016,7 +1025,7 @@ impl X11Client {
|
||||
}
|
||||
|
||||
if new_layout != old_layout {
|
||||
self.handle_keyboard_layout_change();
|
||||
self.handle_keyboard_layout_change(base_group, latched_group, locked_group);
|
||||
}
|
||||
}
|
||||
Event::KeyPress(event) => {
|
||||
@@ -1037,7 +1046,12 @@ impl X11Client {
|
||||
xkb_state.latched_layout,
|
||||
xkb_state.locked_layout,
|
||||
);
|
||||
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
let mut keystroke = crate::Keystroke::from_xkb(
|
||||
&state.xkb,
|
||||
&state.keyboard_mapper,
|
||||
modifiers,
|
||||
code,
|
||||
);
|
||||
let keysym = state.xkb.key_get_one_sym(code);
|
||||
if keysym.is_modifier_key() {
|
||||
return Some(());
|
||||
@@ -1054,9 +1068,8 @@ impl X11Client {
|
||||
}
|
||||
xkbc::Status::Composing => {
|
||||
keystroke.key_char = None;
|
||||
state.pre_edit_text = compose_state
|
||||
.utf8()
|
||||
.or(crate::Keystroke::underlying_dead_key(keysym));
|
||||
state.pre_edit_text =
|
||||
compose_state.utf8().or(underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
state.pre_edit_text.clone().unwrap_or(String::default());
|
||||
drop(state);
|
||||
@@ -1069,7 +1082,7 @@ impl X11Client {
|
||||
if let Some(pre_edit) = pre_edit {
|
||||
window.handle_ime_commit(pre_edit);
|
||||
}
|
||||
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
|
||||
if let Some(current_key) = underlying_dead_key(keysym) {
|
||||
window.handle_ime_preedit(current_key);
|
||||
}
|
||||
state = self.0.borrow_mut();
|
||||
@@ -1105,7 +1118,12 @@ impl X11Client {
|
||||
xkb_state.latched_layout,
|
||||
xkb_state.locked_layout,
|
||||
);
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
let keystroke = crate::Keystroke::from_xkb(
|
||||
&state.xkb,
|
||||
&state.keyboard_mapper,
|
||||
modifiers,
|
||||
code,
|
||||
);
|
||||
let keysym = state.xkb.key_get_one_sym(code);
|
||||
if keysym.is_modifier_key() {
|
||||
return Some(());
|
||||
@@ -1327,6 +1345,7 @@ impl X11Client {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.pre_key_char_down = Some(Keystroke::from_xkb(
|
||||
&state.xkb,
|
||||
&state.keyboard_mapper,
|
||||
state.modifiers,
|
||||
event.detail.into(),
|
||||
));
|
||||
@@ -1412,13 +1431,29 @@ impl X11Client {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn handle_keyboard_layout_change(&self) {
|
||||
fn handle_keyboard_layout_change(
|
||||
&self,
|
||||
base_group: u32,
|
||||
latched_group: u32,
|
||||
locked_group: u32,
|
||||
) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
|
||||
let keymap = state.xkb.get_keymap();
|
||||
let layout_name = keymap.layout_get_name(layout_idx);
|
||||
if layout_name != state.keyboard_layout.name() {
|
||||
state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into());
|
||||
let mapper = state
|
||||
.keyboard_mapper_cache
|
||||
.entry(layout_name.to_string())
|
||||
.or_insert(Rc::new(LinuxKeyboardMapper::new(
|
||||
&keymap,
|
||||
base_group,
|
||||
latched_group,
|
||||
locked_group,
|
||||
)))
|
||||
.clone();
|
||||
state.keyboard_mapper = mapper;
|
||||
if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
|
||||
drop(state);
|
||||
callback();
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::{
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
|
||||
WindowAppearance, WindowParams, hash,
|
||||
};
|
||||
@@ -170,6 +170,7 @@ pub(crate) struct MacPlatformState {
|
||||
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||
dock_menu: Option<id>,
|
||||
menus: Option<Vec<OwnedMenu>>,
|
||||
}
|
||||
|
||||
impl Default for MacPlatform {
|
||||
@@ -207,6 +208,7 @@ impl MacPlatform {
|
||||
finish_launching: None,
|
||||
dock_menu: None,
|
||||
on_keyboard_layout_change: None,
|
||||
menus: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -226,7 +228,7 @@ impl MacPlatform {
|
||||
|
||||
unsafe fn create_menu_bar(
|
||||
&self,
|
||||
menus: Vec<Menu>,
|
||||
menus: &Vec<Menu>,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
keymap: &Keymap,
|
||||
@@ -241,7 +243,7 @@ impl MacPlatform {
|
||||
menu.setTitle_(menu_title);
|
||||
menu.setDelegate_(delegate);
|
||||
|
||||
for item_config in menu_config.items {
|
||||
for item_config in &menu_config.items {
|
||||
menu.addItem_(Self::create_menu_item(
|
||||
item_config,
|
||||
delegate,
|
||||
@@ -277,7 +279,7 @@ impl MacPlatform {
|
||||
dock_menu.setDelegate_(delegate);
|
||||
for item_config in menu_items {
|
||||
dock_menu.addItem_(Self::create_menu_item(
|
||||
item_config,
|
||||
&item_config,
|
||||
delegate,
|
||||
actions,
|
||||
keymap,
|
||||
@@ -289,7 +291,7 @@ impl MacPlatform {
|
||||
}
|
||||
|
||||
unsafe fn create_menu_item(
|
||||
item: MenuItem,
|
||||
item: &MenuItem,
|
||||
delegate: id,
|
||||
actions: &mut Vec<Box<dyn Action>>,
|
||||
keymap: &Keymap,
|
||||
@@ -399,7 +401,7 @@ impl MacPlatform {
|
||||
|
||||
let tag = actions.len() as NSInteger;
|
||||
let _: () = msg_send![item, setTag: tag];
|
||||
actions.push(action);
|
||||
actions.push(action.boxed_clone());
|
||||
item
|
||||
}
|
||||
MenuItem::Submenu(Menu { name, items }) => {
|
||||
@@ -865,10 +867,15 @@ impl Platform for MacPlatform {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
let mut state = self.0.lock();
|
||||
let actions = &mut state.menu_actions;
|
||||
let menu = self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap);
|
||||
let menu = self.create_menu_bar(&menus, NSWindow::delegate(app), actions, keymap);
|
||||
drop(state);
|
||||
app.setMainMenu_(menu);
|
||||
}
|
||||
self.0.lock().menus = Some(menus.into_iter().map(|menu| menu.owned()).collect());
|
||||
}
|
||||
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
self.0.lock().menus.clone()
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {
|
||||
|
||||
@@ -1248,7 +1248,9 @@ fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) ->
|
||||
}
|
||||
_ => {
|
||||
lock.pending_surrogate = None;
|
||||
String::from_utf16(&[code_point]).ok()
|
||||
char::from_u32(code_point as u32)
|
||||
.filter(|c| !c.is_control())
|
||||
.map(|c| c.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1369,6 +1369,31 @@ impl Window {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_keystroke_interceptors(
|
||||
&mut self,
|
||||
event: &dyn Any,
|
||||
context_stack: Vec<KeyContext>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.keystroke_interceptors
|
||||
.clone()
|
||||
.retain(&(), move |callback| {
|
||||
(callback)(
|
||||
&KeystrokeEvent {
|
||||
keystroke: key_down_event.keystroke.clone(),
|
||||
action: None,
|
||||
context_stack: context_stack.clone(),
|
||||
},
|
||||
self,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
|
||||
/// that are currently on the stack to be returned to the app.
|
||||
pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) {
|
||||
@@ -3522,6 +3547,13 @@ impl Window {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.propagate_event = true;
|
||||
self.dispatch_keystroke_interceptors(event, self.context_stack(), cx);
|
||||
if !cx.propagate_event {
|
||||
self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut currently_pending = self.pending_input.take().unwrap_or_default();
|
||||
if currently_pending.focus.is_some() && currently_pending.focus != self.focus {
|
||||
currently_pending = PendingInput::default();
|
||||
@@ -3570,7 +3602,6 @@ impl Window {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.propagate_event = true;
|
||||
for binding in match_result.bindings {
|
||||
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
|
||||
if !cx.propagate_event {
|
||||
|
||||
@@ -13,6 +13,7 @@ pub enum IconName {
|
||||
AiBedrock,
|
||||
AiDeepSeek,
|
||||
AiEdit,
|
||||
AiGemini,
|
||||
AiGoogle,
|
||||
AiLmStudio,
|
||||
AiMistral,
|
||||
@@ -20,7 +21,6 @@ pub enum IconName {
|
||||
AiOpenAi,
|
||||
AiOpenRouter,
|
||||
AiVZero,
|
||||
AiXAi,
|
||||
AiZed,
|
||||
ArrowCircle,
|
||||
ArrowDown,
|
||||
@@ -66,7 +66,6 @@ pub enum IconName {
|
||||
Circle,
|
||||
CircleOff,
|
||||
CircleHelp,
|
||||
Clipboard,
|
||||
Close,
|
||||
Cloud,
|
||||
Code,
|
||||
@@ -118,7 +117,6 @@ pub enum IconName {
|
||||
File,
|
||||
FileCode,
|
||||
FileCreate,
|
||||
FileDelete,
|
||||
FileDiff,
|
||||
FileDoc,
|
||||
FileGeneric,
|
||||
@@ -215,7 +213,6 @@ pub enum IconName {
|
||||
Scissors,
|
||||
Screen,
|
||||
ScrollText,
|
||||
SearchCode,
|
||||
SearchSelection,
|
||||
SelectAll,
|
||||
Send,
|
||||
@@ -253,6 +250,19 @@ pub enum IconName {
|
||||
TextSnippet,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
ToolBulb,
|
||||
ToolCopy,
|
||||
ToolDeleteFile,
|
||||
ToolDiagnostics,
|
||||
ToolFolder,
|
||||
ToolHammer,
|
||||
ToolNotification,
|
||||
ToolPencil,
|
||||
ToolRead,
|
||||
ToolRegex,
|
||||
ToolSearch,
|
||||
ToolTerminal,
|
||||
ToolWeb,
|
||||
Trash,
|
||||
TrashAlt,
|
||||
Triangle,
|
||||
|
||||
@@ -116,12 +116,6 @@ pub enum LanguageModelCompletionError {
|
||||
provider: LanguageModelProviderName,
|
||||
message: String,
|
||||
},
|
||||
#[error("{message}")]
|
||||
UpstreamProviderError {
|
||||
message: String,
|
||||
status: StatusCode,
|
||||
retry_after: Option<Duration>,
|
||||
},
|
||||
#[error("HTTP response error from {provider}'s API: status {status_code} - {message:?}")]
|
||||
HttpResponseError {
|
||||
provider: LanguageModelProviderName,
|
||||
@@ -184,21 +178,6 @@ pub enum LanguageModelCompletionError {
|
||||
}
|
||||
|
||||
impl LanguageModelCompletionError {
|
||||
fn parse_upstream_error_json(message: &str) -> Option<(StatusCode, String)> {
|
||||
let error_json = serde_json::from_str::<serde_json::Value>(message).ok()?;
|
||||
let upstream_status = error_json
|
||||
.get("upstream_status")
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(|status| u16::try_from(status).ok())
|
||||
.and_then(|status| StatusCode::from_u16(status).ok())?;
|
||||
let inner_message = error_json
|
||||
.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(message)
|
||||
.to_string();
|
||||
Some((upstream_status, inner_message))
|
||||
}
|
||||
|
||||
pub fn from_cloud_failure(
|
||||
upstream_provider: LanguageModelProviderName,
|
||||
code: String,
|
||||
@@ -212,18 +191,6 @@ impl LanguageModelCompletionError {
|
||||
Self::PromptTooLarge {
|
||||
tokens: Some(tokens),
|
||||
}
|
||||
} else if code == "upstream_http_error" {
|
||||
if let Some((upstream_status, inner_message)) =
|
||||
Self::parse_upstream_error_json(&message)
|
||||
{
|
||||
return Self::from_http_status(
|
||||
upstream_provider,
|
||||
upstream_status,
|
||||
inner_message,
|
||||
retry_after,
|
||||
);
|
||||
}
|
||||
anyhow!("completion request failed, code: {code}, message: {message}").into()
|
||||
} else if let Some(status_code) = code
|
||||
.strip_prefix("upstream_http_")
|
||||
.and_then(|code| StatusCode::from_str(code).ok())
|
||||
@@ -734,104 +701,3 @@ impl From<String> for LanguageModelProviderName {
|
||||
Self(SharedString::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_cloud_failure_with_upstream_http_error() {
|
||||
let error = LanguageModelCompletionError::from_cloud_failure(
|
||||
String::from("anthropic").into(),
|
||||
"upstream_http_error".to_string(),
|
||||
r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":503}"#.to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
match error {
|
||||
LanguageModelCompletionError::ServerOverloaded { provider, .. } => {
|
||||
assert_eq!(provider.0, "anthropic");
|
||||
}
|
||||
_ => panic!(
|
||||
"Expected ServerOverloaded error for 503 status, got: {:?}",
|
||||
error
|
||||
),
|
||||
}
|
||||
|
||||
let error = LanguageModelCompletionError::from_cloud_failure(
|
||||
String::from("anthropic").into(),
|
||||
"upstream_http_error".to_string(),
|
||||
r#"{"code":"upstream_http_error","message":"Internal server error","upstream_status":500}"#.to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
match error {
|
||||
LanguageModelCompletionError::ApiInternalServerError { provider, message } => {
|
||||
assert_eq!(provider.0, "anthropic");
|
||||
assert_eq!(message, "Internal server error");
|
||||
}
|
||||
_ => panic!(
|
||||
"Expected ApiInternalServerError for 500 status, got: {:?}",
|
||||
error
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_cloud_failure_with_standard_format() {
|
||||
let error = LanguageModelCompletionError::from_cloud_failure(
|
||||
String::from("anthropic").into(),
|
||||
"upstream_http_503".to_string(),
|
||||
"Service unavailable".to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
match error {
|
||||
LanguageModelCompletionError::ServerOverloaded { provider, .. } => {
|
||||
assert_eq!(provider.0, "anthropic");
|
||||
}
|
||||
_ => panic!("Expected ServerOverloaded error for upstream_http_503"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upstream_http_error_connection_timeout() {
|
||||
let error = LanguageModelCompletionError::from_cloud_failure(
|
||||
String::from("anthropic").into(),
|
||||
"upstream_http_error".to_string(),
|
||||
r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":503}"#.to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
match error {
|
||||
LanguageModelCompletionError::ServerOverloaded { provider, .. } => {
|
||||
assert_eq!(provider.0, "anthropic");
|
||||
}
|
||||
_ => panic!(
|
||||
"Expected ServerOverloaded error for connection timeout with 503 status, got: {:?}",
|
||||
error
|
||||
),
|
||||
}
|
||||
|
||||
let error = LanguageModelCompletionError::from_cloud_failure(
|
||||
String::from("anthropic").into(),
|
||||
"upstream_http_error".to_string(),
|
||||
r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":500}"#.to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
match error {
|
||||
LanguageModelCompletionError::ApiInternalServerError { provider, message } => {
|
||||
assert_eq!(provider.0, "anthropic");
|
||||
assert_eq!(
|
||||
message,
|
||||
"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout"
|
||||
);
|
||||
}
|
||||
_ => panic!(
|
||||
"Expected ApiInternalServerError for connection timeout with 500 status, got: {:?}",
|
||||
error
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +391,7 @@ pub struct LanguageModelRequest {
|
||||
pub tool_choice: Option<LanguageModelToolChoice>,
|
||||
pub stop: Vec<String>,
|
||||
pub temperature: Option<f32>,
|
||||
pub thinking_allowed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
open_router = { workspace = true, features = ["schemars"] }
|
||||
vercel = { workspace = true, features = ["schemars"] }
|
||||
x_ai = { workspace = true, features = ["schemars"] }
|
||||
partial-json-fixer.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -20,7 +20,6 @@ use crate::provider::ollama::OllamaLanguageModelProvider;
|
||||
use crate::provider::open_ai::OpenAiLanguageModelProvider;
|
||||
use crate::provider::open_router::OpenRouterLanguageModelProvider;
|
||||
use crate::provider::vercel::VercelLanguageModelProvider;
|
||||
use crate::provider::x_ai::XAiLanguageModelProvider;
|
||||
pub use crate::settings::*;
|
||||
|
||||
pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||
@@ -82,6 +81,5 @@ fn register_language_model_providers(
|
||||
VercelLanguageModelProvider::new(client.http_client(), cx),
|
||||
cx,
|
||||
);
|
||||
registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
|
||||
registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
|
||||
}
|
||||
|
||||
@@ -10,4 +10,3 @@ pub mod ollama;
|
||||
pub mod open_ai;
|
||||
pub mod open_router;
|
||||
pub mod vercel;
|
||||
pub mod x_ai;
|
||||
|
||||
@@ -663,7 +663,9 @@ pub fn into_anthropic(
|
||||
} else {
|
||||
Some(anthropic::StringOrContents::String(system_message))
|
||||
},
|
||||
thinking: if let AnthropicModelMode::Thinking { budget_tokens } = mode {
|
||||
thinking: if request.thinking_allowed
|
||||
&& let AnthropicModelMode::Thinking { budget_tokens } = mode
|
||||
{
|
||||
Some(anthropic::Thinking::Enabled { budget_tokens })
|
||||
} else {
|
||||
None
|
||||
@@ -1108,6 +1110,7 @@ mod tests {
|
||||
temperature: None,
|
||||
tools: vec![],
|
||||
tool_choice: None,
|
||||
thinking_allowed: true,
|
||||
};
|
||||
|
||||
let anthropic_request = into_anthropic(
|
||||
|
||||
@@ -799,7 +799,9 @@ pub fn into_bedrock(
|
||||
max_tokens: max_output_tokens,
|
||||
system: Some(system_message),
|
||||
tools: Some(tool_config),
|
||||
thinking: if let BedrockModelMode::Thinking { budget_tokens } = mode {
|
||||
thinking: if request.thinking_allowed
|
||||
&& let BedrockModelMode::Thinking { budget_tokens } = mode
|
||||
{
|
||||
Some(bedrock::Thinking::Enabled { budget_tokens })
|
||||
} else {
|
||||
None
|
||||
|
||||