Compare commits

..

2 Commits

Author SHA1 Message Date
Richard Feldman
1ff8521612 wip 2025-03-20 09:36:42 -04:00
Richard Feldman
ca22d5d4a3 Add shell_parser crate 2025-03-19 22:19:16 -04:00
1090 changed files with 36530 additions and 51845 deletions

View File

@@ -19,10 +19,6 @@
# https://github.com/zed-industries/zed/pull/2394 # https://github.com/zed-industries/zed/pull/2394
eca93c124a488b4e538946cd2d313bd571aa2b86 eca93c124a488b4e538946cd2d313bd571aa2b86
# 2024-02-15 Format YAML files
# https://github.com/zed-industries/zed/pull/7887
a161a7d0c95ca7505bf9218bfae640ee5444c88b
# 2024-02-25 Format JSON files in assets/ # 2024-02-25 Format JSON files in assets/
# https://github.com/zed-industries/zed/pull/8405 # https://github.com/zed-industries/zed/pull/8405
ffdda588b41f7d9d270ffe76cab116f828ad545e ffdda588b41f7d9d270ffe76cab116f828ad545e

View File

@@ -209,6 +209,7 @@ jobs:
cargo check -p workspace cargo check -p workspace
cargo build -p remote_server cargo build -p remote_server
cargo check -p gpui --examples cargo check -p gpui --examples
script/check-rust-livekit-macos
# Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug. # Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file - name: Clean CI config file
@@ -234,7 +235,7 @@ jobs:
clean: false clean: false
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet" cache-provider: "buildjet"
@@ -286,7 +287,7 @@ jobs:
clean: false clean: false
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet" cache-provider: "buildjet"
@@ -333,7 +334,7 @@ jobs:
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ env.ZED_WORKSPACE }} workspaces: ${{ env.ZED_WORKSPACE }}
@@ -392,7 +393,7 @@ jobs:
Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse Copy-Item -Path "${{ github.workspace }}" -Destination "${{ env.ZED_WORKSPACE }}" -Recurse
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ env.ZED_WORKSPACE }} workspaces: ${{ env.ZED_WORKSPACE }}

View File

@@ -1,7 +1,7 @@
name: "Close Stale Issues" name: "Close Stale Issues"
on: on:
schedule: schedule:
- cron: "0 7,9,11 * * 3" - cron: "0 7,9,11 * * 2"
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View File

@@ -59,9 +59,7 @@ jobs:
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true' if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
run: | run: |
TAG="${{ github.event.release.tag_name }}" TAG="${{ github.event.release.tag_name }}"
cat << 'EOF' > release_body.txt echo \"${{ toJSON(github.event.release.body) }}\" > release_body.txt
${{ github.event.release.body }}
EOF
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \ jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
> release_data.json > release_data.json
curl -X POST "https://zed.dev/api/send_release_notes_email" \ curl -X POST "https://zed.dev/api/send_release_notes_email" \

View File

@@ -22,7 +22,7 @@ jobs:
clean: false clean: false
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github" cache-provider: "github"

View File

@@ -182,7 +182,8 @@ jobs:
runner: buildjet-16vcpu-ubuntu-2204 runner: buildjet-16vcpu-ubuntu-2204
install_nix: true install_nix: true
- os: arm Mac - os: arm Mac
runner: [macOS, ARM64, test] # TODO: once other macs are provisioned for nix, remove that constraint from the runner
runner: [macOS, ARM64, nix]
install_nix: false install_nix: false
- os: arm Linux - os: arm Linux
runner: buildjet-16vcpu-ubuntu-2204-arm runner: buildjet-16vcpu-ubuntu-2204-arm

1114
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -70,7 +70,6 @@ members = [
"crates/html_to_markdown", "crates/html_to_markdown",
"crates/http_client", "crates/http_client",
"crates/http_client_tls", "crates/http_client_tls",
"crates/icons",
"crates/image_viewer", "crates/image_viewer",
"crates/indexed_docs", "crates/indexed_docs",
"crates/inline_completion", "crates/inline_completion",
@@ -87,6 +86,7 @@ members = [
"crates/languages", "crates/languages",
"crates/livekit_api", "crates/livekit_api",
"crates/livekit_client", "crates/livekit_client",
"crates/livekit_client_macos",
"crates/lmstudio", "crates/lmstudio",
"crates/lsp", "crates/lsp",
"crates/markdown", "crates/markdown",
@@ -124,12 +124,14 @@ members = [
"crates/rope", "crates/rope",
"crates/rpc", "crates/rpc",
"crates/schema_generator", "crates/schema_generator",
"crates/scripting_tool",
"crates/search", "crates/search",
"crates/semantic_index", "crates/semantic_index",
"crates/semantic_version", "crates/semantic_version",
"crates/session", "crates/session",
"crates/settings", "crates/settings",
"crates/settings_ui", "crates/settings_ui",
"crates/shell_parser",
"crates/snippet", "crates/snippet",
"crates/snippet_provider", "crates/snippet_provider",
"crates/snippets_ui", "crates/snippets_ui",
@@ -170,8 +172,6 @@ members = [
"crates/zed", "crates/zed",
"crates/zed_actions", "crates/zed_actions",
"crates/zeta", "crates/zeta",
"crates/zlog",
"crates/zlog_settings",
# #
# Extensions # Extensions
@@ -198,7 +198,7 @@ default-members = ["crates/zed"]
[workspace.package] [workspace.package]
publish = false publish = false
edition = "2024" edition = "2021"
[workspace.dependencies] [workspace.dependencies]
@@ -274,7 +274,6 @@ gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" } html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" } http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" } http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" } image_viewer = { path = "crates/image_viewer" }
indexed_docs = { path = "crates/indexed_docs" } indexed_docs = { path = "crates/indexed_docs" }
inline_completion = { path = "crates/inline_completion" } inline_completion = { path = "crates/inline_completion" }
@@ -291,6 +290,7 @@ language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" } languages = { path = "crates/languages" }
livekit_api = { path = "crates/livekit_api" } livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" } livekit_client = { path = "crates/livekit_client" }
livekit_client_macos = { path = "crates/livekit_client_macos" }
lmstudio = { path = "crates/lmstudio" } lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" } lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" } markdown = { path = "crates/markdown" }
@@ -328,6 +328,7 @@ reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" } rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" } rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" } rpc = { path = "crates/rpc" }
scripting_tool = { path = "crates/scripting_tool" }
search = { path = "crates/search" } search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" } semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" } semantic_version = { path = "crates/semantic_version" }
@@ -374,8 +375,6 @@ worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" } zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" } zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" } zeta = { path = "crates/zeta" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
# #
# External crates # External crates
@@ -418,9 +417,8 @@ circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.4", features = ["derive"] }
cocoa = "0.26" cocoa = "0.26"
cocoa-foundation = "0.2.0" cocoa-foundation = "0.2.0"
core-video = { version = "0.4.3", features = ["metal"] }
convert_case = "0.8.0" convert_case = "0.8.0"
core-foundation = "0.10.0" core-foundation = "0.9.3"
core-foundation-sys = "0.8.6" core-foundation-sys = "0.8.6"
ctor = "0.4.0" ctor = "0.4.0"
dashmap = "6.0" dashmap = "6.0"
@@ -458,13 +456,17 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0" linkify = "0.10.0"
linkme = "0.3.31" linkme = "0.3.31"
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
"dispatcher",
"services-dispatcher",
"rustls-tls-native-roots",
], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0" markup5ever_rcdom = "0.3.0"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] } mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
nanoid = "0.4" nanoid = "0.4"
nbformat = { version = "0.10.0" } nbformat = { version = "0.10.0" }
nix = "0.29" nix = "0.29"
open = "5.0.0"
num-format = "0.4.4" num-format = "0.4.4"
ordered-float = "2.1.1" ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] } palette = { version = "0.7.5", default-features = false, features = ["std"] }
@@ -546,7 +548,6 @@ time = { version = "0.3", features = [
tiny_http = "0.8" tiny_http = "0.8"
toml = "0.8" toml = "0.8"
tokio = { version = "1" } tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"]}
tower-http = "0.4.4" tower-http = "0.4.4"
tree-sitter = { version = "0.25.3", features = ["wasm"] } tree-sitter = { version = "0.25.3", features = ["wasm"] }
tree-sitter-bash = "0.23" tree-sitter-bash = "0.23"
@@ -592,7 +593,7 @@ which = "6.0.0"
wit-component = "0.221" wit-component = "0.221"
zed_llm_client = "0.4" zed_llm_client = "0.4"
zstd = "0.11" zstd = "0.11"
metal = "0.29" metal = "0.31"
[workspace.dependencies.async-stripe] [workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe" git = "https://github.com/zed-industries/async-stripe"
@@ -609,7 +610,7 @@ features = [
] ]
[workspace.dependencies.windows] [workspace.dependencies.windows]
version = "0.61" version = "0.60"
features = [ features = [
"Foundation_Collections", "Foundation_Collections",
"Foundation_Numerics", "Foundation_Numerics",

View File

@@ -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-arrow-right-left"><path d="m16 3 4 4-4 4"/><path d="M20 7H4"/><path d="m8 21-4-4 4-4"/><path d="M4 17h16"/></svg>

Before

Width:  |  Height:  |  Size: 316 B

View File

@@ -1,3 +0,0 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.4 2.6H5.75C5.75 2.50717 5.71312 2.41815 5.64749 2.35251C5.58185 2.28688 5.49283 2.25 5.4 2.25V2.6ZM2.6 2.25C2.4067 2.25 2.25 2.4067 2.25 2.6C2.25 2.7933 2.4067 2.95 2.6 2.95V2.25ZM5.05 5.4C5.05 5.5933 5.2067 5.75 5.4 5.75C5.5933 5.75 5.75 5.5933 5.75 5.4H5.05ZM2.35252 5.15251C2.21583 5.2892 2.21583 5.5108 2.35252 5.64748C2.4892 5.78417 2.7108 5.78417 2.84749 5.64748L2.35252 5.15251ZM5.4 2.25H2.6V2.95H5.4V2.25ZM5.05 2.6V5.4H5.75V2.6H5.05ZM5.15252 2.35251L2.35252 5.15251L2.84749 5.64748L5.64749 2.84748L5.15252 2.35251Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 650 B

View File

@@ -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-brain"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/></svg>

Before

Width:  |  Height:  |  Size: 718 B

View File

@@ -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-cog"><path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 2v2"/><path d="M12 22v-2"/><path d="m17 20.66-1-1.73"/><path d="M11 10.27 7 3.34"/><path d="m20.66 17-1.73-1"/><path d="m3.34 7 1.73 1"/><path d="M14 12h8"/><path d="M2 12h2"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m17 3.34-1 1.73"/><path d="m11 13.73-4 6.93"/></svg>

Before

Width:  |  Height:  |  Size: 608 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>

Before

Width:  |  Height:  |  Size: 249 B

View File

@@ -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-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>

Before

Width:  |  Height:  |  Size: 267 B

View File

@@ -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="M6 8H10" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 10V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 762 B

View File

@@ -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

View File

@@ -1 +1 @@
<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> <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-message-circle-more"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg>

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -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-user-round-pen-icon lucide-user-round-pen"><path d="M2 21a8 8 0 0 1 10.821-7.487"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="8" r="5"/></svg>

Before

Width:  |  Height:  |  Size: 461 B

View File

@@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.27772 1.38585L4.39187 4.07909C4.34653 4.21692 4.26946 4.34219 4.16685 4.44479C4.06425 4.5474 3.93898 4.62447 3.80115 4.66981L1.10791 5.55566L3.80115 6.44151C3.93898 6.48685 4.06425 6.56392 4.16685 6.66653C4.26946 6.76913 4.34653 6.8944 4.39187 7.03223L5.27772 9.72547L6.16357 7.03223C6.20891 6.8944 6.28598 6.76913 6.38859 6.66653C6.49119 6.56392 6.61646 6.48685 6.7543 6.44151L9.44753 5.55566L6.7543 4.66981C6.61646 4.62447 6.49119 4.5474 6.38859 4.44479C6.28598 4.34219 6.20891 4.21692 6.16357 4.07909L5.27772 1.38585Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.35938 12.3555C8.35938 12.0664 8.52734 11.8086 9.00781 11.3594L10.2031 10.2344C10.6094 9.85156 10.7891 9.60156 10.7891 9.34375C10.7891 9.05469 10.5781 8.85938 10.2734 8.85938C10.0391 8.85938 9.87109 8.95312 9.66406 9.21094C9.42578 9.50781 9.25391 9.60938 8.99219 9.60938C8.61719 9.60938 8.35156 9.35938 8.35156 9.01172C8.35156 8.25 9.26953 7.57812 10.3594 7.57812C11.4961 7.57812 12.3438 8.26172 12.3438 9.17969C12.3438 9.75391 12.0391 10.3008 11.418 10.8516L10.4961 11.6719V11.7344H11.8047C12.2578 11.7344 12.5391 11.9766 12.5391 12.3711C12.5391 12.7656 12.2656 13 11.8047 13H9.08203C8.65234 13 8.35938 12.7383 8.35938 12.3555Z" fill="black"/>
<path d="M11.0834 1.38585V3.71918M9.91675 2.55248H12.2501" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -126,6 +126,7 @@
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }], // "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette", "ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers", "ctrl-;": "editor::ToggleLineNumbers",
"ctrl-k ctrl-r": "git::Restore",
"ctrl-'": "editor::ToggleSelectedDiffHunks", "ctrl-'": "editor::ToggleSelectedDiffHunks",
"ctrl-\"": "editor::ExpandAllDiffHunks", "ctrl-\"": "editor::ExpandAllDiffHunks",
"ctrl-i": "editor::ShowSignatureHelp", "ctrl-i": "editor::ShowSignatureHelp",
@@ -137,22 +138,6 @@
"shift-f9": "editor::EditLogBreakpoint" "shift-f9": "editor::EditLogBreakpoint"
} }
}, },
{
"context": "Editor && !assistant_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AssistantDiff",
"bindings": {
"ctrl-y": "assistant2::ToggleKeep",
"ctrl-k ctrl-r": "assistant2::Reject"
}
},
{ {
"context": "Editor && mode == full", "context": "Editor && mode == full",
"bindings": { "bindings": {
@@ -210,7 +195,7 @@
"ctrl-shift-g": "search::SelectPreviousMatch", "ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-alt-/": "assistant::ToggleModelSelector", "ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory", "ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "assistant::OpenPromptLibrary", "ctrl-k l": "assistant::DeployPromptLibrary",
"new": "assistant::NewChat", "new": "assistant::NewChat",
"ctrl-t": "assistant::NewChat", "ctrl-t": "assistant::NewChat",
"ctrl-n": "assistant::NewChat" "ctrl-n": "assistant::NewChat"
@@ -397,6 +382,9 @@
"ctrl-k v": "markdown::OpenPreviewToTheSide", "ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview", "ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames", "ctrl-alt-shift-c": "editor::DisplayCursorNames",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext",
"alt-.": "editor::GoToHunk", "alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk" "alt-,": "editor::GoToPreviousHunk"
} }
@@ -629,10 +617,7 @@
"bindings": { "bindings": {
"ctrl-n": "assistant2::NewThread", "ctrl-n": "assistant2::NewThread",
"new": "assistant2::NewThread", "new": "assistant2::NewThread",
"ctrl-alt-n": "assistant2::NewPromptEditor",
"ctrl-shift-h": "assistant2::OpenHistory", "ctrl-shift-h": "assistant2::OpenHistory",
"ctrl-alt-c": "assistant2::OpenConfiguration",
"ctrl-i": "assistant2::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector", "ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "assistant2::ToggleContextPicker", "ctrl-shift-a": "assistant2::ToggleContextPicker",
"ctrl-e": "assistant2::ChatMode", "ctrl-e": "assistant2::ChatMode",
@@ -650,9 +635,7 @@
{ {
"context": "MessageEditor > Editor", "context": "MessageEditor > Editor",
"bindings": { "bindings": {
"enter": "assistant2::Chat", "enter": "assistant2::Chat"
"ctrl-i": "assistant2::ToggleProfileSelector",
"shift-ctrl-r": "assistant2::OpenAssistantDiff"
} }
}, },
{ {
@@ -771,11 +754,9 @@
"escape": "git_panel::ToggleFocus", "escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit", "ctrl-enter": "git::Commit",
"alt-enter": "menu::SecondaryConfirm", "alt-enter": "menu::SecondaryConfirm",
"delete": ["git::RestoreFile", { "skip_prompt": false }], "delete": "git::RestoreFile",
"backspace": ["git::RestoreFile", { "skip_prompt": false }], "shift-delete": "git::RestoreFile",
"shift-delete": ["git::RestoreFile", { "skip_prompt": false }], "backspace": "git::RestoreFile"
"ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
"ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
} }
}, },
{ {

View File

@@ -147,6 +147,10 @@
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }], "ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-cmd-space": "editor::ShowCharacterPalette", "ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers", "cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "git::Restore",
"cmd-alt-y": "git::ToggleStaged",
"cmd-y": "git::StageAndNext",
"cmd-shift-y": "git::UnstageAndNext",
"cmd-'": "editor::ToggleSelectedDiffHunks", "cmd-'": "editor::ToggleSelectedDiffHunks",
"cmd-\"": "editor::ExpandAllDiffHunks", "cmd-\"": "editor::ExpandAllDiffHunks",
"cmd-alt-g b": "editor::ToggleGitBlame", "cmd-alt-g b": "editor::ToggleGitBlame",
@@ -227,24 +231,6 @@
"ctrl-alt-enter": "repl::RunInPlace" "ctrl-alt-enter": "repl::RunInPlace"
} }
}, },
{
"context": "Editor && !assistant_diff",
"use_key_equivalents": true,
"bindings": {
"cmd-alt-z": "git::Restore",
"cmd-alt-y": "git::ToggleStaged",
"cmd-y": "git::StageAndNext",
"cmd-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AssistantDiff",
"use_key_equivalents": true,
"bindings": {
"cmd-y": "assistant2::ToggleKeep",
"cmd-alt-z": "assistant2::Reject"
}
},
{ {
"context": "AssistantPanel", "context": "AssistantPanel",
"use_key_equivalents": true, "use_key_equivalents": true,
@@ -255,7 +241,7 @@
"cmd-shift-g": "search::SelectPreviousMatch", "cmd-shift-g": "search::SelectPreviousMatch",
"cmd-alt-/": "assistant::ToggleModelSelector", "cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory", "cmd-k h": "assistant::DeployHistory",
"cmd-k l": "assistant::OpenPromptLibrary", "cmd-k l": "assistant::DeployPromptLibrary",
"cmd-t": "assistant::NewChat", "cmd-t": "assistant::NewChat",
"cmd-n": "assistant::NewChat" "cmd-n": "assistant::NewChat"
} }
@@ -281,10 +267,8 @@
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-n": "assistant2::NewThread", "cmd-n": "assistant2::NewThread",
"cmd-alt-n": "assistant2::NewPromptEditor", "cmd-alt-p": "assistant2::NewPromptEditor",
"cmd-shift-h": "assistant2::OpenHistory", "cmd-shift-h": "assistant2::OpenHistory",
"cmd-alt-c": "assistant2::OpenConfiguration",
"cmd-i": "assistant2::ToggleProfileSelector",
"cmd-alt-/": "assistant::ToggleModelSelector", "cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-shift-a": "assistant2::ToggleContextPicker", "cmd-shift-a": "assistant2::ToggleContextPicker",
"cmd-e": "assistant2::ChatMode", "cmd-e": "assistant2::ChatMode",
@@ -303,9 +287,7 @@
"context": "MessageEditor > Editor", "context": "MessageEditor > Editor",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"enter": "assistant2::Chat", "enter": "assistant2::Chat"
"cmd-i": "assistant2::ToggleProfileSelector",
"shift-ctrl-r": "assistant2::OpenAssistantDiff"
} }
}, },
{ {
@@ -819,10 +801,9 @@
"shift-tab": "git_panel::FocusEditor", "shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus", "escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit", "cmd-enter": "git::Commit",
"backspace": ["git::RestoreFile", { "skip_prompt": false }], "delete": "git::RestoreFile",
"delete": ["git::RestoreFile", { "skip_prompt": false }], "cmd-backspace": "git::RestoreFile",
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }], "backspace": "git::RestoreFile"
"cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
} }
}, },
{ {

View File

@@ -258,10 +258,9 @@
"u": "vim::ConvertToLowerCase", "u": "vim::ConvertToLowerCase",
"shift-u": "vim::ConvertToUpperCase", "shift-u": "vim::ConvertToUpperCase",
"shift-o": "vim::OtherEnd", "shift-o": "vim::OtherEnd",
"o": "vim::OtherEndRowAware", "o": "vim::OtherEnd",
"d": "vim::VisualDelete", "d": "vim::VisualDelete",
"x": "vim::VisualDelete", "x": "vim::VisualDelete",
"delete": "vim::VisualDelete",
"shift-d": "vim::VisualDeleteLine", "shift-d": "vim::VisualDeleteLine",
"shift-x": "vim::VisualDeleteLine", "shift-x": "vim::VisualDeleteLine",
"y": "vim::VisualYank", "y": "vim::VisualYank",
@@ -339,10 +338,6 @@
"w": "vim::NextWordStart", "w": "vim::NextWordStart",
"e": "vim::NextWordEnd", "e": "vim::NextWordEnd",
"b": "vim::PreviousWordStart", "b": "vim::PreviousWordStart",
"x": "vim::CurrentLine",
"X": "vim::CurrentLine",
"y": "vim::HelixYank",
"p": "vim::HelixPaste",
"h": "vim::Left", "h": "vim::Left",
"j": "vim::Down", "j": "vim::Down",

View File

@@ -1,34 +1,18 @@
You are an AI assistant integrated into a code editor. You have the programming ability of an expert programmer who takes pride in writing high-quality code and is driven to the point of obsession about solving problems effectively. Your goal is to do one of the following two things: You are an AI assistant integrated into a text editor. Your goal is to do one of the following two things:
1. Help users answer questions and perform tasks related to their codebase. 1. Help users answer questions and perform tasks related to their codebase.
2. Answer general-purpose questions unrelated to their particular codebase. 2. Answer general-purpose questions unrelated to their particular codebase.
It will be up to you to decide which of these you are doing based on what the user has told you. When unclear, ask clarifying questions to understand the user's intent before proceeding. It will be up to you to decide which of these you are doing based on what the user has told you. When unclear, ask clarifying questions to understand the user's intent before proceeding.
You should only perform actions that modify the user's system if explicitly requested by the user: You should only perform actions that modify the users system if explicitly requested by the user:
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the users system without explicit instruction. - If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the users system without explicit instruction.
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so. - If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
- The editing actions you perform might produce errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made. You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
Be concise and direct in your responses. Never apologize or thank the user. Don't comment that you have just realized or understood something. When you are going to make a tool call, tersely explain your reasoning for choosing to use that tool, with no flourishes or commentary beyond that information. For example, rather than saying "You're absolutely right! Thank you for providing that context. Now I understand that we're missing a dependency, and I need to add it:" say "I'll add that missing dependency:" instead. Also, don't restate what a tool call is about to do (or just did). For example, don't say "Now I'm going to check diagnostics to see if there are any warnings or errors," followed by running a tool which checks diagnostics and reports warnings or errors; instead, just request the tool call without saying anything. Be concise and direct in your responses.
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files: The user has opened a project that contains the following root directories/files:
{{#each worktrees}} {{#each worktrees}}
- `{{root_name}}` (absolute path: `{{abs_path}}`) - {{root_name}} (absolute path: {{abs_path}})
{{/each}} {{/each}}
{{#if has_rules}}
There are rules that apply to these root directories:
{{#each worktrees}}
{{#if rules_file}}
`{{root_name}}/{{rules_file.rel_path}}`:
``````
{{{rules_file.text}}}
``````
{{/if}}
{{/each}}
{{/if}}

View File

@@ -25,7 +25,7 @@
// Features that can be globally enabled or disabled // Features that can be globally enabled or disabled
"features": { "features": {
// Which edit prediction provider to use. // Which edit prediction provider to use.
"edit_prediction_provider": "zed" "edit_prediction_provider": "copilot"
}, },
// The name of a font to use for rendering text in the editor // The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Plex Mono", "buffer_font_family": "Zed Plex Mono",
@@ -115,15 +115,6 @@
"confirm_quit": false, "confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened. // Whether to restore last closed project when fresh Zed instance is opened.
"restore_on_startup": "last_session", "restore_on_startup": "last_session",
// Whether to attempt to restore previous file's state when opening it again.
// The state is stored per pane.
// When disabled, defaults are applied instead of the state restoration.
//
// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane.
// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default.
//
// Default: true
"restore_on_file_reopen": true,
// Size of the drop target in the editor. // Size of the drop target in the editor.
"drop_target_size": 0.2, "drop_target_size": 0.2,
// Whether the window should be closed when using 'close active item' on a window with no tabs. // Whether the window should be closed when using 'close active item' on a window with no tabs.
@@ -164,8 +155,6 @@
// //
// Default: not set, defaults to "bar" // Default: not set, defaults to "bar"
"cursor_shape": null, "cursor_shape": null,
// Determines when the mouse cursor should be hidden in an editor or input box.
"hide_mouse": "on_typing_and_movement",
// How to highlight the current line in the editor. // How to highlight the current line in the editor.
// //
// 1. Don't highlight the current line: // 1. Don't highlight the current line:
@@ -195,11 +184,6 @@
// Whether to show the signature help after completion or a bracket pair inserted. // Whether to show the signature help after completion or a bracket pair inserted.
// If `auto_signature_help` is enabled, this setting will be treated as enabled also. // If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": false, "show_signature_help_after_edits": false,
// What to do when go to definition yields no results.
//
// 1. Do nothing: `none`
// 2. Find references for the same symbol: `find_all_references` (default)
"go_to_definition_fallback": "find_all_references",
// Whether to show wrap guides (vertical rulers) in the editor. // Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value // Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any // if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -438,8 +422,6 @@
"project_panel": { "project_panel": {
// Whether to show the project panel button in the status bar // Whether to show the project panel button in the status bar
"button": true, "button": true,
// Whether to hide the gitignore entries in the project panel.
"hide_gitignore": false,
// Default width of the project panel. // Default width of the project panel.
"default_width": 240, "default_width": 240,
// Where to dock the project panel. Can be 'left' or 'right'. // Where to dock the project panel. Can be 'left' or 'right'.
@@ -632,53 +614,7 @@
"provider": "zed.dev", "provider": "zed.dev",
// The model to use. // The model to use.
"model": "claude-3-5-sonnet-latest" "model": "claude-3-5-sonnet-latest"
}, }
"default_profile": "write",
"profiles": {
"ask": {
"name": "Ask",
"tools": {
"diagnostics": true,
"fetch": true,
"list-directory": true,
"now": true,
"path-search": true,
"read-file": true,
"regex-search": true,
"thinking": true
}
},
"write": {
"name": "Write",
"tools": {
"bash": true,
"batch-tool": true,
"code-symbols": true,
"copy-path": true,
"create-file": true,
"delete-path": true,
"diagnostics": true,
"find-replace-file": true,
"edit-files": false,
"fetch": true,
"list-directory": true,
"move-path": true,
"now": true,
"path-search": true,
"read-file": true,
"regex-search": true,
"symbol-info": true,
"thinking": true
}
}
},
// Where to show notifications when an agent has either completed
// its response, or else needs confirmation before it can run a
// tool action.
// "primary_screen" - Show the notification only on your primary screen (default)
// "all_screens" - Show these notifications on all screens
// "never" - Never show these notifications
"notify_when_agent_waiting": "primary_screen"
}, },
// The settings for slash commands. // The settings for slash commands.
"slash_commands": { "slash_commands": {
@@ -1045,7 +981,7 @@
// "alternate_scroll": "on", // "alternate_scroll": "on",
// 2. Default alternate scroll mode to off // 2. Default alternate scroll mode to off
// "alternate_scroll": "off", // "alternate_scroll": "off",
"alternate_scroll": "on", "alternate_scroll": "off",
// Set whether the option key behaves as the meta key. // Set whether the option key behaves as the meta key.
// May take 2 values: // May take 2 values:
// 1. Rely on default platform handling of option key, on macOS // 1. Rely on default platform handling of option key, on macOS
@@ -1288,19 +1224,11 @@
"allowed": true "allowed": true
} }
}, },
"LaTeX": {
"format_on_save": "on",
"formatter": "language_server",
"language_servers": ["texlab", "..."],
"prettier": {
"allowed": false
}
},
"Markdown": { "Markdown": {
"format_on_save": "off", "format_on_save": "off",
"use_on_type_format": false, "use_on_type_format": false,
"allow_rewrap": "anywhere", "allow_rewrap": "anywhere",
"soft_wrap": "editor_width", "soft_wrap": "bounded",
"prettier": { "prettier": {
"allowed": true "allowed": true
} }
@@ -1482,6 +1410,11 @@
"dev": { "dev": {
// "theme": "Andromeda" // "theme": "Andromeda"
}, },
// Task-related settings.
"task": {
// Whether to show task status indicator in the status bar. Default: true
"show_status_indicator": true
},
// Whether to show full labels in line indicator or short ones // Whether to show full labels in line indicator or short ones
// //
// Values: // Values:

View File

@@ -3,9 +3,9 @@ use editor::Editor;
use extension_host::ExtensionStore; use extension_host::ExtensionStore;
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter, actions, percentage, Animation, AnimationExt as _, App, Context, CursorStyle, Entity,
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement, EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
Styled, Transformation, Window, actions, percentage, StatefulInteractiveElement, Styled, Transformation, Window,
}; };
use language::{BinaryStatus, LanguageRegistry, LanguageServerId}; use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
use project::{ use project::{
@@ -14,9 +14,9 @@ use project::{
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration}; use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
use util::truncate_and_trailoff; use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]); actions!(activity_indicator, [ShowErrorMessage]);

View File

@@ -2,9 +2,9 @@ mod supported_countries;
use std::{pin::Pin, str::FromStr}; use std::{pin::Pin, str::FromStr};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{anyhow, Context as _, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use futures::{AsyncBufReadExt, AsyncReadExt, Stream, StreamExt, io::BufReader, stream::BoxStream}; use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use http_client::http::{HeaderMap, HeaderValue}; use http_client::http::{HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -24,16 +24,6 @@ pub struct AnthropicModelCacheConfiguration {
pub max_cache_anchors: usize, pub max_cache_anchors: usize,
} }
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub enum AnthropicModelMode {
#[default]
Default,
Thinking {
budget_tokens: Option<u32>,
},
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model { pub enum Model {
@@ -42,11 +32,6 @@ pub enum Model {
Claude3_5Sonnet, Claude3_5Sonnet,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")] #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet, Claude3_7Sonnet,
#[serde(
rename = "claude-3-7-sonnet-thinking",
alias = "claude-3-7-sonnet-thinking-latest"
)]
Claude3_7SonnetThinking,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")] #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku, Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")] #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -69,8 +54,6 @@ pub enum Model {
default_temperature: Option<f32>, default_temperature: Option<f32>,
#[serde(default)] #[serde(default)]
extra_beta_headers: Vec<String>, extra_beta_headers: Vec<String>,
#[serde(default)]
mode: AnthropicModelMode,
}, },
} }
@@ -78,8 +61,6 @@ impl Model {
pub fn from_id(id: &str) -> Result<Self> { pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-3-5-sonnet") { if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet) Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-7-sonnet-thinking") {
Ok(Self::Claude3_7SonnetThinking)
} else if id.starts_with("claude-3-7-sonnet") { } else if id.starts_with("claude-3-7-sonnet") {
Ok(Self::Claude3_7Sonnet) Ok(Self::Claude3_7Sonnet)
} else if id.starts_with("claude-3-5-haiku") { } else if id.starts_with("claude-3-5-haiku") {
@@ -99,20 +80,6 @@ impl Model {
match self { match self {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest", Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest", Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-haiku-20240307",
Self::Custom { name, .. } => name,
}
}
/// The id of the model that should be used for making API requests
pub fn request_id(&self) -> &str {
match self {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest", Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest", Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229", Model::Claude3Sonnet => "claude-3-sonnet-20240229",
@@ -125,7 +92,6 @@ impl Model {
match self { match self {
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet", Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet", Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
Self::Claude3_5Haiku => "Claude 3.5 Haiku", Self::Claude3_5Haiku => "Claude 3.5 Haiku",
Self::Claude3Opus => "Claude 3 Opus", Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet", Self::Claude3Sonnet => "Claude 3 Sonnet",
@@ -141,7 +107,6 @@ impl Model {
Self::Claude3_5Sonnet Self::Claude3_5Sonnet
| Self::Claude3_5Haiku | Self::Claude3_5Haiku
| Self::Claude3_7Sonnet | Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration { | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
min_total_token: 2_048, min_total_token: 2_048,
should_speculate: true, should_speculate: true,
@@ -160,7 +125,6 @@ impl Model {
Self::Claude3_5Sonnet Self::Claude3_5Sonnet
| Self::Claude3_5Haiku | Self::Claude3_5Haiku
| Self::Claude3_7Sonnet | Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3Opus | Self::Claude3Opus
| Self::Claude3Sonnet | Self::Claude3Sonnet
| Self::Claude3Haiku => 200_000, | Self::Claude3Haiku => 200_000,
@@ -171,10 +135,7 @@ impl Model {
pub fn max_output_tokens(&self) -> u32 { pub fn max_output_tokens(&self) -> u32 {
match self { match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096, Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
Self::Claude3_5Sonnet Self::Claude3_5Sonnet | Self::Claude3_7Sonnet | Self::Claude3_5Haiku => 8_192,
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku => 8_192,
Self::Custom { Self::Custom {
max_output_tokens, .. max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096), } => max_output_tokens.unwrap_or(4_096),
@@ -185,7 +146,6 @@ impl Model {
match self { match self {
Self::Claude3_5Sonnet Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet | Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::Claude3_5Haiku | Self::Claude3_5Haiku
| Self::Claude3Opus | Self::Claude3Opus
| Self::Claude3Sonnet | Self::Claude3Sonnet
@@ -197,21 +157,6 @@ impl Model {
} }
} }
pub fn mode(&self) -> AnthropicModelMode {
match self {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
},
Self::Custom { mode, .. } => mode.clone(),
}
}
pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"]; pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"];
pub fn beta_headers(&self) -> String { pub fn beta_headers(&self) -> String {
@@ -243,7 +188,7 @@ impl Model {
{ {
tool_override tool_override
} else { } else {
self.request_id() self.id()
} }
} }
} }
@@ -464,8 +409,6 @@ pub async fn extract_tool_args_from_events(
Err(error) => Some(Err(error)), Err(error) => Some(Err(error)),
Ok(Event::ContentBlockDelta { index, delta }) => match delta { Ok(Event::ContentBlockDelta { index, delta }) => match delta {
ContentDelta::TextDelta { .. } => None, ContentDelta::TextDelta { .. } => None,
ContentDelta::ThinkingDelta { .. } => None,
ContentDelta::SignatureDelta { .. } => None,
ContentDelta::InputJsonDelta { partial_json } => { ContentDelta::InputJsonDelta { partial_json } => {
if index == tool_use_index { if index == tool_use_index {
Some(Ok(partial_json)) Some(Ok(partial_json))
@@ -544,10 +487,6 @@ pub enum RequestContent {
pub enum ResponseContent { pub enum ResponseContent {
#[serde(rename = "text")] #[serde(rename = "text")]
Text { text: String }, Text { text: String },
#[serde(rename = "thinking")]
Thinking { thinking: String },
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
#[serde(rename = "tool_use")] #[serde(rename = "tool_use")]
ToolUse { ToolUse {
id: String, id: String,
@@ -579,19 +518,6 @@ pub enum ToolChoice {
Tool { name: String }, Tool { name: String },
} }
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Thinking {
Enabled { budget_tokens: Option<u32> },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StringOrContents {
String(String),
Content(Vec<RequestContent>),
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Request { pub struct Request {
pub model: String, pub model: String,
@@ -600,11 +526,9 @@ pub struct Request {
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Tool>, pub tools: Vec<Tool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking: Option<Thinking>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>, pub tool_choice: Option<ToolChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub system: Option<StringOrContents>, pub system: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>, pub metadata: Option<Metadata>,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -685,10 +609,6 @@ pub enum Event {
pub enum ContentDelta { pub enum ContentDelta {
#[serde(rename = "text_delta")] #[serde(rename = "text_delta")]
TextDelta { text: String }, TextDelta { text: String },
#[serde(rename = "thinking_delta")]
ThinkingDelta { thinking: String },
#[serde(rename = "signature_delta")]
SignatureDelta { signature: String },
#[serde(rename = "input_json_delta")] #[serde(rename = "input_json_delta")]
InputJsonDelta { partial_json: String }, InputJsonDelta { partial_json: String },
} }

View File

@@ -5,9 +5,9 @@ use std::time::Duration;
use anyhow::Context as _; use anyhow::Context as _;
use futures::channel::{mpsc, oneshot}; use futures::channel::{mpsc, oneshot};
#[cfg(unix)] #[cfg(unix)]
use futures::{AsyncBufReadExt as _, io::BufReader}; use futures::{io::BufReader, AsyncBufReadExt as _};
#[cfg(unix)] #[cfg(unix)]
use futures::{AsyncWriteExt as _, FutureExt as _, select_biased}; use futures::{select_biased, AsyncWriteExt as _, FutureExt as _};
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use gpui::{AsyncApp, BackgroundExecutor, Task}; use gpui::{AsyncApp, BackgroundExecutor, Task};
#[cfg(unix)] #[cfg(unix)]
@@ -188,7 +188,7 @@ impl AskPassSession {
} }
pub async fn run(&mut self) -> AskPassResult { pub async fn run(&mut self) -> AskPassResult {
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))).await; futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))).await;
AskPassResult::Timedout AskPassResult::Timedout
} }
} }

View File

@@ -57,11 +57,10 @@ impl Assets {
pub fn load_test_fonts(&self, cx: &App) { pub fn load_test_fonts(&self, cx: &App) {
cx.text_system() cx.text_system()
.add_fonts(vec![ .add_fonts(vec![self
self.load("fonts/plex-mono/ZedPlexMono-Regular.ttf") .load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
.unwrap() .unwrap()
.unwrap(), .unwrap()])
])
.unwrap() .unwrap()
} }
} }

View File

@@ -48,6 +48,7 @@ lsp.workspace = true
menu.workspace = true menu.workspace = true
multi_buffer.workspace = true multi_buffer.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
paths.workspace = true
project.workspace = true project.workspace = true
prompt_library.workspace = true prompt_library.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
@@ -55,6 +56,7 @@ proto.workspace = true
rope.workspace = true rope.workspace = true
schemars.workspace = true schemars.workspace = true
search.workspace = true search.workspace = true
semantic_index.workspace = true
serde.workspace = true serde.workspace = true
settings.workspace = true settings.workspace = true
smol.workspace = true smol.workspace = true

View File

@@ -10,15 +10,17 @@ use std::sync::Arc;
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use assistant_slash_command::SlashCommandRegistry; use assistant_slash_command::SlashCommandRegistry;
use assistant_slash_commands::{ProjectSlashCommandFeatureFlag, SearchSlashCommandFeatureFlag};
use client::Client; use client::Client;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt; use feature_flags::FeatureFlagAppExt;
use fs::Fs; use fs::Fs;
use gpui::{App, Global, ReadGlobal, UpdateGlobal, actions}; use gpui::{actions, App, Global, UpdateGlobal};
use language_model::{ use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
}; };
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::Deserialize; use serde::Deserialize;
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
@@ -84,10 +86,6 @@ impl Assistant {
filter.show_namespace(Self::NAMESPACE); filter.show_namespace(Self::NAMESPACE);
}); });
} }
pub fn enabled(cx: &App) -> bool {
Self::global(cx).enabled
}
} }
pub fn init( pub fn init(
@@ -100,6 +98,33 @@ pub fn init(
AssistantSettings::register(cx); AssistantSettings::register(cx);
SlashCommandSettings::register(cx); SlashCommandSettings::register(cx);
cx.spawn({
let client = client.clone();
async move |cx| {
let is_search_slash_command_enabled = cx
.update(|cx| cx.wait_for_flag::<SearchSlashCommandFeatureFlag>())?
.await;
let is_project_slash_command_enabled = cx
.update(|cx| cx.wait_for_flag::<ProjectSlashCommandFeatureFlag>())?
.await;
if !is_search_slash_command_enabled && !is_project_slash_command_enabled {
return Ok(());
}
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticDb::new(
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
assistant_context_editor::init(client.clone(), cx); assistant_context_editor::init(client.clone(), cx);
prompt_library::init(cx); prompt_library::init(cx);
init_language_model_settings(cx); init_language_model_settings(cx);
@@ -108,7 +133,7 @@ pub fn init(
assistant_panel::init(cx); assistant_panel::init(cx);
context_server::init(cx); context_server::init(cx);
register_slash_commands(cx); register_slash_commands(Some(prompt_builder.clone()), cx);
inline_assistant::init( inline_assistant::init(
fs.clone(), fs.clone(),
prompt_builder.clone(), prompt_builder.clone(),
@@ -184,7 +209,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
}); });
} }
fn register_slash_commands(cx: &mut App) { fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut App) {
let slash_command_registry = SlashCommandRegistry::global(cx); let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true); slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
@@ -202,6 +227,33 @@ fn register_slash_commands(cx: &mut App) {
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true); .register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true); slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
if let Some(prompt_builder) = prompt_builder {
cx.observe_flag::<assistant_slash_commands::ProjectSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry.register_command(
assistant_slash_commands::ProjectSlashCommand::new(prompt_builder.clone()),
true,
);
}
}
})
.detach();
}
cx.observe_flag::<assistant_slash_commands::AutoSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
// [#auto-staff-ship] TODO remove this when /auto is no longer staff-shipped
slash_command_registry
.register_command(assistant_slash_commands::AutoCommand, true);
}
}
})
.detach();
cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({ cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone(); let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| { move |is_enabled, _cx| {
@@ -218,6 +270,17 @@ fn register_slash_commands(cx: &mut App) {
update_slash_commands_from_settings(cx); update_slash_commands_from_settings(cx);
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings) cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
.detach(); .detach();
cx.observe_flag::<assistant_slash_commands::SearchSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry
.register_command(assistant_slash_commands::SearchSlashCommand, true);
}
}
})
.detach();
} }
fn update_slash_commands_from_settings(cx: &mut App) { fn update_slash_commands_from_settings(cx: &mut App) {

View File

@@ -1,9 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use collections::HashMap; use collections::HashMap;
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription, canvas}; use gpui::{canvas, AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{ElevationIndex, prelude::*}; use ui::{prelude::*, ElevationIndex};
use workspace::Item; use workspace::Item;
pub struct ConfigurationView { pub struct ConfigurationView {

View File

@@ -1,46 +1,44 @@
use crate::Assistant;
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent}; use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
use crate::{ use crate::{
DeployHistory, InlineAssistant, NewChat, terminal_inline_assistant::TerminalInlineAssistant, terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
}; };
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use assistant_context_editor::{ use assistant_context_editor::{
AssistantContext, AssistantPanelDelegate, ContextEditor, ContextEditorToolbarItem, make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor,
ContextEditorToolbarItemEvent, ContextHistory, ContextId, ContextStore, ContextStoreEvent, ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
DEFAULT_TAB_TITLE, InsertDraggedFiles, SlashCommandCompletionProvider, ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
make_lsp_adapter_delegate, DEFAULT_TAB_TITLE,
}; };
use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet; use assistant_slash_command::SlashCommandWorkingSet;
use client::{Client, Status, proto}; use client::{proto, Client, Status};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, prelude::*, Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task, Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled,
UpdateGlobal, WeakEntity, prelude::*, Subscription, Task, UpdateGlobal, WeakEntity,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
AuthenticateError, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID, AuthenticateError, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
}; };
use project::Project; use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library}; use prompt_library::{open_prompt_library, PromptLibrary};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use search::{BufferSearchBar, buffer_search::DivRegistrar}; use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::{Settings, update_settings_file}; use settings::{update_settings_file, Settings};
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*}; use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use util::{ResultExt, maybe}; use util::{maybe, ResultExt};
use workspace::DraggedTab; use workspace::DraggedTab;
use workspace::{ use workspace::{
DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
pane, pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
}; };
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ToggleFocus}; use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
workspace::FollowableViewRegistry::register::<ContextEditor>(cx); workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
@@ -60,7 +58,8 @@ pub fn init(cx: &mut App) {
cx.observe_new( cx.observe_new(
|terminal_panel: &mut TerminalPanel, _, cx: &mut Context<TerminalPanel>| { |terminal_panel: &mut TerminalPanel, _, cx: &mut Context<TerminalPanel>| {
terminal_panel.set_assistant_enabled(Assistant::enabled(cx), cx); let settings = AssistantSettings::get_global(cx);
terminal_panel.set_assistant_enabled(settings.enabled, cx);
}, },
) )
.detach(); .detach();
@@ -260,7 +259,7 @@ impl AssistantPanel {
menu.context(focus_handle.clone()) menu.context(focus_handle.clone())
.action("New Chat", Box::new(NewChat)) .action("New Chat", Box::new(NewChat))
.action("History", Box::new(DeployHistory)) .action("History", Box::new(DeployHistory))
.action("Prompt Library", Box::new(OpenPromptLibrary)) .action("Prompt Library", Box::new(DeployPromptLibrary))
.action("Configure", Box::new(ShowConfiguration)) .action("Configure", Box::new(ShowConfiguration))
.action(zoom_label, Box::new(ToggleZoom)) .action(zoom_label, Box::new(ToggleZoom))
})) }))
@@ -343,12 +342,12 @@ impl AssistantPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) { ) {
if workspace let settings = AssistantSettings::get_global(cx);
.panel::<Self>(cx) if !settings.enabled {
.is_some_and(|panel| panel.read(cx).enabled(cx)) return;
{
workspace.toggle_panel_focus::<Self>(window, cx);
} }
workspace.toggle_panel_focus::<Self>(window, cx);
} }
fn watch_client_status( fn watch_client_status(
@@ -596,10 +595,12 @@ impl AssistantPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) { ) {
let Some(assistant_panel) = workspace let settings = AssistantSettings::get_global(cx);
.panel::<AssistantPanel>(cx) if !settings.enabled {
.filter(|panel| panel.read(cx).enabled(cx)) return;
else { }
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
return; return;
}; };
@@ -1027,7 +1028,7 @@ impl AssistantPanel {
fn deploy_prompt_library( fn deploy_prompt_library(
&mut self, &mut self,
_: &OpenPromptLibrary, _: &DeployPromptLibrary,
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
@@ -1297,8 +1298,12 @@ impl Panel for AssistantPanel {
} }
fn icon(&self, _: &Window, cx: &App) -> Option<IconName> { fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
(self.enabled(cx) && AssistantSettings::get_global(cx).button) let settings = AssistantSettings::get_global(cx);
.then_some(IconName::ZedAssistant) if !settings.enabled || !settings.button {
return None;
}
Some(IconName::ZedAssistant)
} }
fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> { fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> {
@@ -1312,10 +1317,6 @@ impl Panel for AssistantPanel {
fn activation_priority(&self) -> u32 { fn activation_priority(&self) -> u32 {
4 4
} }
fn enabled(&self, cx: &App) -> bool {
Assistant::enabled(cx)
}
} }
impl EventEmitter<PanelEvent> for AssistantPanel {} impl EventEmitter<PanelEvent> for AssistantPanel {}

View File

@@ -1,41 +1,39 @@
use crate::{ use crate::{
Assistant, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist, CyclePreviousInlineAssist,
CyclePreviousInlineAssist,
}; };
use anyhow::{Context as _, Result, anyhow}; use anyhow::{anyhow, Context as _, Result};
use assistant_context_editor::{RequestType, humanize_token_count}; use assistant_context_editor::{humanize_token_count, RequestType};
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use client::{ErrorExt, telemetry::Telemetry}; use client::{telemetry::Telemetry, ErrorExt};
use collections::{HashMap, HashSet, VecDeque, hash_map}; use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{ use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
ToOffset as _, ToPoint,
actions::{MoveDown, MoveUp, SelectAll}, actions::{MoveDown, MoveUp, SelectAll},
display_map::{ display_map::{
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
ToDisplayPoint, ToDisplayPoint,
}, },
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
ToOffset as _, ToPoint,
}; };
use feature_flags::{ use feature_flags::{
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro, Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
}; };
use fs::Fs; use fs::Fs;
use futures::{ use futures::{
SinkExt, Stream, StreamExt,
channel::mpsc, channel::mpsc,
future::{BoxFuture, LocalBoxFuture}, future::{BoxFuture, LocalBoxFuture},
join, join, SinkExt, Stream, StreamExt,
}; };
use gpui::{ use gpui::{
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle, anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
Focusable, FontWeight, Global, HighlightStyle, Subscription, Task, TextStyle, UpdateGlobal, EventEmitter, FocusHandle, Focusable, FontWeight, Global, HighlightStyle, Subscription, Task,
WeakEntity, Window, anchored, deferred, point, TextStyle, UpdateGlobal, WeakEntity, Window,
}; };
use language::{Buffer, IndentKind, Point, Selection, TransactionId, line_diff}; use language::{line_diff, Buffer, IndentKind, Point, Selection, TransactionId};
use language_model::{ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelTextStream, Role, report_assistant_event, LanguageModelRequestMessage, LanguageModelTextStream, Role,
}; };
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
@@ -43,7 +41,7 @@ use parking_lot::Mutex;
use project::{CodeAction, LspAction, ProjectTransaction}; use project::{CodeAction, LspAction, ProjectTransaction};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use rope::Rope; use rope::Rope;
use settings::{Settings, SettingsStore, update_settings_file}; use settings::{update_settings_file, Settings, SettingsStore};
use smol::future::FutureExt; use smol::future::FutureExt;
use std::{ use std::{
cmp, cmp,
@@ -62,10 +60,10 @@ use terminal_view::terminal_panel::TerminalPanel;
use text::{OffsetRangeExt, ToPoint as _}; use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip, prelude::*, text_for_action, prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip,
}; };
use util::{RangeExt, ResultExt}; use util::{RangeExt, ResultExt};
use workspace::{ItemHandle, Toast, Workspace, notifications::NotificationId}; use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
pub fn init( pub fn init(
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@@ -234,7 +232,7 @@ impl InlineAssistant {
) { ) {
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| { let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
( (
editor.snapshot(window, cx), editor.buffer().read(cx).snapshot(cx),
editor.selections.all::<Point>(cx), editor.selections.all::<Point>(cx),
) )
}); });
@@ -248,37 +246,7 @@ impl InlineAssistant {
if selection.end.column == 0 { if selection.end.column == 0 {
selection.end.row -= 1; selection.end.row -= 1;
} }
selection.end.column = snapshot selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
if c == '\n' {
break;
}
if c.is_whitespace() {
continue;
}
if snapshot
.language_at(selection.end)
.is_some_and(|language| language.config().brackets.is_closing_brace(c))
{
selection.end.row += 1;
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
}
}
}
} }
if let Some(prev_selection) = selections.last_mut() { if let Some(prev_selection) = selections.last_mut() {
@@ -294,7 +262,6 @@ impl InlineAssistant {
} }
selections.push(selection); selections.push(selection);
} }
let snapshot = &snapshot.buffer_snapshot;
let newest_selection = newest_selection.unwrap(); let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new(); let mut codegen_ranges = Vec::new();
@@ -3557,7 +3524,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
_: &mut Window, _: &mut Window,
cx: &mut App, cx: &mut App,
) -> Task<Result<Vec<CodeAction>>> { ) -> Task<Result<Vec<CodeAction>>> {
if !Assistant::enabled(cx) { if !AssistantSettings::get_global(cx).enabled {
return Task::ready(Ok(Vec::new())); return Task::ready(Ok(Vec::new()));
} }
@@ -3711,10 +3678,10 @@ mod tests {
use gpui::TestAppContext; use gpui::TestAppContext;
use indoc::indoc; use indoc::indoc;
use language::{ use language::{
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings, language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
tree_sitter_rust, Point,
}; };
use language_model::{LanguageModelRegistry, TokenUsage}; use language_model::LanguageModelRegistry;
use rand::prelude::*; use rand::prelude::*;
use serde::Serialize; use serde::Serialize;
use settings::SettingsStore; use settings::SettingsStore;
@@ -4093,7 +4060,6 @@ mod tests {
future::ready(Ok(LanguageModelTextStream { future::ready(Ok(LanguageModelTextStream {
message_id: None, message_id: None,
stream: chunks_rx.map(Ok).boxed(), stream: chunks_rx.map(Ok).boxed(),
last_token_usage: Arc::new(Mutex::new(TokenUsage::default())),
})), })),
cx, cx,
); );

View File

@@ -1,27 +1,27 @@
use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES}; use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use assistant_context_editor::{RequestType, humanize_token_count}; use assistant_context_editor::{humanize_token_count, RequestType};
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque}; use collections::{HashMap, VecDeque};
use editor::{ use editor::{
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp, SelectAll}, actions::{MoveDown, MoveUp, SelectAll},
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
}; };
use fs::Fs; use fs::Fs;
use futures::{SinkExt, StreamExt, channel::mpsc}; use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{ use gpui::{
App, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, Task, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, Task,
TextStyle, UpdateGlobal, WeakEntity, TextStyle, UpdateGlobal, WeakEntity,
}; };
use language::Buffer; use language::Buffer;
use language_model::{ use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
report_assistant_event, LanguageModelRequestMessage, Role,
}; };
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use settings::{Settings, update_settings_file}; use settings::{update_settings_file, Settings};
use std::{ use std::{
cmp, cmp,
sync::Arc, sync::Arc,
@@ -31,9 +31,9 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal; use terminal::Terminal;
use terminal_view::TerminalView; use terminal_view::TerminalView;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{IconButtonShape, Tooltip, prelude::*, text_for_action}; use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
use util::ResultExt; use util::ResultExt;
use workspace::{Toast, Workspace, notifications::NotificationId}; use workspace::{notifications::NotificationId, Toast, Workspace};
pub fn init( pub fn init(
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,

View File

@@ -25,14 +25,12 @@ assistant_settings.workspace = true
assistant_slash_command.workspace = true assistant_slash_command.workspace = true
assistant_tool.workspace = true assistant_tool.workspace = true
async-watch.workspace = true async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true chrono.workspace = true
client.workspace = true client.workspace = true
clock.workspace = true clock.workspace = true
collections.workspace = true collections.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
context_server.workspace = true context_server.workspace = true
convert_case.workspace = true
db.workspace = true db.workspace = true
editor.workspace = true editor.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
@@ -41,11 +39,11 @@ fs.workspace = true
futures.workspace = true futures.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
git.workspace = true git.workspace = true
git_ui.workspace = true
gpui.workspace = true gpui.workspace = true
heed.workspace = true heed.workspace = true
html_to_markdown.workspace = true html_to_markdown.workspace = true
http_client.workspace = true http_client.workspace = true
indexmap.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
@@ -55,7 +53,6 @@ lsp.workspace = true
markdown.workspace = true markdown.workspace = true
menu.workspace = true menu.workspace = true
multi_buffer.workspace = true multi_buffer.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
paths.workspace = true paths.workspace = true
picker.workspace = true picker.workspace = true
@@ -63,13 +60,11 @@ project.workspace = true
prompt_library.workspace = true prompt_library.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
proto.workspace = true proto.workspace = true
release_channel.workspace = true
rope.workspace = true rope.workspace = true
schemars.workspace = true scripting_tool.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true settings.workspace = true
smallvec.workspace = true
smol.workspace = true smol.workspace = true
streaming_diff.workspace = true streaming_diff.workspace = true
telemetry.workspace = true telemetry.workspace = true
@@ -88,7 +83,6 @@ workspace.workspace = true
zed_actions.workspace = true zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true indoc.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::SharedString;
/// A profile for the Zed Agent that controls its behavior.
#[derive(Debug, Clone)]
pub struct AgentProfile {
/// The name of the profile.
pub name: SharedString,
pub tools: HashMap<Arc<str>, bool>,
#[allow(dead_code)]
pub context_servers: HashMap<Arc<str>, ContextServerPreset>,
}
#[derive(Debug, Clone)]
pub struct ContextServerPreset {
#[allow(dead_code)]
pub tools: HashMap<Arc<str>, bool>,
}
impl AgentProfile {
pub fn read_only() -> Self {
Self {
name: "Read-only".into(),
tools: HashMap::from_iter([
("diagnostics".into(), true),
("fetch".into(), true),
("list-directory".into(), true),
("now".into(), true),
("path-search".into(), true),
("read-file".into(), true),
("regex-search".into(), true),
("thinking".into(), true),
]),
context_servers: HashMap::default(),
}
}
pub fn code_writer() -> Self {
Self {
name: "Code Writer".into(),
tools: HashMap::from_iter([
("bash".into(), true),
("delete-path".into(), true),
("diagnostics".into(), true),
("edit-files".into(), true),
("fetch".into(), true),
("list-directory".into(), true),
("now".into(), true),
("path-search".into(), true),
("read-file".into(), true),
("regex-search".into(), true),
("thinking".into(), true),
]),
context_servers: HashMap::default(),
}
}
}

View File

@@ -1,6 +1,6 @@
mod active_thread; mod active_thread;
mod agent_profile;
mod assistant_configuration; mod assistant_configuration;
mod assistant_diff;
mod assistant_model_selector; mod assistant_model_selector;
mod assistant_panel; mod assistant_panel;
mod buffer_codegen; mod buffer_codegen;
@@ -12,12 +12,12 @@ mod history_store;
mod inline_assistant; mod inline_assistant;
mod inline_prompt_editor; mod inline_prompt_editor;
mod message_editor; mod message_editor;
mod profile_selector;
mod terminal_codegen; mod terminal_codegen;
mod terminal_inline_assistant; mod terminal_inline_assistant;
mod thread; mod thread;
mod thread_history; mod thread_history;
mod thread_store; mod thread_store;
mod tool_selector;
mod tool_use; mod tool_use;
mod ui; mod ui;
@@ -28,19 +28,15 @@ use client::Client;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs; use fs::Fs;
use gpui::{App, actions, impl_actions}; use gpui::{actions, App};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::Settings as _; use settings::Settings as _;
pub use crate::active_thread::ActiveThread; pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}; pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant; pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent}; pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
pub use crate::thread_store::ThreadStore; pub use crate::thread_store::ThreadStore;
pub use assistant_diff::{AssistantDiff, AssistantDiffToolbar};
actions!( actions!(
assistant2, assistant2,
@@ -48,11 +44,9 @@ actions!(
NewThread, NewThread,
NewPromptEditor, NewPromptEditor,
ToggleContextPicker, ToggleContextPicker,
ToggleProfileSelector,
RemoveAllContext, RemoveAllContext,
OpenHistory, OpenHistory,
OpenConfiguration, OpenConfiguration,
AddContextServer,
RemoveSelectedThread, RemoveSelectedThread,
Chat, Chat,
ChatMode, ChatMode,
@@ -64,31 +58,10 @@ actions!(
FocusRight, FocusRight,
RemoveFocusedContext, RemoveFocusedContext,
AcceptSuggestedContext, AcceptSuggestedContext,
OpenActiveThreadAsMarkdown, OpenActiveThreadAsMarkdown
OpenAssistantDiff,
ToggleKeep,
Reject,
RejectAll,
KeepAll
] ]
); );
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<Arc<str>>,
}
impl ManageProfiles {
pub fn customize_tools(profile_id: Arc<str>) -> Self {
Self {
customize_tools: Some(profile_id),
}
}
}
impl_actions!(assistant, [ManageProfiles]);
const NAMESPACE: &str = "assistant2"; const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate. /// Initializes the `assistant2` crate.
@@ -114,8 +87,6 @@ pub fn init(
client.telemetry().clone(), client.telemetry().clone(),
cx, cx,
); );
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
feature_gate_assistant2_actions(cx); feature_gate_assistant2_actions(cx);
} }

View File

@@ -1,7 +1,3 @@
mod add_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
use std::sync::Arc; use std::sync::Arc;
use assistant_tool::{ToolSource, ToolWorkingSet}; use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -9,15 +5,13 @@ use collections::HashMap;
use context_server::manager::ContextServerManager; use context_server::manager::ContextServerManager;
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription}; use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, prelude::*}; use ui::{
prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip,
};
use util::ResultExt as _; use util::ResultExt as _;
use zed_actions::assistant::DeployPromptLibrary;
use zed_actions::ExtensionCategoryFilter; use zed_actions::ExtensionCategoryFilter;
pub(crate) use add_context_server_modal::AddContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;
pub struct AssistantConfiguration { pub struct AssistantConfiguration {
focus_handle: FocusHandle, focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>, configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
@@ -105,7 +99,7 @@ impl AssistantConfiguration {
&mut self, &mut self,
provider: &Arc<dyn LanguageModelProvider>, provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement + use<> { ) -> impl IntoElement {
let provider_id = provider.id().0.clone(); let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone(); let provider_name = provider.name().0.clone();
let configuration_view = self let configuration_view = self
@@ -176,6 +170,7 @@ impl AssistantConfiguration {
v_flex() v_flex()
.p(DynamicSpacing::Base16.rems(cx)) .p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_2() .gap_2()
.flex_1() .flex_1()
.child( .child(
@@ -200,7 +195,6 @@ impl AssistantConfiguration {
let tool_count = tools.len(); let tool_count = tools.len();
v_flex() v_flex()
.id(SharedString::from(context_server.id()))
.border_1() .border_1()
.rounded_sm() .rounded_sm()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
@@ -314,9 +308,8 @@ impl AssistantConfiguration {
.icon(IconName::Plus) .icon(IconName::Plus)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.on_click(|_event, window, cx| { .disabled(true)
window.dispatch_action(AddContextServer.boxed_clone(), cx) .tooltip(Tooltip::text("Not yet implemented")),
}),
), ),
) )
.child( .child(
@@ -358,6 +351,33 @@ impl Render for AssistantConfiguration {
.bg(cx.theme().colors().panel_background) .bg(cx.theme().colors().panel_background)
.size_full() .size_full()
.overflow_y_scroll() .overflow_y_scroll()
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.gap_2()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Prompt Library").size(HeadlineSize::Small))
.child(
Label::new("Create reusable prompts and tag which ones you want sent in every LLM interaction.")
.color(Color::Muted),
),
)
.child(
Button::new("open-prompt-library", "Open Prompt Library")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()
.icon(IconName::Book)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(|_event, window, cx| {
window.dispatch_action(DeployPromptLibrary.boxed_clone(), cx)
}),
),
)
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(cx)) .child(self.render_context_servers_section(cx))
.child(Divider::horizontal().color(DividerColor::Border)) .child(Divider::horizontal().color(DividerColor::Border))
.child( .child(

View File

@@ -1,164 +0,0 @@
use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
use editor::Editor;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
use serde_json::json;
use settings::update_settings_file;
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use workspace::{ModalView, Workspace};
use crate::AddContextServer;
pub struct AddContextServerModal {
workspace: WeakEntity<Workspace>,
name_editor: Entity<Editor>,
command_editor: Entity<Editor>,
}
impl AddContextServerModal {
pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
let workspace_handle = cx.entity().downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
Self::new(workspace_handle, window, cx)
})
});
}
pub fn new(
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
let command_editor = cx.new(|cx| Editor::single_line(window, cx));
name_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Context server name", cx);
});
command_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Command to run the context server", cx);
});
Self {
name_editor,
command_editor,
workspace,
}
}
fn confirm(&mut self, cx: &mut Context<Self>) {
let name = self.name_editor.read(cx).text(cx).trim().to_string();
let command = self.command_editor.read(cx).text(cx).trim().to_string();
if name.is_empty() || command.is_empty() {
return;
}
let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
let Some(path) = command_parts.next() else {
return;
};
let args = command_parts.collect::<Vec<_>>();
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
update_settings_file::<ContextServerSettings>(fs.clone(), cx, |settings, _| {
settings.context_servers.insert(
name.into(),
ServerConfig {
command: Some(ServerCommand {
path,
args,
env: None,
}),
settings: Some(json!({})),
},
);
});
});
}
cx.emit(DismissEvent);
}
fn cancel(&mut self, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl ModalView for AddContextServerModal {}
impl Focusable for AddContextServerModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.name_editor.focus_handle(cx).clone()
}
}
impl EventEmitter<DismissEvent> for AddContextServerModal {}
impl Render for AddContextServerModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_name_empty = self.name_editor.read(cx).text(cx).trim().is_empty();
let is_command_empty = self.command_editor.read(cx).text(cx).trim().is_empty();
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("AddContextServerModal")
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(
Modal::new("add-context-server", None)
.header(ModalHeader::new().headline("Add Context Server"))
.section(
Section::new()
.child(
v_flex()
.gap_1()
.child(Label::new("Name"))
.child(self.name_editor.clone()),
)
.child(
v_flex()
.gap_1()
.child(Label::new("Command"))
.child(self.command_editor.clone()),
),
)
.footer(
ModalFooter::new()
.start_slot(
Button::new("cancel", "Cancel").on_click(
cx.listener(|this, _event, _window, cx| this.cancel(cx)),
),
)
.end_slot(
Button::new("add-server", "Add Server")
.disabled(is_name_empty || is_command_empty)
.map(|button| {
if is_name_empty {
button.tooltip(Tooltip::text("Name is required"))
} else if is_command_empty {
button.tooltip(Tooltip::text("Command is required"))
} else {
button
}
})
.on_click(
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
),
),
),
)
}
}

View File

@@ -1,561 +0,0 @@
mod profile_modal_header;
use std::sync::Arc;
use assistant_settings::{AgentProfile, AssistantSettings};
use assistant_tool::ToolWorkingSet;
use convert_case::{Case, Casing as _};
use editor::Editor;
use fs::Fs;
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity,
prelude::*,
};
use settings::{Settings as _, update_settings_file};
use ui::{
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
};
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles, ThreadStore};
enum Mode {
ChooseProfile(ChooseProfileMode),
NewProfile(NewProfileMode),
ViewProfile(ViewProfileMode),
ConfigureTools {
profile_id: Arc<str>,
tool_picker: Entity<ToolPicker>,
_subscription: Subscription,
},
}
impl Mode {
pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
let settings = AssistantSettings::get_global(cx);
let mut profiles = settings.profiles.clone();
profiles.sort_unstable_by(|_, a, _, b| a.name.cmp(&b.name));
let profiles = profiles
.into_iter()
.map(|(id, profile)| ProfileEntry {
id,
name: profile.name,
navigation: NavigableEntry::focusable(cx),
})
.collect::<Vec<_>>();
Self::ChooseProfile(ChooseProfileMode {
profiles,
add_new_profile: NavigableEntry::focusable(cx),
})
}
}
#[derive(Clone)]
struct ProfileEntry {
pub id: Arc<str>,
pub name: SharedString,
pub navigation: NavigableEntry,
}
#[derive(Clone)]
pub struct ChooseProfileMode {
profiles: Vec<ProfileEntry>,
add_new_profile: NavigableEntry,
}
#[derive(Clone)]
pub struct ViewProfileMode {
profile_id: Arc<str>,
fork_profile: NavigableEntry,
configure_tools: NavigableEntry,
}
#[derive(Clone)]
pub struct NewProfileMode {
name_editor: Entity<Editor>,
base_profile_id: Option<Arc<str>>,
}
pub struct ManageProfilesModal {
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
mode: Mode,
}
impl ManageProfilesModal {
pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store();
let tools = thread_store.read(cx).tools();
let thread_store = thread_store.downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
let mut this = Self::new(fs, tools, thread_store, window, cx);
if let Some(profile_id) = action.customize_tools.clone() {
this.configure_tools(profile_id, window, cx);
}
this
})
}
});
}
pub fn new(
fs: Arc<dyn Fs>,
tools: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
Self {
fs,
tools,
thread_store,
focus_handle,
mode: Mode::choose_profile(window, cx),
}
}
fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.mode = Mode::choose_profile(window, cx);
self.focus_handle(cx).focus(window);
}
fn new_profile(
&mut self,
base_profile_id: Option<Arc<str>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
name_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Profile name", cx);
});
self.mode = Mode::NewProfile(NewProfileMode {
name_editor,
base_profile_id,
});
self.focus_handle(cx).focus(window);
}
pub fn view_profile(
&mut self,
profile_id: Arc<str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id,
fork_profile: NavigableEntry::focusable(cx),
configure_tools: NavigableEntry::focusable(cx),
});
self.focus_handle(cx).focus(window);
}
fn configure_tools(
&mut self,
profile_id: Arc<str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let settings = AssistantSettings::get_global(cx);
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
return;
};
let tool_picker = cx.new(|cx| {
let delegate = ToolPickerDelegate::new(
self.fs.clone(),
self.tools.clone(),
self.thread_store.clone(),
profile_id.clone(),
profile,
cx,
);
ToolPicker::new(delegate, window, cx)
});
let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
let profile_id = profile_id.clone();
move |this, _tool_picker, _: &DismissEvent, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
}
});
self.mode = Mode::ConfigureTools {
profile_id,
tool_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window);
}
fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
Mode::ChooseProfile { .. } => {}
Mode::NewProfile(mode) => {
let settings = AssistantSettings::get_global(cx);
let base_profile = mode
.base_profile_id
.as_ref()
.and_then(|profile_id| settings.profiles.get(profile_id).cloned());
let name = mode.name_editor.read(cx).text(cx);
let profile_id: Arc<str> = name.to_case(Case::Kebab).into();
let profile = AgentProfile {
name: name.into(),
tools: base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
};
self.create_profile(profile_id.clone(), profile, cx);
self.view_profile(profile_id, window, cx);
}
Mode::ViewProfile(_) => {}
Mode::ConfigureTools { .. } => {}
}
}
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
Mode::ChooseProfile { .. } => {
cx.emit(DismissEvent);
}
Mode::NewProfile(mode) => {
if let Some(profile_id) = mode.base_profile_id.clone() {
self.view_profile(profile_id, window, cx);
} else {
self.choose_profile(window, cx);
}
}
Mode::ViewProfile(_) => self.choose_profile(window, cx),
Mode::ConfigureTools { .. } => {}
}
}
fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
move |settings, _cx| {
settings.create_profile(profile_id, profile).log_err();
}
});
}
}
impl ModalView for ManageProfilesModal {}
impl Focusable for ManageProfilesModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
Mode::ChooseProfile(_) => self.focus_handle.clone(),
Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
Mode::ViewProfile(_) => self.focus_handle.clone(),
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
}
}
}
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
impl ManageProfilesModal {
fn render_choose_profile(
&mut self,
mode: ChooseProfileMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(ProfileModalHeader::new(
"Agent Profiles",
IconName::ZedAssistant,
))
.child(
v_flex()
.pb_1()
.child(ListSeparator)
.children(mode.profiles.iter().map(|profile| {
div()
.id(SharedString::from(format!("profile-{}", profile.id)))
.track_focus(&profile.navigation.focus_handle)
.on_action({
let profile_id = profile.id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new(SharedString::from(format!(
"profile-{}",
profile.id
)))
.toggle_state(
profile
.navigation
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.child(Label::new(profile.name.clone()))
.end_slot(
h_flex()
.gap_1()
.child(Label::new("Customize").size(LabelSize::Small))
.children(KeyBinding::for_action_in(
&menu::Confirm,
&self.focus_handle,
window,
cx,
)),
)
.on_click({
let profile_id = profile.id.clone();
cx.listener(move |this, _, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
})
}),
)
}))
.child(ListSeparator)
.child(
div()
.id("new-profile")
.track_focus(&mode.add_new_profile.focus_handle)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
this.new_profile(None, window, cx);
}))
.child(
ListItem::new("new-profile")
.toggle_state(
mode.add_new_profile
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Plus))
.child(Label::new("Add New Profile"))
.on_click({
cx.listener(move |this, _, window, cx| {
this.new_profile(None, window, cx);
})
}),
),
),
)
.into_any_element(),
)
.map(|mut navigable| {
for profile in mode.profiles {
navigable = navigable.entry(profile.navigation);
}
navigable
})
.entry(mode.add_new_profile)
}
fn render_new_profile(
&mut self,
mode: NewProfileMode,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
settings
.profiles
.get(base_profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into())
});
v_flex()
.id("new-profile")
.track_focus(&self.focus_handle(cx))
.child(ProfileModalHeader::new(
match base_profile_name {
Some(base_profile) => format!("Fork {base_profile}"),
None => "New Profile".into(),
},
IconName::Plus,
))
.child(ListSeparator)
.child(h_flex().p_2().child(mode.name_editor.clone()))
}
fn render_view_profile(
&mut self,
mode: ViewProfileMode,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let profile_name = settings
.profiles
.get(&mode.profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(ProfileModalHeader::new(
profile_name,
IconName::ZedAssistant,
))
.child(
v_flex()
.pb_1()
.child(ListSeparator)
.child(
div()
.id("fork-profile")
.track_focus(&mode.fork_profile.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.new_profile(Some(profile_id.clone()), window, cx);
})
})
.child(
ListItem::new("fork-profile")
.toggle_state(
mode.fork_profile
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::GitBranch))
.child(Label::new("Fork Profile"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.new_profile(
Some(profile_id.clone()),
window,
cx,
);
})
}),
),
)
.child(
div()
.id("configure-tools")
.track_focus(&mode.configure_tools.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new("configure-tools")
.toggle_state(
mode.configure_tools
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Cog))
.child(Label::new("Configure Tools"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_tools(
profile_id.clone(),
window,
cx,
);
})
}),
),
),
)
.into_any_element(),
)
.entry(mode.fork_profile)
.entry(mode.configure_tools)
}
}
impl Render for ManageProfilesModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("ManageProfilesModal")
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode {
Mode::ChooseProfile(mode) => self
.render_choose_profile(mode.clone(), window, cx)
.into_any_element(),
Mode::NewProfile(mode) => self
.render_new_profile(mode.clone(), window, cx)
.into_any_element(),
Mode::ViewProfile(mode) => self
.render_view_profile(mode.clone(), window, cx)
.into_any_element(),
Mode::ConfigureTools {
profile_id,
tool_picker,
..
} => {
let profile_name = settings
.profiles
.get(profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
div()
.child(ProfileModalHeader::new(
format!("{profile_name}: Configure Tools"),
IconName::Cog,
))
.child(ListSeparator)
.child(tool_picker.clone())
.into_any_element()
}
})
}
}

View File

@@ -1,38 +0,0 @@
use ui::prelude::*;
#[derive(IntoElement)]
pub struct ProfileModalHeader {
label: SharedString,
icon: IconName,
}
impl ProfileModalHeader {
pub fn new(label: impl Into<SharedString>, icon: IconName) -> Self {
Self {
label: label.into(),
icon,
}
}
}
impl RenderOnce for ProfileModalHeader {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.w_full()
.px(DynamicSpacing::Base12.rems(cx))
.pt(DynamicSpacing::Base08.rems(cx))
.pb(DynamicSpacing::Base04.rems(cx))
.rounded_t_sm()
.gap_1p5()
.child(Icon::new(self.icon).size(IconSize::XSmall))
.child(
h_flex().gap_1().overflow_x_hidden().child(
div()
.max_w_96()
.overflow_x_hidden()
.text_ellipsis()
.child(Headline::new(self.label).size(HeadlineSize::XSmall)),
),
)
}
}

View File

@@ -1,299 +0,0 @@
use std::sync::Arc;
use assistant_settings::{
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
ContextServerPresetContent, VersionedAssistantSettingsContent,
};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::{Settings as _, update_settings_file};
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
use util::ResultExt as _;
use crate::ThreadStore;
pub struct ToolPicker {
picker: Entity<Picker<ToolPickerDelegate>>,
}
impl ToolPicker {
pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
Self { picker }
}
}
impl EventEmitter<DismissEvent> for ToolPicker {}
impl Focusable for ToolPicker {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ToolPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
#[derive(Debug, Clone)]
pub struct ToolEntry {
pub name: Arc<str>,
pub source: ToolSource,
}
pub struct ToolPickerDelegate {
tool_picker: WeakEntity<ToolPicker>,
thread_store: WeakEntity<ThreadStore>,
fs: Arc<dyn Fs>,
tools: Vec<ToolEntry>,
profile_id: Arc<str>,
profile: AgentProfile,
matches: Vec<StringMatch>,
selected_index: usize,
}
impl ToolPickerDelegate {
pub fn new(
fs: Arc<dyn Fs>,
tool_set: Arc<ToolWorkingSet>,
thread_store: WeakEntity<ThreadStore>,
profile_id: Arc<str>,
profile: AgentProfile,
cx: &mut Context<ToolPicker>,
) -> Self {
let mut tool_entries = Vec::new();
for (source, tools) in tool_set.tools_by_source(cx) {
tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
name: tool.name().into(),
source: source.clone(),
}));
}
Self {
tool_picker: cx.entity().downgrade(),
thread_store,
fs,
tools: tool_entries,
profile_id,
profile,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for ToolPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search tools…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let background = cx.background_executor().clone();
let candidates = self
.tools
.iter()
.enumerate()
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
.collect::<Vec<_>>();
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
};
this.update(cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(window, cx);
return;
}
let candidate_id = self.matches[self.selected_index].candidate_id;
let tool = &self.tools[candidate_id];
let is_enabled = match &tool.source {
ToolSource::Native => {
let is_enabled = self.profile.tools.entry(tool.name.clone()).or_default();
*is_enabled = !*is_enabled;
*is_enabled
}
ToolSource::ContextServer { id } => {
let preset = self
.profile
.context_servers
.entry(id.clone().into())
.or_default();
let is_enabled = preset.tools.entry(tool.name.clone()).or_default();
*is_enabled = !*is_enabled;
*is_enabled
}
};
let active_profile_id = &AssistantSettings::get_global(cx).default_profile;
if active_profile_id == &self.profile_id {
self.thread_store
.update(cx, |this, _cx| {
this.load_profile(&self.profile);
})
.log_err();
}
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
let profile_id = self.profile_id.clone();
let default_profile = self.profile.clone();
let tool = tool.clone();
move |settings, _cx| match settings {
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
settings,
)) => {
let profiles = settings.profiles.get_or_insert_default();
let profile =
profiles
.entry(profile_id)
.or_insert_with(|| AgentProfileContent {
name: default_profile.name.into(),
tools: default_profile.tools,
context_servers: default_profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
});
match tool.source {
ToolSource::Native => {
*profile.tools.entry(tool.name).or_default() = is_enabled;
}
ToolSource::ContextServer { id } => {
let preset = profile
.context_servers
.entry(id.clone().into())
.or_default();
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
}
}
}
_ => {}
}
});
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.tool_picker
.update(cx, |_this, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let tool_match = &self.matches[ix];
let tool = &self.tools[tool_match.candidate_id];
let is_enabled = match &tool.source {
ToolSource::Native => self.profile.tools.get(&tool.name).copied().unwrap_or(false),
ToolSource::ContextServer { id } => self
.profile
.context_servers
.get(id.as_ref())
.and_then(|preset| preset.tools.get(&tool.name))
.copied()
.unwrap_or(false),
};
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
h_flex()
.gap_2()
.child(HighlightedLabel::new(
tool_match.string.clone(),
tool_match.positions.clone(),
))
.map(|parent| match &tool.source {
ToolSource::Native => parent,
ToolSource::ContextServer { id } => parent
.child(Label::new(id).size(LabelSize::XSmall).color(Color::Muted)),
}),
)
.end_slot::<Icon>(is_enabled.then(|| {
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success)
})),
)
}
}

View File

@@ -1,772 +0,0 @@
use crate::{Thread, ThreadEvent, ToggleKeep};
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::HashSet;
use editor::{
Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
};
use gpui::{
Action, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Capability, DiskState, OffsetRangeExt, Point};
use multi_buffer::PathKey;
use project::{Project, ProjectPath};
use std::{
any::{Any, TypeId},
ops::Range,
sync::Arc,
};
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*};
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
item::{BreadcrumbText, ItemEvent, TabContentParams},
searchable::SearchableItemHandle,
};
pub struct AssistantDiff {
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
thread: Entity<Thread>,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
title: SharedString,
_subscriptions: Vec<Subscription>,
}
impl AssistantDiff {
pub fn deploy(
thread: Entity<Thread>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Result<()> {
let existing_diff = workspace.update(cx, |workspace, cx| {
workspace
.items_of_type::<AssistantDiff>(cx)
.find(|diff| diff.read(cx).thread == thread)
})?;
if let Some(existing_diff) = existing_diff {
workspace.update(cx, |workspace, cx| {
workspace.activate_item(&existing_diff, true, true, window, cx);
})
} else {
let assistant_diff =
cx.new(|cx| AssistantDiff::new(thread.clone(), workspace.clone(), window, cx));
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_center(Box::new(assistant_diff), window, cx);
})
}
}
pub fn new(
thread: Entity<Thread>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let project = thread.read(cx).project().clone();
let render_diff_hunk_controls = Arc::new({
let assistant_diff = cx.entity();
move |row,
status: &DiffHunkStatus,
hunk_range,
is_created_file,
line_height,
_editor: &Entity<Editor>,
window: &mut Window,
cx: &mut App| {
render_diff_hunk_controls(
row,
status,
hunk_range,
is_created_file,
line_height,
&assistant_diff,
window,
cx,
)
}
});
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
editor.disable_inline_diagnostics();
editor.set_expand_all_diff_hunks(cx);
editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
editor.register_addon(AssistantDiffAddon);
editor
});
let action_log = thread.read(cx).action_log().clone();
let mut this = Self {
_subscriptions: vec![
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
this.update_excerpts(window, cx)
}),
cx.subscribe(&thread, |this, _thread, event, cx| {
this.handle_thread_event(event, cx)
}),
],
title: SharedString::default(),
multibuffer,
editor,
thread,
focus_handle,
workspace,
};
this.update_excerpts(window, cx);
this.update_title(cx);
this
}
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let thread = self.thread.read(cx);
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
for (buffer, changed) in changed_buffers {
let Some(file) = buffer.read(cx).file().cloned() else {
continue;
};
let path_key = PathKey::namespaced("", file.full_path(cx).into());
paths_to_delete.remove(&path_key);
let snapshot = buffer.read(cx).snapshot();
let diff = changed.diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(
language::Anchor::MIN..language::Anchor::MAX,
&snapshot,
cx,
)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.collect::<Vec<_>>();
let (was_empty, is_excerpt_newly_added) =
self.multibuffer.update(cx, |multibuffer, cx| {
let was_empty = multibuffer.is_empty();
let is_excerpt_newly_added = multibuffer.set_excerpts_for_path(
path_key.clone(),
buffer.clone(),
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
multibuffer.add_diff(changed.diff.clone(), cx);
(was_empty, is_excerpt_newly_added)
});
self.editor.update(cx, |editor, cx| {
if was_empty {
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges([0..0])
});
}
if is_excerpt_newly_added
&& buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state() == DiskState::Deleted)
{
editor.fold_buffer(snapshot.text.remote_id(), cx)
}
});
}
self.multibuffer.update(cx, |multibuffer, cx| {
for path in paths_to_delete {
multibuffer.remove_excerpts_for_path(path, cx);
}
});
if self.multibuffer.read(cx).is_empty()
&& self
.editor
.read(cx)
.focus_handle(cx)
.contains_focused(window, cx)
{
self.focus_handle.focus(window);
} else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
self.editor.update(cx, |editor, cx| {
editor.focus_handle(cx).focus(window);
});
}
}
fn update_title(&mut self, cx: &mut Context<Self>) {
let new_title = self
.thread
.read(cx)
.summary()
.unwrap_or("Assistant Changes".into());
if new_title != self.title {
self.title = new_title;
cx.emit(EditorEvent::TitleChanged);
}
}
fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
match event {
ThreadEvent::SummaryChanged => self.update_title(cx),
_ => {}
}
}
fn toggle_keep(&mut self, _: &crate::ToggleKeep, _window: &mut Window, cx: &mut Context<Self>) {
let ranges = self
.editor
.read(cx)
.selections
.disjoint_anchor_ranges()
.collect::<Vec<_>>();
let snapshot = self.multibuffer.read(cx).snapshot(cx);
let diff_hunks_in_ranges = self
.editor
.read(cx)
.diff_hunks_in_ranges(&ranges, &snapshot)
.collect::<Vec<_>>();
for hunk in diff_hunks_in_ranges {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
self.thread.update(cx, |thread, cx| {
let accept = hunk.status().has_secondary_hunk();
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
});
}
}
}
fn reject(&mut self, _: &crate::Reject, window: &mut Window, cx: &mut Context<Self>) {
let ranges = self
.editor
.update(cx, |editor, cx| editor.selections.ranges(cx));
self.editor.update(cx, |editor, cx| {
editor.restore_hunks_in_ranges(ranges, window, cx)
})
}
fn reject_all(&mut self, _: &crate::RejectAll, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
let max_point = editor.buffer().read(cx).read(cx).max_point();
editor.restore_hunks_in_ranges(vec![Point::zero()..max_point], window, cx)
})
}
fn keep_all(&mut self, _: &crate::KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
self.thread
.update(cx, |thread, cx| thread.keep_all_edits(cx));
}
fn review_diff_hunks(
&mut self,
hunk_ranges: Vec<Range<editor::Anchor>>,
accept: bool,
cx: &mut Context<Self>,
) {
let snapshot = self.multibuffer.read(cx).snapshot(cx);
let diff_hunks_in_ranges = self
.editor
.read(cx)
.diff_hunks_in_ranges(&hunk_ranges, &snapshot)
.collect::<Vec<_>>();
for hunk in diff_hunks_in_ranges {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
self.thread.update(cx, |thread, cx| {
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
});
}
}
}
}
impl EventEmitter<EditorEvent> for AssistantDiff {}
impl Focusable for AssistantDiff {
fn focus_handle(&self, cx: &App) -> FocusHandle {
if self.multibuffer.read(cx).is_empty() {
self.focus_handle.clone()
} else {
self.editor.focus_handle(cx)
}
}
}
impl Item for AssistantDiff {
type Event = EditorEvent;
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(Icon::new(IconName::ZedAssistant).color(Color::Muted))
}
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn navigate(
&mut self,
data: Box<dyn Any>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some("Assistant Diff".into())
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let summary = self
.thread
.read(cx)
.summary()
.unwrap_or("Assistant Changes".into());
Label::new(format!("Review: {}", summary))
.color(if params.selected {
Color::Default
} else {
Color::Muted
})
.into_any_element()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Assistant Diff Opened")
}
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
fn for_each_project_item(
&self,
cx: &App,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
) {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &App) -> bool {
false
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>>
where
Self: Sized,
{
Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
}
fn is_dirty(&self, cx: &App) -> bool {
self.multibuffer.read(cx).is_dirty(cx)
}
fn has_conflict(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_conflict(cx)
}
fn can_save(&self, _: &App) -> bool {
true
}
fn save(
&mut self,
format: bool,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(format, project, window, cx)
}
fn save_as(
&mut self,
_: Entity<Project>,
_: ProjectPath,
_window: &mut Window,
_: &mut Context<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn reload(
&mut self,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.reload(project, window, cx)
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
} else {
None
}
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx)
});
}
}
impl Render for AssistantDiff {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_empty = self.multibuffer.read(cx).is_empty();
div()
.track_focus(&self.focus_handle)
.key_context(if is_empty {
"EmptyPane"
} else {
"AssistantDiff"
})
.on_action(cx.listener(Self::toggle_keep))
.on_action(cx.listener(Self::reject))
.on_action(cx.listener(Self::reject_all))
.on_action(cx.listener(Self::keep_all))
.bg(cx.theme().colors().editor_background)
.flex()
.items_center()
.justify_center()
.size_full()
.when(is_empty, |el| el.child("No changes to review"))
.when(!is_empty, |el| el.child(self.editor.clone()))
}
}
fn render_diff_hunk_controls(
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<editor::Anchor>,
is_created_file: bool,
line_height: Pixels,
assistant_diff: &Entity<AssistantDiff>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let editor = assistant_diff.read(cx).editor.clone();
h_flex()
.h(line_height)
.mr_0p5()
.gap_1()
.px_0p5()
.pb_1()
.border_x_1()
.border_b_1()
.border_color(cx.theme().colors().border)
.rounded_b_md()
.bg(cx.theme().colors().editor_background)
.gap_1()
.occlude()
.shadow_md()
.children(if status.has_secondary_hunk() {
vec![
Button::new("reject", "Reject")
.disabled(is_created_file)
.key_binding(
KeyBinding::for_action_in(
&crate::Reject,
&editor.read(cx).focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let point = hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.restore_hunks_in_ranges(vec![point..point], window, cx);
});
}
}),
Button::new(("keep", row as u64), "Keep")
.key_binding(
KeyBinding::for_action_in(
&crate::ToggleKeep,
&editor.read(cx).focus_handle(cx),
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click({
let assistant_diff = assistant_diff.clone();
move |_event, _window, cx| {
assistant_diff.update(cx, |diff, cx| {
diff.review_diff_hunks(
vec![hunk_range.start..hunk_range.start],
true,
cx,
);
});
}
}),
]
} else {
vec![
Button::new(("review", row as u64), "Review")
.key_binding(KeyBinding::for_action_in(
&ToggleKeep,
&editor.read(cx).focus_handle(cx),
window,
cx,
))
.on_click({
let assistant_diff = assistant_diff.clone();
move |_event, _window, cx| {
assistant_diff.update(cx, |diff, cx| {
diff.review_diff_hunks(
vec![hunk_range.start..hunk_range.start],
false,
cx,
);
});
}
}),
]
})
.when(
!editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(),
|el| {
el.child(
IconButton::new(("next-hunk", row as u64), IconName::ArrowDown)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
// .disabled(!has_multiple_hunks)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Next Hunk",
&GoToHunk,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let position =
hunk_range.end.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_or_after_position(
&snapshot,
position,
Direction::Next,
window,
cx,
);
editor.expand_selected_diff_hunks(cx);
});
}
}),
)
.child(
IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
// .disabled(!has_multiple_hunks)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"Previous Hunk",
&GoToPreviousHunk,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let editor = editor.clone();
move |_event, window, cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_or_after_position(
&snapshot,
point,
Direction::Prev,
window,
cx,
);
editor.expand_selected_diff_hunks(cx);
});
}
}),
)
},
)
.into_any_element()
}
struct AssistantDiffAddon;
impl editor::Addon for AssistantDiffAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
key_context.add("assistant_diff");
}
}
pub struct AssistantDiffToolbar {
assistant_diff: Option<WeakEntity<AssistantDiff>>,
_workspace: WeakEntity<Workspace>,
}
impl AssistantDiffToolbar {
pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
Self {
assistant_diff: None,
_workspace: workspace.weak_handle(),
}
}
fn assistant_diff(&self, _: &App) -> Option<Entity<AssistantDiff>> {
self.assistant_diff.as_ref()?.upgrade()
}
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
if let Some(assistant_diff) = self.assistant_diff(cx) {
assistant_diff.focus_handle(cx).focus(window);
}
let action = action.boxed_clone();
cx.defer(move |cx| {
cx.dispatch_action(action.as_ref());
})
}
}
impl EventEmitter<ToolbarItemEvent> for AssistantDiffToolbar {}
impl ToolbarItemView for AssistantDiffToolbar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
self.assistant_diff = active_pane_item
.and_then(|item| item.act_as::<AssistantDiff>(cx))
.map(|entity| entity.downgrade());
if self.assistant_diff.is_some() {
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
}
fn pane_focus_update(
&mut self,
_pane_focused: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
}
impl Render for AssistantDiffToolbar {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let assistant_diff = match self.assistant_diff(cx) {
Some(ad) => ad,
None => return div(),
};
let is_empty = assistant_diff.read(cx).multibuffer.read(cx).is_empty();
if is_empty {
return div();
}
h_group_xl()
.my_neg_1()
.items_center()
.p_1()
.flex_wrap()
.justify_between()
.child(
h_group_sm()
.child(
Button::new("reject-all", "Reject All").on_click(cx.listener(
|this, _, window, cx| {
this.dispatch_action(&crate::RejectAll, window, cx)
},
)),
)
.child(Button::new("keep-all", "Keep All").on_click(cx.listener(
|this, _, window, cx| this.dispatch_action(&crate::KeepAll, window, cx),
))),
)
}
}

View File

@@ -7,7 +7,7 @@ use language_model_selector::{
}; };
use settings::update_settings_file; use settings::update_settings_file;
use std::sync::Arc; use std::sync::Arc;
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*}; use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
pub struct AssistantModelSelector { pub struct AssistantModelSelector {
selector: Entity<LanguageModelSelector>, selector: Entity<LanguageModelSelector>,

View File

@@ -1,10 +1,10 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use assistant_context_editor::{ use assistant_context_editor::{
AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider, make_lsp_adapter_delegate, render_remaining_tokens, AssistantPanelDelegate, ConfigurationError,
make_lsp_adapter_delegate, render_remaining_tokens, ContextEditor, SlashCommandCompletionProvider,
}; };
use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet; use assistant_slash_command::SlashCommandWorkingSet;
@@ -14,23 +14,22 @@ use client::zed_urls;
use editor::{Editor, MultiBuffer}; use editor::{Editor, MultiBuffer};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
action_with_deprecated_aliases, prelude::*, WeakEntity,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry}; use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use project::Project; use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library}; use prompt_library::{open_prompt_library, PromptLibrary};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use settings::{Settings, update_settings_file}; use settings::{update_settings_file, Settings};
use time::UtcOffset; use time::UtcOffset;
use ui::{ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*}; use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip};
use util::ResultExt as _; use util::ResultExt as _;
use workspace::Workspace;
use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::dock::{DockPosition, Panel, PanelEvent};
use zed_actions::assistant::ToggleFocus; use workspace::Workspace;
use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
use crate::active_thread::ActiveThread; use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent}; use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
@@ -40,16 +39,10 @@ use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread_history::{PastContext, PastThread, ThreadHistory}; use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::{ use crate::{
AssistantDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown, OpenConfiguration,
OpenAssistantDiff, OpenConfiguration, OpenHistory, ToggleContextPicker, OpenHistory,
}; };
action_with_deprecated_aliases!(
assistant,
OpenPromptLibrary,
["assistant::DeployPromptLibrary"]
);
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
cx.observe_new( cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| { |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
@@ -66,39 +59,17 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.open_history(window, cx)); panel.update(cx, |panel, cx| panel.open_history(window, cx));
} }
}) })
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
}
})
.register_action(|workspace, _: &NewPromptEditor, window, cx| { .register_action(|workspace, _: &NewPromptEditor, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) { if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx); workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
} }
}) })
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
});
}
})
.register_action(|workspace, _: &OpenConfiguration, window, cx| { .register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) { if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx); workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx)); panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
} }
})
.register_action(|workspace, _: &OpenAssistantDiff, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.open_assistant_diff(&OpenAssistantDiff, window, cx);
});
}
}); });
}, },
) )
@@ -128,7 +99,7 @@ pub struct AssistantPanel {
active_view: ActiveView, active_view: ActiveView,
history_store: Entity<HistoryStore>, history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>, history: Entity<ThreadHistory>,
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>, new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
width: Option<Pixels>, width: Option<Pixels>,
height: Option<Pixels>, height: Option<Pixels>,
} }
@@ -203,7 +174,6 @@ impl AssistantPanel {
thread_store.clone(), thread_store.clone(),
language_registry.clone(), language_registry.clone(),
message_editor_context_store.clone(), message_editor_context_store.clone(),
workspace.clone(),
window, window,
cx, cx,
) )
@@ -228,7 +198,7 @@ impl AssistantPanel {
.unwrap(), .unwrap(),
history_store: history_store.clone(), history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, cx)), history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, cx)),
assistant_dropdown_menu_handle: PopoverMenuHandle::default(), new_item_context_menu_handle: PopoverMenuHandle::default(),
width: None, width: None,
height: None, height: None,
} }
@@ -240,12 +210,12 @@ impl AssistantPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Workspace>, cx: &mut Context<Workspace>,
) { ) {
if workspace let settings = AssistantSettings::get_global(cx);
.panel::<Self>(cx) if !settings.enabled {
.is_some_and(|panel| panel.read(cx).enabled(cx)) return;
{
workspace.toggle_panel_focus::<Self>(window, cx);
} }
workspace.toggle_panel_focus::<Self>(window, cx);
} }
pub(crate) fn local_timezone(&self) -> UtcOffset { pub(crate) fn local_timezone(&self) -> UtcOffset {
@@ -282,7 +252,6 @@ impl AssistantPanel {
self.thread_store.clone(), self.thread_store.clone(),
self.language_registry.clone(), self.language_registry.clone(),
message_editor_context_store.clone(), message_editor_context_store.clone(),
self.workspace.clone(),
window, window,
cx, cx,
) )
@@ -332,7 +301,7 @@ impl AssistantPanel {
fn deploy_prompt_library( fn deploy_prompt_library(
&mut self, &mut self,
_: &OpenPromptLibrary, _: &DeployPromptLibrary,
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
@@ -420,7 +389,6 @@ impl AssistantPanel {
this.thread_store.clone(), this.thread_store.clone(),
this.language_registry.clone(), this.language_registry.clone(),
message_editor_context_store.clone(), message_editor_context_store.clone(),
this.workspace.clone(),
window, window,
cx, cx,
) )
@@ -441,16 +409,6 @@ impl AssistantPanel {
}) })
} }
pub fn open_assistant_diff(
&mut self,
_: &OpenAssistantDiff,
window: &mut Window,
cx: &mut Context<Self>,
) {
let thread = self.thread.read(cx).thread().clone();
AssistantDiff::deploy(thread, self.workspace.clone(), window, cx).log_err();
}
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let context_server_manager = self.thread_store.read(cx).context_server_manager(); let context_server_manager = self.thread_store.read(cx).context_server_manager();
let tools = self.thread_store.read(cx).tools(); let tools = self.thread_store.read(cx).tools();
@@ -497,7 +455,7 @@ impl AssistantPanel {
workspace.update_in(cx, |workspace, window, cx| { workspace.update_in(cx, |workspace, window, cx| {
let thread = thread.read(cx); let thread = thread.read(cx);
let markdown = thread.to_markdown(cx)?; let markdown = thread.to_markdown()?;
let thread_summary = thread let thread_summary = thread
.summary() .summary()
.map(|summary| summary.to_string()) .map(|summary| summary.to_string())
@@ -662,8 +620,12 @@ impl Panel for AssistantPanel {
} }
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> { fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
(self.enabled(cx) && AssistantSettings::get_global(cx).button) let settings = AssistantSettings::get_global(cx);
.then_some(IconName::ZedAssistant) if !settings.enabled || !settings.button {
return None;
}
Some(IconName::ZedAssistant)
} }
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
@@ -677,16 +639,11 @@ impl Panel for AssistantPanel {
fn activation_priority(&self) -> u32 { fn activation_priority(&self) -> u32 {
3 3
} }
fn enabled(&self, cx: &App) -> bool {
AssistantSettings::get_global(cx).enabled
}
} }
impl AssistantPanel { impl AssistantPanel {
fn render_toolbar(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render_toolbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
let thread = self.thread.read(cx); let thread = self.thread.read(cx);
let focus_handle = self.focus_handle(cx);
let title = match self.active_view { let title = match self.active_view {
ActiveView::Thread => { ActiveView::Thread => {
@@ -706,7 +663,7 @@ impl AssistantPanel {
}) })
.unwrap_or_else(|| SharedString::from("Loading Summary…")), .unwrap_or_else(|| SharedString::from("Loading Summary…")),
ActiveView::History => "History".into(), ActiveView::History => "History".into(),
ActiveView::Configuration => "Settings".into(), ActiveView::Configuration => "Assistant Settings".into(),
}; };
h_flex() h_flex()
@@ -746,47 +703,57 @@ impl AssistantPanel {
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx)) .gap(DynamicSpacing::Base02.rems(cx))
.child( .child(
IconButton::new("new", IconName::Plus) PopoverMenu::new("assistant-toolbar-new-popover-menu")
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"New Thread",
&NewThread,
&focus_handle,
window,
cx,
)
})
.on_click(move |_event, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx);
}),
)
.child(
PopoverMenu::new("assistant-menu")
.trigger_with_tooltip( .trigger_with_tooltip(
IconButton::new("new", IconName::Ellipsis) IconButton::new("new", IconName::Plus)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.style(ButtonStyle::Subtle), .style(ButtonStyle::Subtle),
Tooltip::text("Toggle Assistant Menu"), Tooltip::text("New…"),
) )
.anchor(Corner::TopRight) .anchor(Corner::TopRight)
.with_handle(self.assistant_dropdown_menu_handle.clone()) .with_handle(self.new_item_context_menu_handle.clone())
.menu(move |window, cx| { .menu(move |window, cx| {
Some(ContextMenu::build( Some(ContextMenu::build(
window, window,
cx, cx,
|menu, _window, _cx| { |menu, _window, _cx| {
menu.action( menu.action("New Thread", NewThread.boxed_clone())
"New Prompt Editor", .action(
NewPromptEditor.boxed_clone(), "New Prompt Editor",
) NewPromptEditor.boxed_clone(),
.separator() )
.action("History", OpenHistory.boxed_clone())
.action("Settings", OpenConfiguration.boxed_clone())
}, },
)) ))
}), }),
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = self.focus_handle(cx);
move |window, cx| {
Tooltip::for_action_in(
"History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
})
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx);
}),
)
.child(
IconButton::new("configure-assistant", IconName::Settings)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(Tooltip::text("Assistant Settings"))
.on_click(move |_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
}),
), ),
), ),
) )
@@ -831,150 +798,66 @@ impl AssistantPanel {
.history_store .history_store
.update(cx, |this, cx| this.recent_entries(6, cx)); .update(cx, |this, cx| this.recent_entries(6, cx));
let create_welcome_heading = || {
h_flex()
.w_full()
.child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small))
};
let configuration_error = self.configuration_error(cx); let configuration_error = self.configuration_error(cx);
let no_error = configuration_error.is_none(); let no_error = configuration_error.is_none();
let focus_handle = self.focus_handle(cx);
v_flex() v_flex()
.p_1p5()
.size_full() .size_full()
.when(recent_history.is_empty(), |this| { .justify_end()
this.child( .gap_1()
v_flex() .map(|parent| {
.size_full() match configuration_error {
.max_w_80() Some(ConfigurationError::ProviderNotAuthenticated)
.mx_auto() | Some(ConfigurationError::NoProvider) => {
.justify_center() parent.child(
.items_center() v_flex()
.gap_1() .px_1p5()
.child( .gap_0p5()
h_flex().child( .child(create_welcome_heading())
Headline::new("Welcome to the Assistant Panel") .child(
), Label::new(
) "To start using the assistant, configure at least one LLM provider.",
.when(no_error, |parent| { )
parent.child( .color(Color::Muted),
h_flex().child( )
Label::new("Ask and build anything.") .child(
.color(Color::Muted) h_flex().mt_1().w_full().child(
.mb_2p5(), Button::new("open-configuration", "Configure a Provider")
), .size(ButtonSize::Compact)
) .icon(Some(IconName::Sliders))
.child( .icon_size(IconSize::Small)
Button::new("new-thread", "Start New Thread") .icon_position(IconPosition::Start)
.icon(IconName::Plus) .on_click(cx.listener(|this, _, window, cx| {
.icon_position(IconPosition::Start) this.open_configuration(window, cx);
.icon_size(IconSize::Small) })),
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&NewThread,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx)
}),
)
.child(
Button::new("context", "Add Context")
.icon(IconName::FileCode)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&ToggleContextPicker,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
}),
)
.child(
Button::new("mode", "Switch Model")
.icon(IconName::DatabaseZap)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&ToggleModelSelector,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
}),
)
.child(
Button::new("settings", "View Settings")
.icon(IconName::Settings)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
})
.map(|parent| {
match configuration_error {
Some(ConfigurationError::ProviderNotAuthenticated)
| Some(ConfigurationError::NoProvider) => {
parent
.child(
h_flex().child(
Label::new("To start using the assistant, configure at least one LLM provider.")
.color(Color::Muted)
.mb_2p5()
)
)
.child(
Button::new("settings", "Configure a Provider")
.icon(IconName::Settings)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
}),
)
}
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
.children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
),
), ),
None => parent, ),
} )
}) }
) Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
.child(v_flex().px_1p5().gap_0p5().child(create_welcome_heading()).children(
provider.render_accept_terms(
LanguageModelProviderTosView::ThreadEmptyState,
cx,
),
)),
None => parent,
}
})
.when(recent_history.is_empty() && no_error, |parent| {
parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child(
Label::new("Start typing to chat with your codebase").color(Color::Muted),
))
}) })
.when(!recent_history.is_empty(), |parent| { .when(!recent_history.is_empty(), |parent| {
parent parent
.p_1p5()
.justify_end()
.gap_1()
.child( .child(
h_flex() h_flex()
.pl_1p5() .pl_1p5()
@@ -997,7 +880,7 @@ impl AssistantPanel {
&self.focus_handle(cx), &self.focus_handle(cx),
window, window,
cx, cx,
).map(|kb| kb.size(rems_from_px(12.))),) ))
.on_click(move |_event, window, cx| { .on_click(move |_event, window, cx| {
window.dispatch_action(OpenHistory.boxed_clone(), cx); window.dispatch_action(OpenHistory.boxed_clone(), cx);
}), }),
@@ -1005,7 +888,7 @@ impl AssistantPanel {
) )
.child(v_flex().gap_1().children( .child(v_flex().gap_1().children(
recent_history.into_iter().map(|entry| { recent_history.into_iter().map(|entry| {
// TODO: Add keyboard navigation. // TODO: Add keyboard navigation.
match entry { match entry {
HistoryEntry::Thread(thread) => { HistoryEntry::Thread(thread) => {
PastThread::new(thread, cx.entity().downgrade(), false) PastThread::new(thread, cx.entity().downgrade(), false)
@@ -1039,8 +922,8 @@ impl AssistantPanel {
ThreadError::MaxMonthlySpendReached => { ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx) self.render_max_monthly_spend_reached_error(cx)
} }
ThreadError::Message { header, message } => { ThreadError::Message(error_message) => {
self.render_error_message(header, message, cx) self.render_error_message(&error_message, cx)
} }
}) })
.into_any(), .into_any(),
@@ -1143,8 +1026,7 @@ impl AssistantPanel {
fn render_error_message( fn render_error_message(
&self, &self,
header: SharedString, error_message: &SharedString,
message: SharedString,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> AnyElement { ) -> AnyElement {
v_flex() v_flex()
@@ -1154,14 +1036,17 @@ impl AssistantPanel {
.gap_1p5() .gap_1p5()
.items_center() .items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error)) .child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new(header).weight(FontWeight::MEDIUM)), .child(
Label::new("Error interacting with language model")
.weight(FontWeight::MEDIUM),
),
) )
.child( .child(
div() div()
.id("error-message") .id("error-message")
.max_h_32() .max_h_32()
.overflow_y_scroll() .overflow_y_scroll()
.child(Label::new(message)), .child(Label::new(error_message.clone())),
) )
.child( .child(
h_flex() h_flex()
@@ -1203,13 +1088,9 @@ impl Render for AssistantPanel {
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| { .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
this.open_history(window, cx); this.open_history(window, cx);
})) }))
.on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
this.open_configuration(window, cx);
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown)) .on_action(cx.listener(Self::open_active_thread_as_markdown))
.on_action(cx.listener(Self::deploy_prompt_library)) .on_action(cx.listener(Self::deploy_prompt_library))
.on_action(cx.listener(Self::open_assistant_diff)) .child(self.render_toolbar(cx))
.child(self.render_toolbar(window, cx))
.map(|parent| match self.active_view { .map(|parent| match self.active_view {
ActiveView::Thread => parent ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(window, cx)) .child(self.render_active_thread_or_empty_state(window, cx))

View File

@@ -5,12 +5,12 @@ use anyhow::{Context as _, Result};
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
use collections::HashSet; use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint}; use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use futures::{SinkExt, Stream, StreamExt, channel::mpsc, future::LocalBoxFuture, join}; use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, StreamExt};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task}; use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
use language::{Buffer, IndentKind, Point, TransactionId, line_diff}; use language::{line_diff, Buffer, IndentKind, Point, TransactionId};
use language_model::{ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
LanguageModelTextStream, Role, report_assistant_event, LanguageModelRequestMessage, LanguageModelTextStream, Role,
}; };
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
@@ -414,11 +414,7 @@ impl CodegenAlternative {
}; };
if let Some(context_store) = &self.context_store { if let Some(context_store) = &self.context_store {
attach_context_to_message( attach_context_to_message(&mut request_message, context_store.read(cx).snapshot(cx));
&mut request_message,
context_store.read(cx).context().iter(),
cx,
);
} }
request_message.content.push(prompt.into()); request_message.content.push(prompt.into());
@@ -486,17 +482,11 @@ impl CodegenAlternative {
self.generation = cx.spawn(async move |codegen, cx| { self.generation = cx.spawn(async move |codegen, cx| {
let stream = stream.await; let stream = stream.await;
let token_usage = stream
.as_ref()
.ok()
.map(|stream| stream.last_token_usage.clone());
let message_id = stream let message_id = stream
.as_ref() .as_ref()
.ok() .ok()
.and_then(|stream| stream.message_id.clone()); .and_then(|stream| stream.message_id.clone());
let generate = async { let generate = async {
let model_telemetry_id = model_telemetry_id.clone();
let model_provider_id = model_provider_id.clone();
let (mut diff_tx, mut diff_rx) = mpsc::channel(1); let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
let executor = cx.background_executor().clone(); let executor = cx.background_executor().clone();
let message_id = message_id.clone(); let message_id = message_id.clone();
@@ -606,7 +596,7 @@ impl CodegenAlternative {
kind: AssistantKind::Inline, kind: AssistantKind::Inline,
phase: AssistantPhase::Response, phase: AssistantPhase::Response,
model: model_telemetry_id, model: model_telemetry_id,
model_provider: model_provider_id, model_provider: model_provider_id.to_string(),
response_latency, response_latency,
error_message, error_message,
language_name: language_name.map(|name| name.to_proto()), language_name: language_name.map(|name| name.to_proto()),
@@ -687,16 +677,6 @@ impl CodegenAlternative {
} }
this.elapsed_time = Some(elapsed_time); this.elapsed_time = Some(elapsed_time);
this.completion = Some(completion.lock().clone()); this.completion = Some(completion.lock().clone());
if let Some(usage) = token_usage {
let usage = usage.lock();
telemetry::event!(
"Inline Assistant Completion",
model = model_telemetry_id,
model_provider = model_provider_id,
input_tokens = usage.input_tokens,
output_tokens = usage.output_tokens,
)
}
cx.emit(CodegenEvent::Finished); cx.emit(CodegenEvent::Finished);
cx.notify(); cx.notify();
}) })
@@ -1032,16 +1012,16 @@ impl Diff {
mod tests { mod tests {
use super::*; use super::*;
use futures::{ use futures::{
Stream,
stream::{self}, stream::{self},
Stream,
}; };
use gpui::TestAppContext; use gpui::TestAppContext;
use indoc::indoc; use indoc::indoc;
use language::{ use language::{
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings, language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
tree_sitter_rust, Point,
}; };
use language_model::{LanguageModelRegistry, TokenUsage}; use language_model::LanguageModelRegistry;
use rand::prelude::*; use rand::prelude::*;
use serde::Serialize; use serde::Serialize;
use settings::SettingsStore; use settings::SettingsStore;
@@ -1425,7 +1405,6 @@ mod tests {
future::ready(Ok(LanguageModelTextStream { future::ready(Ok(LanguageModelTextStream {
message_id: None, message_id: None,
stream: chunks_rx.map(Ok).boxed(), stream: chunks_rx.map(Ok).boxed(),
last_token_usage: Arc::new(Mutex::new(TokenUsage::default())),
})), })),
cx, cx,
); );

View File

@@ -1,15 +1,16 @@
use std::{ops::Range, sync::Arc}; use std::path::Path;
use std::rc::Rc;
use file_icons::FileIcons;
use gpui::{App, Entity, SharedString}; use gpui::{App, Entity, SharedString};
use language::{Buffer, File}; use language::Buffer;
use language_model::{LanguageModelRequestMessage, MessageContent}; use language_model::{LanguageModelRequestMessage, MessageContent};
use project::ProjectPath;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use text::{Anchor, BufferId}; use text::BufferId;
use ui::IconName; use ui::IconName;
use util::post_inc; use util::post_inc;
use crate::thread::Thread; use crate::{context_store::buffer_path_log_err, thread::Thread};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize); pub struct ContextId(pub(crate) usize);
@@ -20,11 +21,23 @@ impl ContextId {
} }
} }
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct ContextSnapshot {
pub id: ContextId,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
pub kind: ContextKind,
/// Joining these strings separated by \n yields text for model. Not refreshed by `snapshot`.
pub text: Box<[SharedString]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextKind { pub enum ContextKind {
File, File,
Directory, Directory,
Symbol,
FetchedUrl, FetchedUrl,
Thread, Thread,
} }
@@ -34,18 +47,16 @@ impl ContextKind {
match self { match self {
ContextKind::File => IconName::File, ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder, ContextKind::Directory => IconName::Folder,
ContextKind::Symbol => IconName::Code,
ContextKind::FetchedUrl => IconName::Globe, ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageBubbles, ContextKind::Thread => IconName::MessageCircle,
} }
} }
} }
#[derive(Debug, Clone)] #[derive(Debug)]
pub enum AssistantContext { pub enum AssistantContext {
File(FileContext), File(FileContext),
Directory(DirectoryContext), Directory(DirectoryContext),
Symbol(SymbolContext),
FetchedUrl(FetchedUrlContext), FetchedUrl(FetchedUrlContext),
Thread(ThreadContext), Thread(ThreadContext),
} }
@@ -54,34 +65,27 @@ impl AssistantContext {
pub fn id(&self) -> ContextId { pub fn id(&self) -> ContextId {
match self { match self {
Self::File(file) => file.id, Self::File(file) => file.id,
Self::Directory(directory) => directory.id, Self::Directory(directory) => directory.snapshot.id,
Self::Symbol(symbol) => symbol.id,
Self::FetchedUrl(url) => url.id, Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id, Self::Thread(thread) => thread.id,
} }
} }
} }
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct FileContext { pub struct FileContext {
pub id: ContextId, pub id: ContextId,
pub context_buffer: ContextBuffer, pub context_buffer: ContextBuffer,
} }
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct DirectoryContext { pub struct DirectoryContext {
pub id: ContextId, pub path: Rc<Path>,
pub project_path: ProjectPath,
pub context_buffers: Vec<ContextBuffer>, pub context_buffers: Vec<ContextBuffer>,
pub snapshot: ContextSnapshot,
} }
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct SymbolContext {
pub id: ContextId,
pub context_symbol: ContextSymbol,
}
#[derive(Debug, Clone)]
pub struct FetchedUrlContext { pub struct FetchedUrlContext {
pub id: ContextId, pub id: ContextId,
pub url: SharedString, pub url: SharedString,
@@ -91,129 +95,218 @@ pub struct FetchedUrlContext {
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this // TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
// explicitly or have a WeakModel<Thread> and remove during snapshot. // explicitly or have a WeakModel<Thread> and remove during snapshot.
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct ThreadContext { pub struct ThreadContext {
pub id: ContextId, pub id: ContextId,
pub thread: Entity<Thread>, pub thread: Entity<Thread>,
pub text: SharedString, pub text: SharedString,
} }
impl ThreadContext {
pub fn summary(&self, cx: &App) -> SharedString {
self.thread
.read(cx)
.summary()
.unwrap_or("New thread".into())
}
}
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove // TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
// the context from the message editor in this case. // the context from the message editor in this case.
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct ContextBuffer { pub struct ContextBuffer {
pub id: BufferId, pub id: BufferId,
pub buffer: Entity<Buffer>, pub buffer: Entity<Buffer>,
pub file: Arc<dyn File>,
pub version: clock::Global, pub version: clock::Global,
pub text: SharedString, pub text: SharedString,
} }
impl std::fmt::Debug for ContextBuffer { impl AssistantContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
f.debug_struct("ContextBuffer") match &self {
.field("id", &self.id) Self::File(file_context) => file_context.snapshot(cx),
.field("buffer", &self.buffer) Self::Directory(directory_context) => Some(directory_context.snapshot()),
.field("version", &self.version) Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
.field("text", &self.text) Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
.finish() }
} }
} }
#[derive(Debug, Clone)] impl FileContext {
pub struct ContextSymbol { pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
pub id: ContextSymbolId, let buffer = self.context_buffer.buffer.read(cx);
pub buffer: Entity<Buffer>, let path = buffer_path_log_err(buffer)?;
pub buffer_version: clock::Global, let full_path: SharedString = path.to_string_lossy().into_owned().into();
/// The range that the symbol encloses, e.g. for function symbol, this will let name = match path.file_name() {
/// include not only the signature, but also the body Some(name) => name.to_string_lossy().into_owned().into(),
pub enclosing_range: Range<Anchor>, None => full_path.clone(),
pub text: SharedString, };
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().into_owned().into());
let icon_path = FileIcons::get_icon(&path, cx);
Some(ContextSnapshot {
id: self.id,
name,
parent,
tooltip: Some(full_path),
icon_path,
kind: ContextKind::File,
text: Box::new([self.context_buffer.text.clone()]),
})
}
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash)] impl DirectoryContext {
pub struct ContextSymbolId { pub fn new(
pub path: ProjectPath, id: ContextId,
pub name: SharedString, path: &Path,
pub range: Range<Anchor>, context_buffers: Vec<ContextBuffer>,
} ) -> DirectoryContext {
let full_path: SharedString = path.to_string_lossy().into_owned().into();
pub fn attach_context_to_message<'a>( let name = match path.file_name() {
message: &mut LanguageModelRequestMessage, Some(name) => name.to_string_lossy().into_owned().into(),
contexts: impl Iterator<Item = &'a AssistantContext>, None => full_path.clone(),
cx: &App, };
) {
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut symbol_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
for context in contexts { let parent = path
match context { .parent()
AssistantContext::File(context) => file_context.push(context), .and_then(|p| p.file_name())
AssistantContext::Directory(context) => directory_context.push(context), .map(|p| p.to_string_lossy().into_owned().into());
AssistantContext::Symbol(context) => symbol_context.push(context),
AssistantContext::FetchedUrl(context) => fetch_context.push(context), // TODO: include directory path in text?
AssistantContext::Thread(context) => thread_context.push(context), let text = context_buffers
.iter()
.map(|b| b.text.clone())
.collect::<Vec<_>>()
.into();
DirectoryContext {
path: path.into(),
context_buffers,
snapshot: ContextSnapshot {
id,
name,
parent,
tooltip: Some(full_path),
icon_path: None,
kind: ContextKind::Directory,
text,
},
} }
} }
let mut context_chunks = Vec::new(); pub fn snapshot(&self) -> ContextSnapshot {
self.snapshot.clone()
}
}
impl FetchedUrlContext {
pub fn snapshot(&self) -> ContextSnapshot {
ContextSnapshot {
id: self.id,
name: self.url.clone(),
parent: None,
tooltip: None,
icon_path: None,
kind: ContextKind::FetchedUrl,
text: Box::new([self.text.clone()]),
}
}
}
impl ThreadContext {
pub fn snapshot(&self, cx: &App) -> ContextSnapshot {
let thread = self.thread.read(cx);
ContextSnapshot {
id: self.id,
name: thread.summary().unwrap_or("New thread".into()),
parent: None,
tooltip: None,
icon_path: None,
kind: ContextKind::Thread,
text: Box::new([self.text.clone()]),
}
}
}
pub fn attach_context_to_message(
message: &mut LanguageModelRequestMessage,
contexts: impl Iterator<Item = ContextSnapshot>,
) {
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
let mut capacity = 0;
for context in contexts {
capacity += context.text.len();
match context.kind {
ContextKind::File => file_context.push(context),
ContextKind::Directory => directory_context.push(context),
ContextKind::FetchedUrl => fetch_context.push(context),
ContextKind::Thread => thread_context.push(context),
}
}
if !file_context.is_empty() {
capacity += 1;
}
if !directory_context.is_empty() {
capacity += 1;
}
if !fetch_context.is_empty() {
capacity += 1 + fetch_context.len();
}
if !thread_context.is_empty() {
capacity += 1 + thread_context.len();
}
if capacity == 0 {
return;
}
let mut context_chunks = Vec::with_capacity(capacity);
if !file_context.is_empty() { if !file_context.is_empty() {
context_chunks.push("The following files are available:\n"); context_chunks.push("The following files are available:\n");
for context in file_context { for context in &file_context {
context_chunks.push(&context.context_buffer.text); for chunk in &context.text {
context_chunks.push(&chunk);
}
} }
} }
if !directory_context.is_empty() { if !directory_context.is_empty() {
context_chunks.push("The following directories are available:\n"); context_chunks.push("The following directories are available:\n");
for context in directory_context { for context in &directory_context {
for context_buffer in &context.context_buffers { for chunk in &context.text {
context_chunks.push(&context_buffer.text); context_chunks.push(&chunk);
} }
} }
} }
if !symbol_context.is_empty() {
context_chunks.push("The following symbols are available:\n");
for context in symbol_context {
context_chunks.push(&context.context_symbol.text);
}
}
if !fetch_context.is_empty() { if !fetch_context.is_empty() {
context_chunks.push("The following fetched results are available:\n"); context_chunks.push("The following fetched results are available:\n");
for context in &fetch_context { for context in &fetch_context {
context_chunks.push(&context.url); context_chunks.push(&context.name);
context_chunks.push(&context.text); for chunk in &context.text {
context_chunks.push(&chunk);
}
} }
} }
// Need to own the SharedString for summary so that it can be referenced.
let mut thread_context_chunks = Vec::new();
if !thread_context.is_empty() { if !thread_context.is_empty() {
context_chunks.push("The following previous conversation threads are available:\n"); context_chunks.push("The following previous conversation threads are available:\n");
for context in &thread_context { for context in &thread_context {
thread_context_chunks.push(context.summary(cx)); context_chunks.push(&context.name);
thread_context_chunks.push(context.text.clone()); for chunk in &context.text {
context_chunks.push(&chunk);
}
} }
} }
for chunk in &thread_context_chunks {
context_chunks.push(chunk); debug_assert!(
} context_chunks.len() == capacity,
"attach_context_message calculated capacity of {}, but length was {}",
capacity,
context_chunks.len()
);
if !context_chunks.is_empty() { if !context_chunks.is_empty() {
message message

View File

@@ -1,36 +1,25 @@
mod completion_provider;
mod fetch_context_picker; mod fetch_context_picker;
mod file_context_picker; mod file_context_picker;
mod symbol_context_picker;
mod thread_context_picker; mod thread_context_picker;
use std::ops::Range;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use editor::display_map::{Crease, FoldId}; use editor::Editor;
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use file_context_picker::render_file_context_entry; use file_context_picker::render_file_context_entry;
use gpui::{ use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
};
use multi_buffer::MultiBufferRow;
use project::ProjectPath; use project::ProjectPath;
use symbol_context_picker::SymbolContextPicker; use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
use thread_context_picker::{ThreadContextEntry, render_thread_context_entry}; use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
use ui::{ use workspace::{notifications::NotifyResultExt, Workspace};
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
};
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::AssistantPanel;
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
use crate::context_picker::fetch_context_picker::FetchContextPicker; use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker; use crate::context_picker::file_context_picker::FileContextPicker;
use crate::context_picker::thread_context_picker::ThreadContextPicker; use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::AssistantPanel;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum ConfirmBehavior { pub enum ConfirmBehavior {
@@ -41,39 +30,14 @@ pub enum ConfirmBehavior {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerMode { enum ContextPickerMode {
File, File,
Symbol,
Fetch, Fetch,
Thread, Thread,
} }
impl TryFrom<&str> for ContextPickerMode {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"file" => Ok(Self::File),
"symbol" => Ok(Self::Symbol),
"fetch" => Ok(Self::Fetch),
"thread" => Ok(Self::Thread),
_ => Err(format!("Invalid context picker mode: {}", value)),
}
}
}
impl ContextPickerMode { impl ContextPickerMode {
pub fn mention_prefix(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
Self::Fetch => "fetch",
Self::Thread => "thread",
}
}
pub fn label(&self) -> &'static str { pub fn label(&self) -> &'static str {
match self { match self {
Self::File => "Files & Directories", Self::File => "File/Directory",
Self::Symbol => "Symbols",
Self::Fetch => "Fetch", Self::Fetch => "Fetch",
Self::Thread => "Thread", Self::Thread => "Thread",
} }
@@ -82,9 +46,8 @@ impl ContextPickerMode {
pub fn icon(&self) -> IconName { pub fn icon(&self) -> IconName {
match self { match self {
Self::File => IconName::File, Self::File => IconName::File,
Self::Symbol => IconName::Code,
Self::Fetch => IconName::Globe, Self::Fetch => IconName::Globe,
Self::Thread => IconName::MessageBubbles, Self::Thread => IconName::MessageCircle,
} }
} }
} }
@@ -93,7 +56,6 @@ impl ContextPickerMode {
enum ContextPickerState { enum ContextPickerState {
Default(Entity<ContextMenu>), Default(Entity<ContextMenu>),
File(Entity<FileContextPicker>), File(Entity<FileContextPicker>),
Symbol(Entity<SymbolContextPicker>),
Fetch(Entity<FetchContextPicker>), Fetch(Entity<FetchContextPicker>),
Thread(Entity<ThreadContextPicker>), Thread(Entity<ThreadContextPicker>),
} }
@@ -101,6 +63,7 @@ enum ContextPickerState {
pub(super) struct ContextPicker { pub(super) struct ContextPicker {
mode: ContextPickerState, mode: ContextPickerState,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: Option<WeakEntity<ThreadStore>>,
confirm_behavior: ConfirmBehavior, confirm_behavior: ConfirmBehavior,
@@ -111,6 +74,7 @@ impl ContextPicker {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: Option<WeakEntity<ThreadStore>>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
editor: WeakEntity<Editor>,
confirm_behavior: ConfirmBehavior, confirm_behavior: ConfirmBehavior,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@@ -124,6 +88,7 @@ impl ContextPicker {
workspace, workspace,
context_store, context_store,
thread_store, thread_store,
editor,
confirm_behavior, confirm_behavior,
} }
} }
@@ -144,7 +109,10 @@ impl ContextPicker {
.enumerate() .enumerate()
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry)); .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
let modes = supported_context_picker_modes(&self.thread_store); let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
if self.allow_threads() {
modes.push(ContextPickerMode::Thread);
}
let menu = menu let menu = menu
.when(has_recent, |menu| { .when(has_recent, |menu| {
@@ -206,18 +174,7 @@ impl ContextPicker {
FileContextPicker::new( FileContextPicker::new(
context_picker.clone(), context_picker.clone(),
self.workspace.clone(), self.workspace.clone(),
self.context_store.clone(), self.editor.clone(),
self.confirm_behavior,
window,
cx,
)
}));
}
ContextPickerMode::Symbol => {
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
SymbolContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(), self.context_store.clone(),
self.confirm_behavior, self.confirm_behavior,
window, window,
@@ -321,7 +278,7 @@ impl ContextPicker {
}; };
let task = context_store.update(cx, |context_store, cx| { let task = context_store.update(cx, |context_store, cx| {
context_store.add_file_from_path(project_path.clone(), true, cx) context_store.add_file_from_path(project_path.clone(), cx)
}); });
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx)) cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
@@ -351,7 +308,7 @@ impl ContextPicker {
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?; let thread = open_thread_task.await?;
context_store.update(cx, |context_store, cx| { context_store.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx); context_store.add_thread(thread, cx);
})?; })?;
this.update(cx, |_this, cx| cx.notify()) this.update(cx, |_this, cx| cx.notify())
@@ -371,7 +328,7 @@ impl ContextPicker {
let mut current_files = context_store.file_paths(cx); let mut current_files = context_store.file_paths(cx);
if let Some(active_path) = active_singleton_buffer_path(&workspace, cx) { if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
current_files.insert(active_path); current_files.insert(active_path);
} }
@@ -427,6 +384,16 @@ impl ContextPicker {
recent recent
} }
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
let active_item = workspace.active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let buffer = editor.buffer().read(cx).as_singleton()?;
let path = buffer.read(cx).file()?.path().to_path_buf();
Some(path)
}
} }
impl EventEmitter<DismissEvent> for ContextPicker {} impl EventEmitter<DismissEvent> for ContextPicker {}
@@ -436,7 +403,6 @@ impl Focusable for ContextPicker {
match &self.mode { match &self.mode {
ContextPickerState::Default(menu) => menu.focus_handle(cx), ContextPickerState::Default(menu) => menu.focus_handle(cx),
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx), ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx), ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx), ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
} }
@@ -451,7 +417,6 @@ impl Render for ContextPicker {
.map(|parent| match &self.mode { .map(|parent| match &self.mode {
ContextPickerState::Default(menu) => parent.child(menu.clone()), ContextPickerState::Default(menu) => parent.child(menu.clone()),
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()), ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()), ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()), ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
}) })
@@ -464,216 +429,3 @@ enum RecentEntry {
}, },
Thread(ThreadContextEntry), Thread(ThreadContextEntry),
} }
fn supported_context_picker_modes(
thread_store: &Option<WeakEntity<ThreadStore>>,
) -> Vec<ContextPickerMode> {
let mut modes = vec![
ContextPickerMode::File,
ContextPickerMode::Symbol,
ContextPickerMode::Fetch,
];
if thread_store.is_some() {
modes.push(ContextPickerMode::Thread);
}
modes
}
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
let active_item = workspace.active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let buffer = editor.buffer().read(cx).as_singleton()?;
let path = buffer.read(cx).file()?.path().to_path_buf();
Some(path)
}
fn recent_context_picker_entries(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
workspace: Entity<Workspace>,
cx: &App,
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
let mut current_files = context_store.read(cx).file_paths(cx);
let workspace = workspace.read(cx);
if let Some(active_path) = active_singleton_buffer_path(workspace, cx) {
current_files.insert(active_path);
}
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
.take(4)
.filter_map(|(project_path, _)| {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| RecentEntry::File {
project_path,
path_prefix: worktree.read(cx).root_name().into(),
})
}),
);
let mut current_threads = context_store.read(cx).thread_ids();
if let Some(active_thread) = workspace
.panel::<AssistantPanel>(cx)
.map(|panel| panel.read(cx).active_thread(cx))
{
current_threads.insert(active_thread.read(cx).id().clone());
}
if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
recent.extend(
thread_store
.read(cx)
.threads()
.into_iter()
.filter(|thread| !current_threads.contains(&thread.id))
.take(2)
.map(|thread| {
RecentEntry::Thread(ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
}),
);
}
recent
}
pub(crate) fn insert_crease_for_mention(
excerpt_id: ExcerptId,
crease_start: text::Anchor,
content_len: usize,
crease_label: SharedString,
crease_icon_path: SharedString,
editor_entity: Entity<Editor>,
window: &mut Window,
cx: &mut App,
) {
editor_entity.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, crease_start) else {
return;
};
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
crease_icon_path,
crease_label,
editor_entity.downgrade(),
),
..Default::default()
};
let render_trailer =
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
let crease = Crease::inline(
start..end,
placeholder.clone(),
fold_toggle("mention"),
render_trailer,
);
editor.insert_creases(vec![crease.clone()], cx);
editor.fold_creases(vec![crease], false, window, cx);
});
}
fn render_fold_icon_button(
icon_path: SharedString,
label: SharedString,
editor: WeakEntity<Editor>,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new({
move |fold_id, fold_range, cx| {
let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
editor.update(cx, |editor, cx| {
let snapshot = editor
.buffer()
.update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
let is_in_pending_selection = || {
editor
.selections
.pending
.as_ref()
.is_some_and(|pending_selection| {
pending_selection
.selection
.range()
.includes(&fold_range, &snapshot)
})
};
let mut is_in_complete_selection = || {
editor
.selections
.disjoint_in_range::<usize>(fold_range.clone(), cx)
.into_iter()
.any(|selection| {
// This is needed to cover a corner case, if we just check for an existing
// selection in the fold range, having a cursor at the start of the fold
// marks it as selected. Non-empty selections don't cause this.
let length = selection.end - selection.start;
length > 0
})
};
is_in_pending_selection() || is_in_complete_selection()
})
});
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(icon_path.clone())
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.single_line(),
),
)
.into_any_element()
}
})
}
fn fold_toggle(
name: &'static str,
) -> impl Fn(
MultiBufferRow,
bool,
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
&mut Window,
&mut App,
) -> AnyElement {
move |row, is_folded, fold, _window, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
.into_any_element()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,13 @@ use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context as _, Result, bail}; use anyhow::{bail, Context as _, Result};
use futures::AsyncReadExt as _; use futures::AsyncReadExt as _;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl}; use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use ui::{Context, ListItem, Window, prelude::*}; use ui::{prelude::*, Context, ListItem, Window};
use workspace::Workspace; use workspace::Workspace;
use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_picker::{ConfirmBehavior, ContextPicker};
@@ -81,80 +81,77 @@ impl FetchContextPickerDelegate {
url: String::new(), url: String::new(),
} }
} }
}
pub(crate) async fn fetch_url_content( async fn build_message(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
http_client: Arc<HttpClientWithUrl>, let url = if !url.starts_with("https://") && !url.starts_with("http://") {
url: String, format!("https://{url}")
) -> Result<String> { } else {
let url = if !url.starts_with("https://") && !url.starts_with("http://") { url
format!("https://{url}") };
} else {
url
};
let mut response = http_client.get(&url, AsyncBody::default(), true).await?; let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new(); let mut body = Vec::new();
response response
.body_mut() .body_mut()
.read_to_end(&mut body) .read_to_end(&mut body)
.await .await
.context("error reading response body")?; .context("error reading response body")?;
if response.status().is_client_error() { if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice()); let text = String::from_utf8_lossy(body.as_slice());
bail!( bail!(
"status error {}, response: {text:?}", "status error {}, response: {text:?}",
response.status().as_u16() response.status().as_u16()
); );
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
} }
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!( let Some(content_type) = response.headers().get("content-type") else {
"```json\n{}\n```", bail!("missing Content-Type header");
serde_json::to_string_pretty(&json)? };
)) let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
} }
} }
} }
@@ -163,7 +160,11 @@ impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem; type ListItem = ListItem;
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
if self.url.is_empty() { 0 } else { 1 } if self.url.is_empty() {
0
} else {
1
}
} }
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> { fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
@@ -207,7 +208,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
let confirm_behavior = self.confirm_behavior; let confirm_behavior = self.confirm_behavior;
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let text = cx let text = cx
.background_spawn(fetch_url_content(http_client, url.clone())) .background_spawn(Self::build_message(http_client, url.clone()))
.await?; .await?;
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {

View File

@@ -1,17 +1,27 @@
use std::collections::BTreeSet;
use std::ops::Range;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use editor::actions::FoldAt;
use editor::display_map::{Crease, FoldId};
use editor::scroll::Autoscroll;
use editor::{Anchor, AnchorRangeExt, Editor, FoldPlaceholder, ToPoint};
use file_icons::FileIcons; use file_icons::FileIcons;
use fuzzy::PathMatch; use fuzzy::PathMatch;
use gpui::{ use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity, AnyElement, App, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful,
Task, WeakEntity,
}; };
use multi_buffer::{MultiBufferPoint, MultiBufferRow};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId}; use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{ListItem, Tooltip, prelude::*}; use rope::Point;
use text::SelectionGoal;
use ui::{prelude::*, ButtonLike, Disclosure, ListItem, TintColor, Tooltip};
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{Workspace, notifications::NotifyResultExt}; use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{ContextStore, FileInclusion}; use crate::context_store::{ContextStore, FileInclusion};
@@ -24,6 +34,7 @@ impl FileContextPicker {
pub fn new( pub fn new(
context_picker: WeakEntity<ContextPicker>, context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior, confirm_behavior: ConfirmBehavior,
window: &mut Window, window: &mut Window,
@@ -32,6 +43,7 @@ impl FileContextPicker {
let delegate = FileContextPickerDelegate::new( let delegate = FileContextPickerDelegate::new(
context_picker, context_picker,
workspace, workspace,
editor,
context_store, context_store,
confirm_behavior, confirm_behavior,
); );
@@ -56,6 +68,7 @@ impl Render for FileContextPicker {
pub struct FileContextPickerDelegate { pub struct FileContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>, context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior, confirm_behavior: ConfirmBehavior,
matches: Vec<PathMatch>, matches: Vec<PathMatch>,
@@ -66,18 +79,95 @@ impl FileContextPickerDelegate {
pub fn new( pub fn new(
context_picker: WeakEntity<ContextPicker>, context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior, confirm_behavior: ConfirmBehavior,
) -> Self { ) -> Self {
Self { Self {
context_picker, context_picker,
workspace, workspace,
editor,
context_store, context_store,
confirm_behavior, confirm_behavior,
matches: Vec::new(), matches: Vec::new(),
selected_index: 0, selected_index: 0,
} }
} }
fn search(
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &mut Context<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let recent_matches = workspace
.recent_navigation_history(Some(10), cx)
.into_iter()
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
})
});
Task::ready(recent_matches.chain(file_matches).collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Entries,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
} }
impl PickerDelegate for FileContextPickerDelegate { impl PickerDelegate for FileContextPickerDelegate {
@@ -114,7 +204,7 @@ impl PickerDelegate for FileContextPickerDelegate {
return Task::ready(()); return Task::ready(());
}; };
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx); let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background. // TODO: This should be probably be run in the background.
@@ -132,6 +222,14 @@ impl PickerDelegate for FileContextPickerDelegate {
return; return;
}; };
let file_name = mat
.path
.file_name()
.map(|os_str| os_str.to_string_lossy().into_owned())
.unwrap_or(mat.path_prefix.to_string());
let full_path = mat.path.display().to_string();
let project_path = ProjectPath { let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id), worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(), path: mat.path.clone(),
@@ -139,13 +237,106 @@ impl PickerDelegate for FileContextPickerDelegate {
let is_directory = mat.is_dir; let is_directory = mat.is_dir;
let Some(editor_entity) = self.editor.upgrade() else {
return;
};
editor_entity.update(cx, |editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
// Move empty selections left by 1 column to select the `@`s, so they get overwritten when we insert.
{
let mut selections = editor.selections.all::<MultiBufferPoint>(cx);
for selection in selections.iter_mut() {
if selection.is_empty() {
let old_head = selection.head();
let new_head = MultiBufferPoint::new(
old_head.row,
old_head.column.saturating_sub(1),
);
selection.set_head(new_head, SelectionGoal::None);
}
}
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select(selections)
});
}
let start_anchors = {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor
.selections
.all::<Point>(cx)
.into_iter()
.map(|selection| snapshot.anchor_before(selection.start))
.collect::<Vec<_>>()
};
editor.insert(&full_path, window, cx);
let end_anchors = {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor
.selections
.all::<Point>(cx)
.into_iter()
.map(|selection| snapshot.anchor_after(selection.end))
.collect::<Vec<_>>()
};
editor.insert("\n", window, cx); // Needed to end the fold
let file_icon = if is_directory {
FileIcons::get_folder_icon(false, cx)
} else {
FileIcons::get_icon(&Path::new(&full_path), cx)
}
.unwrap_or_else(|| SharedString::new(""));
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
file_icon,
file_name.into(),
editor_entity.downgrade(),
),
..Default::default()
};
let render_trailer =
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
let buffer = editor.buffer().read(cx).snapshot(cx);
let mut rows_to_fold = BTreeSet::new();
let crease_iter = start_anchors
.into_iter()
.zip(end_anchors)
.map(|(start, end)| {
rows_to_fold.insert(MultiBufferRow(start.to_point(&buffer).row));
Crease::inline(
start..end,
placeholder.clone(),
fold_toggle("tool-use"),
render_trailer,
)
});
editor.insert_creases(crease_iter, cx);
for buffer_row in rows_to_fold {
editor.fold_at(&FoldAt { buffer_row }, window, cx);
}
});
});
let Some(task) = self let Some(task) = self
.context_store .context_store
.update(cx, |context_store, cx| { .update(cx, |context_store, cx| {
if is_directory { if is_directory {
context_store.add_directory(project_path, true, cx) context_store.add_directory(project_path, cx)
} else { } else {
context_store.add_file_from_path(project_path, true, cx) context_store.add_file_from_path(project_path, cx)
} }
}) })
.ok() .ok()
@@ -199,116 +390,6 @@ impl PickerDelegate for FileContextPickerDelegate {
} }
} }
pub(crate) fn search_paths(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &App,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let recent_matches = workspace
.recent_navigation_history(Some(10), cx)
.into_iter()
.filter_map(|(project_path, _)| {
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix: worktree.read(cx).root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
});
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
})
});
Task::ready(recent_matches.chain(file_matches).collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Entries,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
pub fn extract_file_name_and_directory(
path: &Path,
path_prefix: &str,
) -> (SharedString, Option<SharedString>) {
if path == Path::new("") {
(
SharedString::from(
path_prefix
.trim_end_matches(std::path::MAIN_SEPARATOR)
.to_string(),
),
None,
)
} else {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = path_prefix
.trim_end_matches(std::path::MAIN_SEPARATOR)
.to_string();
if !directory.ends_with('/') {
directory.push('/');
}
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory.into()))
}
}
pub fn render_file_context_entry( pub fn render_file_context_entry(
id: ElementId, id: ElementId,
path: &Path, path: &Path,
@@ -317,11 +398,32 @@ pub fn render_file_context_entry(
context_store: WeakEntity<ContextStore>, context_store: WeakEntity<ContextStore>,
cx: &App, cx: &App,
) -> Stateful<Div> { ) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix); let (file_name, directory) = if path == Path::new("") {
(SharedString::from(path_prefix.clone()), None)
} else {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = format!("{}/", path_prefix);
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory))
};
let added = context_store.upgrade().and_then(|context_store| { let added = context_store.upgrade().and_then(|context_store| {
if is_directory { if is_directory {
context_store.read(cx).includes_directory(path) context_store
.read(cx)
.includes_directory(path)
.map(FileInclusion::Direct)
} else { } else {
context_store.read(cx).will_include_file_path(path, cx) context_store.read(cx).will_include_file_path(path, cx)
} }
@@ -382,3 +484,85 @@ pub fn render_file_context_entry(
} }
}) })
} }
fn render_fold_icon_button(
icon: SharedString,
label: SharedString,
editor: WeakEntity<Editor>,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new(move |fold_id, fold_range, cx| {
let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
editor.update(cx, |editor, cx| {
let snapshot = editor
.buffer()
.update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
let is_in_pending_selection = || {
editor
.selections
.pending
.as_ref()
.is_some_and(|pending_selection| {
pending_selection
.selection
.range()
.includes(&fold_range, &snapshot)
})
};
let mut is_in_complete_selection = || {
editor
.selections
.disjoint_in_range::<usize>(fold_range.clone(), cx)
.into_iter()
.any(|selection| {
// This is needed to cover a corner case, if we just check for an existing
// selection in the fold range, having a cursor at the start of the fold
// marks it as selected. Non-empty selections don't cause this.
let length = selection.end - selection.start;
length > 0
})
};
is_in_pending_selection() || is_in_complete_selection()
})
});
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(icon.clone())
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.single_line(),
),
)
.into_any_element()
})
}
fn fold_toggle(
name: &'static str,
) -> impl Fn(
MultiBufferRow,
bool,
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
&mut Window,
&mut App,
) -> AnyElement {
move |row, is_folded, fold, _window, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
.into_any_element()
}
}

View File

@@ -1,438 +0,0 @@
use std::cmp::Reverse;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::{Context as _, Result};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::{DocumentSymbol, Symbol};
use text::OffsetRangeExt;
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
pub struct SymbolContextPicker {
picker: Entity<Picker<SymbolContextPickerDelegate>>,
}
impl SymbolContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = SymbolContextPickerDelegate::new(
context_picker,
workspace,
context_store,
confirm_behavior,
);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for SymbolContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for SymbolContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct SymbolContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
matches: Vec<SymbolEntry>,
selected_index: usize,
}
impl SymbolContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
confirm_behavior: ConfirmBehavior,
) -> Self {
Self {
context_picker,
workspace,
context_store,
confirm_behavior,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for SymbolContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search symbols…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
let context_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| {
let symbols = search_task
.await
.context("Failed to load symbols")
.log_err()
.unwrap_or_default();
let symbol_entries = context_store
.read_with(cx, |context_store, cx| {
compute_symbol_entries(symbols, context_store, cx)
})
.log_err()
.unwrap_or_default();
this.update(cx, |this, _cx| {
this.delegate.matches = symbol_entries;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let confirm_behavior = self.confirm_behavior;
let add_symbol_task = add_symbol(
mat.symbol.clone(),
true,
workspace,
self.context_store.clone(),
cx,
);
let selected_index = self.selected_index;
cx.spawn_in(window, async move |this, cx| {
let included = add_symbol_task.await?;
this.update_in(cx, |this, window, cx| {
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
mat.is_included = included;
}
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
}
})
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_symbol_context_entry(
ElementId::NamedInteger("symbol-ctx-picker".into(), ix),
mat,
),
))
}
}
pub(crate) struct SymbolEntry {
pub symbol: Symbol,
pub is_included: bool,
}
pub(crate) fn add_symbol(
symbol: Symbol,
remove_if_exists: bool,
workspace: Entity<Workspace>,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Task<Result<bool>> {
let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| {
project.open_buffer(symbol.path.clone(), cx)
});
cx.spawn(async move |cx| {
let buffer = open_buffer_task.await?;
let document_symbols = project
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
.await?;
// Try to find a matching document symbol. Document symbols include
// not only the symbol itself (e.g. function name), but they also
// include the context that they contain (e.g. function body).
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
name,
range,
selection_range,
..
}) =
find_matching_symbol(&symbol, document_symbols.as_slice())
{
(name, selection_range, range)
} else {
// If we do not find a matching document symbol, fall back to
// just the symbol itself
(symbol.name, symbol.range.clone(), symbol.range)
};
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
(
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
buffer.anchor_after(enclosing_range.start)
..buffer.anchor_before(enclosing_range.end),
)
})?;
context_store
.update(cx, move |context_store, cx| {
context_store.add_symbol(
buffer,
name.into(),
range,
enclosing_range,
remove_if_exists,
cx,
)
})?
.await
})
}
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
let mut candidates = candidates.iter();
let mut candidate = candidates.next()?;
loop {
if candidate.range.start > symbol.range.end {
return None;
}
if candidate.range.end < symbol.range.start {
candidate = candidates.next()?;
continue;
}
if candidate.selection_range == symbol.range {
return Some(candidate.clone());
}
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
candidates = candidate.children.iter();
candidate = candidates.next()?;
continue;
}
return None;
}
}
pub(crate) fn search_symbols(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Task<Result<Vec<(StringMatch, Symbol)>>> {
let symbols_task = workspace.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| project.symbols(&query, cx))
});
let project = workspace.read(cx).project().clone();
cx.spawn(async move |cx| {
let symbols = symbols_task.await?;
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
.update(cx, |project, cx| {
symbols
.iter()
.enumerate()
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text()))
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.map_or(false, |e| !e.is_ignored)
})
})?;
const MAX_MATCHES: usize = 100;
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
&visible_match_candidates,
&query,
false,
MAX_MATCHES,
&cancellation_flag,
cx.background_executor().clone(),
));
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
&external_match_candidates,
&query,
false,
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
&cancellation_flag,
cx.background_executor().clone(),
));
let sort_key_for_match = |mat: &StringMatch| {
let symbol = &symbols[mat.candidate_id];
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
};
visible_matches.sort_unstable_by_key(sort_key_for_match);
external_matches.sort_unstable_by_key(sort_key_for_match);
let mut matches = visible_matches;
matches.append(&mut external_matches);
Ok(matches
.into_iter()
.map(|mut mat| {
let symbol = symbols[mat.candidate_id].clone();
let filter_start = symbol.label.filter_range.start;
for position in &mut mat.positions {
*position += filter_start;
}
(mat, symbol)
})
.collect())
})
}
fn compute_symbol_entries(
symbols: Vec<(StringMatch, Symbol)>,
context_store: &ContextStore,
cx: &App,
) -> Vec<SymbolEntry> {
let mut symbol_entries = Vec::with_capacity(symbols.len());
for (_, symbol) in symbols {
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
let is_included = if let Some(symbols_for_path) = symbols_for_path {
let mut is_included = false;
for included_symbol_id in symbols_for_path {
if included_symbol_id.name.as_ref() == symbol.name.as_str() {
if let Some(buffer) = context_store.buffer_for_symbol(included_symbol_id) {
let snapshot = buffer.read(cx).snapshot();
let included_symbol_range =
included_symbol_id.range.to_point_utf16(&snapshot);
if included_symbol_range.start == symbol.range.start.0
&& included_symbol_range.end == symbol.range.end.0
{
is_included = true;
break;
}
}
}
}
is_included
} else {
false
};
symbol_entries.push(SymbolEntry {
symbol,
is_included,
})
}
symbol_entries
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
let path = entry
.symbol
.path
.path
.file_name()
.map(|s| s.to_string_lossy())
.unwrap_or_default();
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
h_flex()
.id(id)
.gap_1p5()
.w_full()
.child(
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.gap_1()
.child(Label::new(&entry.symbol.name))
.child(
Label::new(symbol_location)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.when(entry.is_included, |el| {
el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use ui::{ListItem, prelude::*}; use ui::{prelude::*, ListItem};
use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{self, ContextStore}; use crate::context_store::{self, ContextStore};
@@ -110,11 +110,45 @@ impl PickerDelegate for ThreadContextPickerDelegate {
window: &mut Window, window: &mut Window,
cx: &mut Context<Picker<Self>>, cx: &mut Context<Picker<Self>>,
) -> Task<()> { ) -> Task<()> {
let Some(threads) = self.thread_store.upgrade() else { let Ok(threads) = self.thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
.collect::<Vec<_>>()
}) else {
return Task::ready(()); return Task::ready(());
}; };
let search_task = search_threads(query, threads, cx); let executor = cx.background_executor().clone();
let search_task = cx.background_spawn(async move {
if query.is_empty() {
threads
} else {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
});
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await; let matches = search_task.await;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
@@ -142,9 +176,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.delegate this.delegate
.context_store .context_store
.update(cx, |context_store, cx| { .update(cx, |context_store, cx| context_store.add_thread(thread, cx))
context_store.add_thread(thread, true, cx)
})
.ok(); .ok();
match this.delegate.confirm_behavior { match this.delegate.confirm_behavior {
@@ -197,7 +229,7 @@ pub fn render_thread_context_entry(
.gap_1p5() .gap_1p5()
.max_w_72() .max_w_72()
.child( .child(
Icon::new(IconName::MessageBubbles) Icon::new(IconName::MessageCircle)
.size(IconSize::XSmall) .size(IconSize::XSmall)
.color(Color::Muted), .color(Color::Muted),
) )
@@ -216,46 +248,3 @@ pub fn render_thread_context_entry(
) )
}) })
} }
pub(crate) fn search_threads(
query: String,
thread_store: Entity<ThreadStore>,
cx: &mut App,
) -> Task<Vec<ThreadContextEntry>> {
let threads = thread_store.update(cx, |this, _cx| {
this.threads()
.into_iter()
.map(|thread| ThreadContextEntry {
id: thread.id,
summary: thread.summary,
})
.collect::<Vec<_>>()
});
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
if query.is_empty() {
threads
} else {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
})
}

View File

@@ -1,21 +1,20 @@
use std::ops::Range;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{anyhow, bail, Result};
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use futures::{self, Future, FutureExt, future}; use futures::{self, future, Future, FutureExt};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity}; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
use language::{Buffer, File}; use language::Buffer;
use project::{ProjectItem, ProjectPath, Worktree}; use project::{ProjectPath, Worktree};
use rope::Rope; use rope::Rope;
use text::{Anchor, BufferId, OffsetRangeExt}; use text::BufferId;
use util::{ResultExt, maybe}; use util::maybe;
use workspace::Workspace; use workspace::Workspace;
use crate::context::{ use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext, AssistantContext, ContextBuffer, ContextId, ContextSnapshot, DirectoryContext,
FetchedUrlContext, FileContext, SymbolContext, ThreadContext, FetchedUrlContext, FileContext, ThreadContext,
}; };
use crate::context_strip::SuggestedContext; use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId}; use crate::thread::{Thread, ThreadId};
@@ -27,9 +26,6 @@ pub struct ContextStore {
next_context_id: ContextId, next_context_id: ContextId,
files: BTreeMap<BufferId, ContextId>, files: BTreeMap<BufferId, ContextId>,
directories: HashMap<PathBuf, ContextId>, directories: HashMap<PathBuf, ContextId>,
symbols: HashMap<ContextSymbolId, ContextId>,
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
threads: HashMap<ThreadId, ContextId>, threads: HashMap<ThreadId, ContextId>,
fetched_urls: HashMap<String, ContextId>, fetched_urls: HashMap<String, ContextId>,
} }
@@ -42,20 +38,19 @@ impl ContextStore {
next_context_id: ContextId(0), next_context_id: ContextId(0),
files: BTreeMap::default(), files: BTreeMap::default(),
directories: HashMap::default(), directories: HashMap::default(),
symbols: HashMap::default(),
symbol_buffers: HashMap::default(),
symbols_by_path: HashMap::default(),
threads: HashMap::default(), threads: HashMap::default(),
fetched_urls: HashMap::default(), fetched_urls: HashMap::default(),
} }
} }
pub fn context(&self) -> &Vec<AssistantContext> { pub fn snapshot<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = ContextSnapshot> + 'a {
&self.context self.context()
.iter()
.flat_map(|context| context.snapshot(cx))
} }
pub fn context_for_id(&self, id: ContextId) -> Option<&AssistantContext> { pub fn context(&self) -> &Vec<AssistantContext> {
self.context().iter().find(|context| context.id() == id) &self.context
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
@@ -69,7 +64,6 @@ impl ContextStore {
pub fn add_file_from_path( pub fn add_file_from_path(
&mut self, &mut self,
project_path: ProjectPath, project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
@@ -92,9 +86,7 @@ impl ContextStore {
let already_included = this.update(cx, |this, _cx| { let already_included = this.update(cx, |this, _cx| {
match this.will_include_buffer(buffer_id, &project_path.path) { match this.will_include_buffer(buffer_id, &project_path.path) {
Some(FileInclusion::Direct(context_id)) => { Some(FileInclusion::Direct(context_id)) => {
if remove_if_exists { this.remove_context(context_id);
this.remove_context(context_id);
}
true true
} }
Some(FileInclusion::InDirectory(_)) => true, Some(FileInclusion::InDirectory(_)) => true,
@@ -112,10 +104,9 @@ impl ContextStore {
project_path.path.clone(), project_path.path.clone(),
buffer_entity, buffer_entity,
buffer, buffer,
None,
cx.to_async(), cx.to_async(),
) )
})??; })?;
let text = text_task.await; let text = text_task.await;
@@ -138,13 +129,12 @@ impl ContextStore {
let Some(file) = buffer.file() else { let Some(file) = buffer.file() else {
return Err(anyhow!("Buffer has no path.")); return Err(anyhow!("Buffer has no path."));
}; };
collect_buffer_info_and_text( Ok(collect_buffer_info_and_text(
file.path().clone(), file.path().clone(),
buffer_entity, buffer_entity,
buffer, buffer,
None,
cx.to_async(), cx.to_async(),
) ))
})??; })??;
let text = text_task.await; let text = text_task.await;
@@ -160,16 +150,13 @@ impl ContextStore {
fn insert_file(&mut self, context_buffer: ContextBuffer) { fn insert_file(&mut self, context_buffer: ContextBuffer) {
let id = self.next_context_id.post_inc(); let id = self.next_context_id.post_inc();
self.files.insert(context_buffer.id, id); self.files.insert(context_buffer.id, id);
self.context.push(AssistantContext::File(FileContext { self.context
id, .push(AssistantContext::File(FileContext { id, context_buffer }));
context_buffer: context_buffer,
}));
} }
pub fn add_directory( pub fn add_directory(
&mut self, &mut self,
project_path: ProjectPath, project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
@@ -180,15 +167,12 @@ impl ContextStore {
return Task::ready(Err(anyhow!("failed to read project"))); return Task::ready(Err(anyhow!("failed to read project")));
}; };
let already_included = match self.includes_directory(&project_path.path) { let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
Some(FileInclusion::Direct(context_id)) => { {
if remove_if_exists { self.remove_context(context_id);
self.remove_context(context_id); true
} } else {
true false
}
Some(FileInclusion::InDirectory(_)) => true,
None => false,
}; };
if already_included { if already_included {
return Task::ready(Ok(())); return Task::ready(Ok(()));
@@ -228,18 +212,14 @@ impl ContextStore {
// Skip all binary files and other non-UTF8 files // Skip all binary files and other non-UTF8 files
if let Ok(buffer_entity) = buffer_entity { if let Ok(buffer_entity) = buffer_entity {
let buffer = buffer_entity.read(cx); let buffer = buffer_entity.read(cx);
if let Some((buffer_info, text_task)) = collect_buffer_info_and_text( let (buffer_info, text_task) = collect_buffer_info_and_text(
path, path,
buffer_entity, buffer_entity,
buffer, buffer,
None,
cx.to_async(), cx.to_async(),
) );
.log_err() buffer_infos.push(buffer_info);
{ text_tasks.push(text_task);
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
}
} }
} }
anyhow::Ok(()) anyhow::Ok(())
@@ -253,123 +233,32 @@ impl ContextStore {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if context_buffers.is_empty() { if context_buffers.is_empty() {
return Err(anyhow!( bail!("No text files found in {}", &project_path.path.display());
"No text files found in {}",
&project_path.path.display()
));
} }
this.update(cx, |this, _| { this.update(cx, |this, _| {
this.insert_directory(project_path, context_buffers); this.insert_directory(&project_path.path, context_buffers);
})?; })?;
anyhow::Ok(()) anyhow::Ok(())
}) })
} }
fn insert_directory(&mut self, project_path: ProjectPath, context_buffers: Vec<ContextBuffer>) { fn insert_directory(&mut self, path: &Path, context_buffers: Vec<ContextBuffer>) {
let id = self.next_context_id.post_inc(); let id = self.next_context_id.post_inc();
self.directories.insert(project_path.path.to_path_buf(), id); self.directories.insert(path.to_path_buf(), id);
self.context self.context
.push(AssistantContext::Directory(DirectoryContext { .push(AssistantContext::Directory(DirectoryContext::new(
id, id,
project_path, path,
context_buffers, context_buffers,
})); )));
} }
pub fn add_symbol( pub fn add_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
&mut self,
buffer: Entity<Buffer>,
symbol_name: SharedString,
symbol_range: Range<Anchor>,
symbol_enclosing_range: Range<Anchor>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<bool>> {
let buffer_ref = buffer.read(cx);
let Some(file) = buffer_ref.file() else {
return Task::ready(Err(anyhow!("Buffer has no path.")));
};
let Some(project_path) = buffer_ref.project_path(cx) else {
return Task::ready(Err(anyhow!("Buffer has no project path.")));
};
if let Some(symbols_for_path) = self.symbols_by_path.get(&project_path) {
let mut matching_symbol_id = None;
for symbol in symbols_for_path {
if &symbol.name == &symbol_name {
let snapshot = buffer_ref.snapshot();
if symbol.range.to_offset(&snapshot) == symbol_range.to_offset(&snapshot) {
matching_symbol_id = self.symbols.get(symbol).cloned();
break;
}
}
}
if let Some(id) = matching_symbol_id {
if remove_if_exists {
self.remove_context(id);
}
return Task::ready(Ok(false));
}
}
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text(
file.path().clone(),
buffer,
buffer_ref,
Some(symbol_enclosing_range.clone()),
cx.to_async(),
) {
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
Err(err) => return Task::ready(Err(err)),
};
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
this.update(cx, |this, _cx| {
this.insert_symbol(make_context_symbol(
buffer_info,
project_path,
symbol_name,
symbol_range,
symbol_enclosing_range,
content,
))
})?;
anyhow::Ok(true)
})
}
fn insert_symbol(&mut self, context_symbol: ContextSymbol) {
let id = self.next_context_id.post_inc();
self.symbols.insert(context_symbol.id.clone(), id);
self.symbols_by_path
.entry(context_symbol.id.path.clone())
.or_insert_with(Vec::new)
.push(context_symbol.id.clone());
self.symbol_buffers
.insert(context_symbol.id.clone(), context_symbol.buffer.clone());
self.context.push(AssistantContext::Symbol(SymbolContext {
id,
context_symbol,
}));
}
pub fn add_thread(
&mut self,
thread: Entity<Thread>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) {
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) { if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
if remove_if_exists { self.remove_context(context_id);
self.remove_context(context_id);
}
} else { } else {
self.insert_thread(thread, cx); self.insert_thread(thread, cx);
} }
@@ -438,19 +327,6 @@ impl ContextStore {
AssistantContext::Directory(_) => { AssistantContext::Directory(_) => {
self.directories.retain(|_, context_id| *context_id != id); self.directories.retain(|_, context_id| *context_id != id);
} }
AssistantContext::Symbol(symbol) => {
if let Some(symbols_in_path) =
self.symbols_by_path.get_mut(&symbol.context_symbol.id.path)
{
symbols_in_path.retain(|s| {
self.symbols
.get(s)
.map_or(false, |context_id| *context_id != id)
});
}
self.symbol_buffers.remove(&symbol.context_symbol.id);
self.symbols.retain(|_, context_id| *context_id != id);
}
AssistantContext::FetchedUrl(_) => { AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id); self.fetched_urls.retain(|_, context_id| *context_id != id);
} }
@@ -478,7 +354,7 @@ impl ContextStore {
let found_file_context = self.context.iter().find(|context| match &context { let found_file_context = self.context.iter().find(|context| match &context {
AssistantContext::File(file_context) => { AssistantContext::File(file_context) => {
let buffer = file_context.context_buffer.buffer.read(cx); let buffer = file_context.context_buffer.buffer.read(cx);
if let Some(file_path) = buffer_path_log_err(buffer, cx) { if let Some(file_path) = buffer_path_log_err(buffer) {
*file_path == *path *file_path == *path
} else { } else {
false false
@@ -510,24 +386,8 @@ impl ContextStore {
None None
} }
pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> { pub fn includes_directory(&self, path: &Path) -> Option<ContextId> {
if let Some(context_id) = self.directories.get(path) { self.directories.get(path).copied()
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(path)
}
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
self.symbols.get(symbol_id).copied()
}
pub fn included_symbols_by_path(&self) -> &HashMap<ProjectPath, Vec<ContextSymbolId>> {
&self.symbols_by_path
}
pub fn buffer_for_symbol(&self, symbol_id: &ContextSymbolId) -> Option<Entity<Buffer>> {
self.symbol_buffers.get(symbol_id).cloned()
} }
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> { pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
@@ -555,10 +415,9 @@ impl ContextStore {
.filter_map(|context| match context { .filter_map(|context| match context {
AssistantContext::File(file) => { AssistantContext::File(file) => {
let buffer = file.context_buffer.buffer.read(cx); let buffer = file.context_buffer.buffer.read(cx);
buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf()) buffer_path_log_err(buffer).map(|p| p.to_path_buf())
} }
AssistantContext::Directory(_) AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
| AssistantContext::FetchedUrl(_) | AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_) => None, | AssistantContext::Thread(_) => None,
}) })
@@ -578,7 +437,6 @@ pub enum FileInclusion {
// ContextBuffer without text. // ContextBuffer without text.
struct BufferInfo { struct BufferInfo {
buffer_entity: Entity<Buffer>, buffer_entity: Entity<Buffer>,
file: Arc<dyn File>,
id: BufferId, id: BufferId,
version: clock::Global, version: clock::Global,
} }
@@ -587,62 +445,31 @@ fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
ContextBuffer { ContextBuffer {
id: info.id, id: info.id,
buffer: info.buffer_entity, buffer: info.buffer_entity,
file: info.file,
version: info.version, version: info.version,
text, text,
} }
} }
fn make_context_symbol(
info: BufferInfo,
path: ProjectPath,
name: SharedString,
range: Range<Anchor>,
enclosing_range: Range<Anchor>,
text: SharedString,
) -> ContextSymbol {
ContextSymbol {
id: ContextSymbolId { name, range, path },
buffer_version: info.version,
enclosing_range,
buffer: info.buffer_entity,
text,
}
}
fn collect_buffer_info_and_text( fn collect_buffer_info_and_text(
path: Arc<Path>, path: Arc<Path>,
buffer_entity: Entity<Buffer>, buffer_entity: Entity<Buffer>,
buffer: &Buffer, buffer: &Buffer,
range: Option<Range<Anchor>>,
cx: AsyncApp, cx: AsyncApp,
) -> Result<(BufferInfo, Task<SharedString>)> { ) -> (BufferInfo, Task<SharedString>) {
let buffer_info = BufferInfo { let buffer_info = BufferInfo {
id: buffer.remote_id(), id: buffer.remote_id(),
buffer_entity, buffer_entity,
file: buffer
.file()
.context("buffer context must have a file")?
.clone(),
version: buffer.version(), version: buffer.version(),
}; };
// Important to collect version at the same time as content so that staleness logic is correct. // Important to collect version at the same time as content so that staleness logic is correct.
let content = if let Some(range) = range { let content = buffer.as_rope().clone();
buffer.text_for_range(range).collect::<Rope>()
} else {
buffer.as_rope().clone()
};
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) }); let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
Ok((buffer_info, text_task)) (buffer_info, text_task)
} }
pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> { pub fn buffer_path_log_err(buffer: &Buffer) -> Option<Arc<Path>> {
if let Some(file) = buffer.file() { if let Some(file) = buffer.file() {
let mut path = file.path().clone(); Some(file.path().clone())
if path.as_os_str().is_empty() {
path = file.full_path(cx).into();
}
Some(path)
} else { } else {
log::error!("Buffer that had a path unexpectedly no longer has a path."); log::error!("Buffer that had a path unexpectedly no longer has a path.");
None None
@@ -707,7 +534,7 @@ pub fn refresh_context_store_text(
context_store: Entity<ContextStore>, context_store: Entity<ContextStore>,
changed_buffers: &HashSet<Entity<Buffer>>, changed_buffers: &HashSet<Entity<Buffer>>,
cx: &App, cx: &App,
) -> impl Future<Output = Vec<ContextId>> + use<> { ) -> impl Future<Output = Vec<ContextId>> {
let mut tasks = Vec::new(); let mut tasks = Vec::new();
for context in &context_store.read(cx).context { for context in &context_store.read(cx).context {
@@ -728,9 +555,8 @@ pub fn refresh_context_store_text(
|| changed_buffers.iter().any(|buffer| { || changed_buffers.iter().any(|buffer| {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
buffer_path_log_err(&buffer, cx).map_or(false, |path| { buffer_path_log_err(&buffer)
path.starts_with(&directory_context.project_path.path) .map_or(false, |path| path.starts_with(&directory_context.path))
})
}); });
if should_refresh { if should_refresh {
@@ -738,14 +564,6 @@ pub fn refresh_context_store_text(
return refresh_directory_text(context_store, directory_context, cx); return refresh_directory_text(context_store, directory_context, cx);
} }
} }
AssistantContext::Symbol(symbol_context) => {
if changed_buffers.is_empty()
|| changed_buffers.contains(&symbol_context.context_symbol.buffer)
{
let context_store = context_store.clone();
return refresh_symbol_text(context_store, symbol_context, cx);
}
}
AssistantContext::Thread(thread_context) => { AssistantContext::Thread(thread_context) => {
if changed_buffers.is_empty() { if changed_buffers.is_empty() {
let context_store = context_store.clone(); let context_store = context_store.clone();
@@ -816,45 +634,19 @@ fn refresh_directory_text(
let context_buffers = future::join_all(futures); let context_buffers = future::join_all(futures);
let id = directory_context.id; let id = directory_context.snapshot.id;
let project_path = directory_context.project_path.clone(); let path = directory_context.path.clone();
Some(cx.spawn(async move |cx| { Some(cx.spawn(async move |cx| {
let context_buffers = context_buffers.await; let context_buffers = context_buffers.await;
context_store context_store
.update(cx, |context_store, _| { .update(cx, |context_store, _| {
let new_directory_context = DirectoryContext { let new_directory_context = DirectoryContext::new(id, &path, context_buffers);
id,
project_path,
context_buffers,
};
context_store.replace_context(AssistantContext::Directory(new_directory_context)); context_store.replace_context(AssistantContext::Directory(new_directory_context));
}) })
.ok(); .ok();
})) }))
} }
fn refresh_symbol_text(
context_store: Entity<ContextStore>,
symbol_context: &SymbolContext,
cx: &App,
) -> Option<Task<()>> {
let id = symbol_context.id;
let task = refresh_context_symbol(&symbol_context.context_symbol, cx);
if let Some(task) = task {
Some(cx.spawn(async move |cx| {
let context_symbol = task.await;
context_store
.update(cx, |context_store, _| {
let new_symbol_context = SymbolContext { id, context_symbol };
context_store.replace_context(AssistantContext::Symbol(new_symbol_context));
})
.ok();
}))
} else {
None
}
}
fn refresh_thread_text( fn refresh_thread_text(
context_store: Entity<ContextStore>, context_store: Entity<ContextStore>,
thread_context: &ThreadContext, thread_context: &ThreadContext,
@@ -879,54 +671,18 @@ fn refresh_thread_text(
fn refresh_context_buffer( fn refresh_context_buffer(
context_buffer: &ContextBuffer, context_buffer: &ContextBuffer,
cx: &App, cx: &App,
) -> Option<impl Future<Output = ContextBuffer> + use<>> { ) -> Option<impl Future<Output = ContextBuffer>> {
let buffer = context_buffer.buffer.read(cx); let buffer = context_buffer.buffer.read(cx);
let path = buffer_path_log_err(buffer, cx)?; let path = buffer_path_log_err(buffer)?;
if buffer.version.changed_since(&context_buffer.version) { if buffer.version.changed_since(&context_buffer.version) {
let (buffer_info, text_task) = collect_buffer_info_and_text( let (buffer_info, text_task) = collect_buffer_info_and_text(
path, path,
context_buffer.buffer.clone(), context_buffer.buffer.clone(),
buffer, buffer,
None,
cx.to_async(), cx.to_async(),
) );
.log_err()?;
Some(text_task.map(move |text| make_context_buffer(buffer_info, text))) Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
} else { } else {
None None
} }
} }
fn refresh_context_symbol(
context_symbol: &ContextSymbol,
cx: &App,
) -> Option<impl Future<Output = ContextSymbol> + use<>> {
let buffer = context_symbol.buffer.read(cx);
let path = buffer_path_log_err(buffer, cx)?;
let project_path = buffer.project_path(cx)?;
if buffer.version.changed_since(&context_symbol.buffer_version) {
let (buffer_info, text_task) = collect_buffer_info_and_text(
path,
context_symbol.buffer.clone(),
buffer,
Some(context_symbol.enclosing_range.clone()),
cx.to_async(),
)
.log_err()?;
let name = context_symbol.id.name.clone();
let range = context_symbol.id.range.clone();
let enclosing_range = context_symbol.enclosing_range.clone();
Some(text_task.map(move |text| {
make_context_symbol(
buffer_info,
project_path,
name,
range,
enclosing_range,
text,
)
}))
} else {
None
}
}

View File

@@ -4,20 +4,20 @@ use collections::HashSet;
use editor::Editor; use editor::Editor;
use file_icons::FileIcons; use file_icons::FileIcons;
use gpui::{ use gpui::{
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
Subscription, WeakEntity, WeakEntity,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::Buffer; use language::Buffer;
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::{Workspace, notifications::NotifyResultExt}; use workspace::{notifications::NotifyResultExt, Workspace};
use crate::context::{ContextId, ContextKind}; use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::thread::Thread; use crate::thread::Thread;
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::ui::{AddedContext, ContextPill}; use crate::ui::ContextPill;
use crate::{ use crate::{
AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp, AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
@@ -39,6 +39,7 @@ impl ContextStrip {
pub fn new( pub fn new(
context_store: Entity<ContextStore>, context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
editor: WeakEntity<Editor>,
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: Option<WeakEntity<ThreadStore>>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>, context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
suggest_context_kind: SuggestContextKind, suggest_context_kind: SuggestContextKind,
@@ -50,6 +51,7 @@ impl ContextStrip {
workspace.clone(), workspace.clone(),
thread_store.clone(), thread_store.clone(),
context_store.downgrade(), context_store.downgrade(),
editor.clone(),
ConfirmBehavior::KeepOpen, ConfirmBehavior::KeepOpen,
window, window,
cx, cx,
@@ -92,12 +94,12 @@ impl ContextStrip {
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?; let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
let active_buffer = active_buffer_entity.read(cx); let active_buffer = active_buffer_entity.read(cx);
let path = active_buffer.file()?.full_path(cx); let path = active_buffer.file()?.path();
if self if self
.context_store .context_store
.read(cx) .read(cx)
.will_include_buffer(active_buffer.remote_id(), &path) .will_include_buffer(active_buffer.remote_id(), path)
.is_some() .is_some()
{ {
return None; return None;
@@ -108,7 +110,7 @@ impl ContextStrip {
None => path.to_string_lossy().into_owned().into(), None => path.to_string_lossy().into_owned().into(),
}; };
let icon_path = FileIcons::get_icon(&path, cx); let icon_path = FileIcons::get_icon(path, cx);
Some(SuggestedContext::File { Some(SuggestedContext::File {
name, name,
@@ -239,7 +241,11 @@ impl ContextStrip {
let eraser = if bounds.len() < 3 { 0 } else { 1 }; let eraser = if bounds.len() < 3 { 0 } else { 1 };
let pills = &bounds[1..bounds.len() - eraser]; let pills = &bounds[1..bounds.len() - eraser];
if pills.is_empty() { None } else { Some(pills) } if pills.is_empty() {
None
} else {
Some(pills)
}
} }
fn last_pill_index(&self) -> Option<usize> { fn last_pill_index(&self) -> Option<usize> {
@@ -273,14 +279,6 @@ impl ContextStrip {
best.map(|(index, _, _)| index) best.map(|(index, _, _)| index)
} }
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
}
fn remove_focused_context( fn remove_focused_context(
&mut self, &mut self,
_: &RemoveFocusedContext, _: &RemoveFocusedContext,
@@ -363,19 +361,19 @@ impl Focusable for ContextStrip {
impl Render for ContextStrip { impl Render for ContextStrip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let context_store = self.context_store.read(cx); let context_store = self.context_store.read(cx);
let context = context_store.context(); let context = context_store
.context()
.iter()
.flat_map(|context| context.snapshot(cx))
.collect::<Vec<_>>();
let context_picker = self.context_picker.clone(); let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone(); let focus_handle = self.focus_handle.clone();
let suggested_context = self.suggested_context(cx); let suggested_context = self.suggested_context(cx);
let added_contexts = context let dupe_names = context
.iter() .iter()
.map(|c| AddedContext::new(c, cx)) .map(|context| context.name.clone())
.collect::<Vec<_>>();
let dupe_names = added_contexts
.iter()
.map(|c| c.name.clone())
.sorted() .sorted()
.tuple_windows() .tuple_windows()
.filter(|(a, b)| a == b) .filter(|(a, b)| a == b)
@@ -461,39 +459,27 @@ impl Render for ContextStrip {
) )
} }
}) })
.children( .children(context.iter().enumerate().map(|(i, context)| {
added_contexts ContextPill::added(
.into_iter() context.clone(),
.enumerate() dupe_names.contains(&context.name),
.map(|(i, added_context)| { self.focused_index == Some(i),
let name = added_context.name.clone(); Some({
let id = added_context.id; let id = context.id;
ContextPill::added( let context_store = self.context_store.clone();
added_context, Rc::new(cx.listener(move |_this, _event, _window, cx| {
dupe_names.contains(&name), context_store.update(cx, |this, _cx| {
self.focused_index == Some(i), this.remove_context(id);
Some({ });
let context_store = self.context_store.clone(); cx.notify();
Rc::new(cx.listener(move |_this, _event, _window, cx| { }))
context_store.update(cx, |this, _cx| {
this.remove_context(id);
});
cx.notify();
}))
}),
)
.on_click({
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
if event.down.click_count > 1 {
this.open_context(id, window, cx);
} else {
this.focused_index = Some(i);
}
cx.notify();
}))
})
}), }),
) )
.on_click(Rc::new(cx.listener(move |this, _, _window, cx| {
this.focused_index = Some(i);
cx.notify();
})))
}))
.when_some(suggested_context, |el, suggested| { .when_some(suggested_context, |el, suggested| {
el.child( el.child(
ContextPill::suggested( ContextPill::suggested(

View File

@@ -1,6 +1,6 @@
use assistant_context_editor::SavedContextMetadata; use assistant_context_editor::SavedContextMetadata;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use gpui::{Entity, prelude::*}; use gpui::{prelude::*, Entity};
use crate::thread_store::{SerializedThreadMetadata, ThreadStore}; use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
@@ -43,11 +43,6 @@ impl HistoryStore {
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> { pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
let mut history_entries = Vec::new(); let mut history_entries = Vec::new();
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return history_entries;
}
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) { for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
history_entries.push(HistoryEntry::Thread(thread)); history_entries.push(HistoryEntry::Thread(thread));
} }

View File

@@ -7,24 +7,24 @@ use std::sync::Arc;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map}; use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{ use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
actions::SelectAll, actions::SelectAll,
display_map::{ display_map::{
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
ToDisplayPoint, ToDisplayPoint,
}, },
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
}; };
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _}; use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal, point, App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task,
WeakEntity, Window, point, UpdateGlobal, WeakEntity, Window,
}; };
use language::{Buffer, Point, Selection, TransactionId}; use language::{Buffer, Point, Selection, TransactionId};
use language_model::{LanguageModelRegistry, report_assistant_event}; use language_model::{report_assistant_event, LanguageModelRegistry};
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::LspAction; use project::LspAction;
@@ -32,20 +32,20 @@ use project::{CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use text::{OffsetRangeExt, ToPoint as _}; use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*; use ui::prelude::*;
use util::RangeExt; use util::RangeExt;
use util::ResultExt; use util::ResultExt;
use workspace::{ItemHandle, Toast, Workspace, notifications::NotificationId}; use workspace::{dock::Panel, ShowConfiguration};
use workspace::{ShowConfiguration, dock::Panel}; use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
use crate::AssistantPanel;
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent}; use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
use crate::context_store::ContextStore; use crate::context_store::ContextStore;
use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent}; use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
use crate::terminal_inline_assistant::TerminalInlineAssistant; use crate::terminal_inline_assistant::TerminalInlineAssistant;
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::AssistantPanel;
pub fn init( pub fn init(
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@@ -324,7 +324,7 @@ impl InlineAssistant {
) { ) {
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| { let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
( (
editor.snapshot(window, cx), editor.buffer().read(cx).snapshot(cx),
editor.selections.all::<Point>(cx), editor.selections.all::<Point>(cx),
) )
}); });
@@ -338,37 +338,7 @@ impl InlineAssistant {
if selection.end.column == 0 { if selection.end.column == 0 {
selection.end.row -= 1; selection.end.row -= 1;
} }
selection.end.column = snapshot selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
let chars = snapshot
.buffer_snapshot
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
if c == '\n' {
break;
}
if c.is_whitespace() {
continue;
}
if snapshot
.language_at(selection.end)
.is_some_and(|language| language.config().brackets.is_closing_brace(c))
{
selection.end.row += 1;
selection.end.column = snapshot
.buffer_snapshot
.line_len(MultiBufferRow(selection.end.row));
}
}
}
} }
if let Some(prev_selection) = selections.last_mut() { if let Some(prev_selection) = selections.last_mut() {
@@ -384,7 +354,6 @@ impl InlineAssistant {
} }
selections.push(selection); selections.push(selection);
} }
let snapshot = &snapshot.buffer_snapshot;
let newest_selection = newest_selection.unwrap(); let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new(); let mut codegen_ranges = Vec::new();

View File

@@ -10,14 +10,14 @@ use crate::{RemoveAllContext, ToggleContextPicker};
use client::ErrorExt; use client::ErrorExt;
use collections::VecDeque; use collections::VecDeque;
use editor::{ use editor::{
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
actions::{MoveDown, MoveUp}, actions::{MoveDown, MoveUp},
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
}; };
use feature_flags::{FeatureFlagAppExt as _, ZedPro}; use feature_flags::{FeatureFlagAppExt as _, ZedPro};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle, anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point, EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
}; };
use language_model::{LanguageModel, LanguageModelRegistry}; use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector; use language_model_selector::ToggleModelSelector;
@@ -28,7 +28,7 @@ use std::sync::Arc;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::utils::WithRemSize; use ui::utils::WithRemSize;
use ui::{ use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*, prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
}; };
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@@ -455,55 +455,47 @@ impl<T: 'static> PromptEditor<T> {
match codegen_status { match codegen_status {
CodegenStatus::Idle => { CodegenStatus::Idle => {
vec![ vec![Button::new("start", mode.start_label())
Button::new("start", mode.start_label()) .label_size(LabelSize::Small)
.label_size(LabelSize::Small) .icon(IconName::Return)
.icon(IconName::Return) .icon_size(IconSize::XSmall)
.icon_size(IconSize::XSmall) .icon_color(Color::Muted)
.icon_color(Color::Muted) .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
.on_click( .into_any_element()]
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
)
.into_any_element(),
]
} }
CodegenStatus::Pending => vec![ CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
IconButton::new("stop", IconName::Stop) .icon_color(Color::Error)
.icon_color(Color::Error) .shape(IconButtonShape::Square)
.shape(IconButtonShape::Square) .tooltip(move |window, cx| {
.tooltip(move |window, cx| { Tooltip::with_meta(
Tooltip::with_meta( mode.tooltip_interrupt(),
mode.tooltip_interrupt(), Some(&menu::Cancel),
Some(&menu::Cancel), "Changes won't be discarded",
"Changes won't be discarded", window,
window, cx,
cx, )
) })
}) .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested))) .into_any_element()],
.into_any_element(),
],
CodegenStatus::Done | CodegenStatus::Error(_) => { CodegenStatus::Done | CodegenStatus::Error(_) => {
let has_error = matches!(codegen_status, CodegenStatus::Error(_)); let has_error = matches!(codegen_status, CodegenStatus::Error(_));
if has_error || self.edited_since_done { if has_error || self.edited_since_done {
vec![ vec![IconButton::new("restart", IconName::RotateCw)
IconButton::new("restart", IconName::RotateCw) .icon_color(Color::Info)
.icon_color(Color::Info) .shape(IconButtonShape::Square)
.shape(IconButtonShape::Square) .tooltip(move |window, cx| {
.tooltip(move |window, cx| { Tooltip::with_meta(
Tooltip::with_meta( mode.tooltip_restart(),
mode.tooltip_restart(), Some(&menu::Confirm),
Some(&menu::Confirm), "Changes will be discarded",
"Changes will be discarded", window,
window, cx,
cx, )
) })
}) .on_click(cx.listener(|_, _, _, cx| {
.on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::StartRequested);
cx.emit(PromptEditorEvent::StartRequested); }))
})) .into_any_element()]
.into_any_element(),
]
} else { } else {
let accept = IconButton::new("accept", IconName::Check) let accept = IconButton::new("accept", IconName::Check)
.icon_color(Color::Info) .icon_color(Color::Info)
@@ -869,6 +861,7 @@ impl PromptEditor<BufferCodegen> {
ContextStrip::new( ContextStrip::new(
context_store.clone(), context_store.clone(),
workspace.clone(), workspace.clone(),
prompt_editor.downgrade(),
thread_store.clone(), thread_store.clone(),
context_picker_menu_handle.clone(), context_picker_menu_handle.clone(),
SuggestContextKind::Thread, SuggestContextKind::Thread,
@@ -1021,6 +1014,7 @@ impl PromptEditor<TerminalCodegen> {
ContextStrip::new( ContextStrip::new(
context_store.clone(), context_store.clone(),
workspace.clone(), workspace.clone(),
prompt_editor.downgrade(),
thread_store.clone(), thread_store.clone(),
context_picker_menu_handle.clone(), context_picker_menu_handle.clone(),
SuggestContextKind::Thread, SuggestContextKind::Thread,

View File

@@ -2,43 +2,42 @@ use std::sync::Arc;
use collections::HashSet; use collections::HashSet;
use editor::actions::MoveUp; use editor::actions::MoveUp;
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use file_icons::FileIcons;
use fs::Fs; use fs::Fs;
use git::ExpandCommitEditor;
use git_ui::git_panel;
use gpui::{ use gpui::{
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, WeakEntity,
}; };
use language_model::LanguageModelRegistry; use language_model::LanguageModelRegistry;
use language_model_selector::ToggleModelSelector; use language_model_selector::ToggleModelSelector;
use project::Project; use project::Project;
use rope::Point;
use settings::Settings; use settings::Settings;
use std::time::Duration; use std::time::Duration;
use text::Bias;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
}; };
use util::ResultExt; use util::ResultExt;
use vim_mode_setting::VimModeSetting; use vim_mode_setting::VimModeSetting;
use workspace::Workspace; use workspace::notifications::{NotificationId, NotifyTaskExt};
use workspace::{Toast, Workspace};
use crate::assistant_model_selector::AssistantModelSelector; use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider}; use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::{ContextStore, refresh_context_store_text}; use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{RequestKind, Thread}; use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use crate::{ use crate::tool_selector::ToolSelector;
AssistantDiff, Chat, ChatMode, OpenAssistantDiff, RemoveAllContext, ThreadEvent, use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
ToggleContextPicker, ToggleProfileSelector,
};
pub struct MessageEditor { pub struct MessageEditor {
thread: Entity<Thread>, thread: Entity<Thread>,
editor: Entity<Editor>, editor: Entity<Editor>,
#[allow(dead_code)]
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
project: Entity<Project>, project: Entity<Project>,
context_store: Entity<ContextStore>, context_store: Entity<ContextStore>,
@@ -47,8 +46,7 @@ pub struct MessageEditor {
inline_context_picker: Entity<ContextPicker>, inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>, inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>, model_selector: Entity<AssistantModelSelector>,
profile_selector: Entity<ProfileSelector>, tool_selector: Entity<ToolSelector>,
edits_expanded: bool,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@@ -62,6 +60,7 @@ impl MessageEditor {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let tools = thread.read(cx).tools().clone();
let context_picker_menu_handle = PopoverMenuHandle::default(); let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default(); let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default(); let model_selector_menu_handle = PopoverMenuHandle::default();
@@ -70,30 +69,16 @@ impl MessageEditor {
let mut editor = Editor::auto_height(10, window, cx); let mut editor = Editor::auto_height(10, window, cx);
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx); editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
editor.set_show_indent_guides(false, cx); editor.set_show_indent_guides(false, cx);
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
placement: Some(ContextMenuPlacement::Above),
});
editor editor
}); });
let editor_entity = editor.downgrade();
editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
Some(thread_store.clone()),
editor_entity,
))));
});
let inline_context_picker = cx.new(|cx| { let inline_context_picker = cx.new(|cx| {
ContextPicker::new( ContextPicker::new(
workspace.clone(), workspace.clone(),
Some(thread_store.clone()), Some(thread_store.clone()),
context_store.downgrade(), context_store.downgrade(),
editor.downgrade(),
ConfirmBehavior::Close, ConfirmBehavior::Close,
window, window,
cx, cx,
@@ -104,6 +89,7 @@ impl MessageEditor {
ContextStrip::new( ContextStrip::new(
context_store.clone(), context_store.clone(),
workspace.clone(), workspace.clone(),
editor.downgrade(),
Some(thread_store.clone()), Some(thread_store.clone()),
context_picker_menu_handle.clone(), context_picker_menu_handle.clone(),
SuggestContextKind::File, SuggestContextKind::File,
@@ -113,6 +99,7 @@ impl MessageEditor {
}); });
let subscriptions = vec![ let subscriptions = vec![
cx.subscribe_in(&editor, window, Self::handle_editor_event),
cx.subscribe_in( cx.subscribe_in(
&inline_context_picker, &inline_context_picker,
window, window,
@@ -133,16 +120,14 @@ impl MessageEditor {
inline_context_picker_menu_handle, inline_context_picker_menu_handle,
model_selector: cx.new(|cx| { model_selector: cx.new(|cx| {
AssistantModelSelector::new( AssistantModelSelector::new(
fs.clone(), fs,
model_selector_menu_handle, model_selector_menu_handle,
editor.focus_handle(cx), editor.focus_handle(cx),
window, window,
cx, cx,
) )
}), }),
edits_expanded: false, tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)),
profile_selector: cx
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
@@ -159,6 +144,7 @@ impl MessageEditor {
) { ) {
self.context_picker_menu_handle.toggle(window, cx); self.context_picker_menu_handle.toggle(window, cx);
} }
pub fn remove_all_context( pub fn remove_all_context(
&mut self, &mut self,
_: &RemoveAllContext, _: &RemoveAllContext,
@@ -220,29 +206,16 @@ impl MessageEditor {
let refresh_task = let refresh_task =
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx); refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
let system_prompt_context_task = self.thread.read(cx).load_system_prompt_context(cx);
let thread = self.thread.clone(); let thread = self.thread.clone();
let context_store = self.context_store.clone(); let context_store = self.context_store.clone();
let checkpoint = self.project.read(cx).git_store().read(cx).checkpoint(cx); let git_store = self.project.read(cx).git_store();
let checkpoint = git_store.read(cx).checkpoint(cx);
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
let checkpoint = checkpoint.await.ok();
refresh_task.await; refresh_task.await;
let (system_prompt_context, load_error) = system_prompt_context_task.await; let checkpoint = checkpoint.await.log_err();
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.set_system_prompt_context(system_prompt_context); let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
if let Some(load_error) = load_error {
cx.emit(ThreadEvent::ShowError(load_error));
}
})
.ok();
thread
.update(cx, |thread, cx| {
let context = context_store.read(cx).context().clone();
thread.action_log().update(cx, |action_log, cx| {
action_log.clear_reviewed_changes(cx);
});
thread.insert_user_message(user_message, context, checkpoint, cx); thread.insert_user_message(user_message, context, checkpoint, cx);
thread.send_to_model(model, request_kind, cx); thread.send_to_model(model, request_kind, cx);
}) })
@@ -251,6 +224,34 @@ impl MessageEditor {
.detach(); .detach();
} }
fn handle_editor_event(
&mut self,
editor: &Entity<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
EditorEvent::SelectionsChanged { .. } => {
editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0 {
let behind_cursor = snapshot.clip_point(
Point::new(newest_cursor.row, newest_cursor.column - 1),
Bias::Left,
);
let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
if char_behind_cursor == Some('@') {
self.inline_context_picker_menu_handle.show(window, cx);
}
}
});
}
_ => {}
}
}
fn handle_inline_context_picker_event( fn handle_inline_context_picker_event(
&mut self, &mut self,
_inline_context_picker: &Entity<ContextPicker>, _inline_context_picker: &Entity<ContextPicker>,
@@ -290,8 +291,32 @@ impl MessageEditor {
} }
} }
fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) { fn handle_feedback_click(
AssistantDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err(); &mut self,
is_positive: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let workspace = self.workspace.clone();
let report = self
.thread
.update(cx, |thread, cx| thread.report_feedback(is_positive, cx));
cx.spawn(async move |_, cx| {
report.await?;
workspace.update(cx, |workspace, cx| {
let message = if is_positive {
"Positive feedback recorded. Thank you!"
} else {
"Negative feedback recorded. Thank you for helping us improve!"
};
struct ThreadFeedback;
let id = NotificationId::unique::<ThreadFeedback>();
workspace.show_toast(Toast::new(id, message).autohide(), cx)
})
})
.detach_and_notify_err(window, cx);
} }
} }
@@ -305,10 +330,9 @@ impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx); let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.5; let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
let focus_handle = self.editor.focus_handle(cx); let focus_handle = self.editor.focus_handle(cx);
let inline_context_picker = self.inline_context_picker.clone(); let inline_context_picker = self.inline_context_picker.clone();
let bg_color = cx.theme().colors().editor_background;
let is_generating = self.thread.read(cx).is_generating(); let is_generating = self.thread.read(cx).is_generating();
let is_model_selected = self.is_model_selected(cx); let is_model_selected = self.is_model_selected(cx);
let is_editor_empty = self.is_editor_empty(cx); let is_editor_empty = self.is_editor_empty(cx);
@@ -328,14 +352,12 @@ impl Render for MessageEditor {
px(64.) px(64.)
}; };
let action_log = self.thread.read(cx).action_log(); let project = self.thread.read(cx).project();
let changed_buffers = action_log.read(cx).changed_buffers(cx); let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
let changed_buffers_count = changed_buffers.len(); repository.read(cx).status().count()
} else {
let editor_bg_color = cx.theme().colors().editor_background; 0
let border_color = cx.theme().colors().border; };
let active_color = cx.theme().colors().element_selected;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
v_flex() v_flex()
.size_full() .size_full()
@@ -348,7 +370,7 @@ impl Render for MessageEditor {
.pl_2() .pl_2()
.pr_1() .pr_1()
.py_1() .py_1()
.bg(editor_bg_color) .bg(cx.theme().colors().editor_background)
.border_1() .border_1()
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.rounded_lg() .rounded_lg()
@@ -397,213 +419,77 @@ impl Render for MessageEditor {
), ),
) )
}) })
.when(changed_buffers_count > 0, |parent| { .when(changed_files > 0, |parent| {
parent.child( parent.child(
v_flex() v_flex()
.mx_2() .mx_2()
.bg(bg_edit_files_disclosure) .bg(cx.theme().colors().element_background)
.border_1() .border_1()
.border_b_0() .border_b_0()
.border_color(border_color) .border_color(cx.theme().colors().border)
.rounded_t_md() .rounded_t_md()
.shadow(smallvec::smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.15),
offset: point(px(1.), px(-1.)),
blur_radius: px(3.),
spread_radius: px(0.),
}])
.child( .child(
h_flex() h_flex()
.p_1p5()
.justify_between() .justify_between()
.when(self.edits_expanded, |this| { .p_2()
this.border_b_1().border_color(border_color)
})
.child( .child(
h_flex() h_flex()
.gap_1() .gap_2()
.child( .child(
Disclosure::new( IconButton::new(
"edits-disclosure", "edits-disclosure",
self.edits_expanded, IconName::GitBranchSmall,
) )
.icon_size(IconSize::Small)
.on_click( .on_click(
cx.listener(|this, _ev, _window, cx| { |_ev, _window, cx| {
this.edits_expanded = !this.edits_expanded; cx.defer(|cx| {
cx.notify(); cx.dispatch_action(&git_panel::ToggleFocus)
}), });
},
), ),
) )
.child(
Label::new("Edits")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new("")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child( .child(
Label::new(format!( Label::new(format!(
"{} {}", "{} {} changed",
changed_buffers_count, changed_files,
if changed_buffers_count == 1 { if changed_files == 1 { "file" } else { "files" }
"file"
} else {
"files"
}
)) ))
.size(LabelSize::Small) .size(LabelSize::XSmall)
.color(Color::Muted), .color(Color::Muted),
), ),
) )
.child( .child(
Button::new("review", "Review Changes") h_flex()
.label_size(LabelSize::Small) .gap_2()
.key_binding( .child(
KeyBinding::for_action_in( Button::new("review", "Review")
&OpenAssistantDiff, .label_size(LabelSize::XSmall)
&focus_handle, .on_click(|_event, _window, cx| {
window, cx.defer(|cx| {
cx, cx.dispatch_action(
) &git_ui::project_diff::Diff,
.map(|kb| kb.size(rems_from_px(12.))), );
});
}),
) )
.on_click(cx.listener(|this, _, window, cx| { .child(
this.handle_review_click(window, cx) Button::new("commit", "Commit")
})), .label_size(LabelSize::XSmall)
.on_click(|_event, _window, cx| {
cx.defer(|cx| {
cx.dispatch_action(&ExpandCommitEditor)
});
}),
),
), ),
) ),
.when(self.edits_expanded, |parent| {
parent.child(
v_flex().bg(cx.theme().colors().editor_background).children(
changed_buffers.into_iter().enumerate().flat_map(
|(index, (buffer, changed))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let parent_label = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
None
} else {
Some(
Label::new(format!(
"{}{}",
parent_str,
std::path::MAIN_SEPARATOR_STR
))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
)
}
});
let name_label = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
});
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.map(|icon| {
icon.color(Color::Muted).size(IconSize::Small)
})
.unwrap_or_else(|| {
Icon::new(IconName::File)
.color(Color::Muted)
.size(IconSize::Small)
});
let element = div()
.relative()
.py_1()
.px_2()
.when(index + 1 < changed_buffers_count, |parent| {
parent.border_color(border_color).border_b_1()
})
.child(
h_flex()
.gap_2()
.justify_between()
.child(
h_flex()
.id("file-container")
.pr_8()
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(file_icon)
.child(
h_flex()
.children(parent_label)
.children(name_label),
) // TODO: show lines changed
.child(
Label::new("+")
.color(Color::Created),
)
.child(
Label::new("-")
.color(Color::Deleted),
),
)
.when(!changed.needs_review, |parent| {
parent.child(
Icon::new(IconName::Check)
.color(Color::Success),
)
})
.child(
div()
.h_full()
.absolute()
.w_8()
.bottom_0()
.map(|this| {
if !changed.needs_review {
this.right_4()
} else {
this.right_0()
}
})
.bg(linear_gradient(
90.,
linear_color_stop(
editor_bg_color,
1.,
),
linear_color_stop(
editor_bg_color
.opacity(0.2),
0.,
),
)),
),
);
Some(element)
},
),
),
)
}),
) )
}) })
.child( .child(
v_flex() v_flex()
.key_context("MessageEditor") .key_context("MessageEditor")
.on_action(cx.listener(Self::chat)) .on_action(cx.listener(Self::chat))
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
this.profile_selector
.read(cx)
.menu_handle()
.toggle(window, cx);
}))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx)); .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
@@ -614,10 +500,48 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::toggle_chat_mode)) .on_action(cx.listener(Self::toggle_chat_mode))
.gap_2() .gap_2()
.p_2() .p_2()
.bg(editor_bg_color) .bg(bg_color)
.border_t_1() .border_t_1()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.child(h_flex().justify_between().child(self.context_strip.clone())) .child(
h_flex()
.justify_between()
.child(self.context_strip.clone())
.when(!self.thread.read(cx).is_empty(), |this| {
this.child(
h_flex()
.gap_2()
.child(
IconButton::new(
"feedback-thumbs-up",
IconName::ThumbsUp,
)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Helpful"))
.on_click(
cx.listener(|this, _, window, cx| {
this.handle_feedback_click(true, window, cx);
}),
),
)
.child(
IconButton::new(
"feedback-thumbs-down",
IconName::ThumbsDown,
)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Not Helpful"))
.on_click(
cx.listener(|this, _, window, cx| {
this.handle_feedback_click(false, window, cx);
}),
),
),
)
}),
)
.child( .child(
v_flex() v_flex()
.gap_5() .gap_5()
@@ -637,10 +561,9 @@ impl Render for MessageEditor {
EditorElement::new( EditorElement::new(
&self.editor, &self.editor,
EditorStyle { EditorStyle {
background: editor_bg_color, background: bg_color,
local_player: cx.theme().players().local(), local_player: cx.theme().players().local(),
text: text_style, text: text_style,
syntax: cx.theme().syntax().clone(),
..Default::default() ..Default::default()
}, },
) )
@@ -666,7 +589,7 @@ impl Render for MessageEditor {
.child( .child(
h_flex() h_flex()
.justify_between() .justify_between()
.child(h_flex().gap_2().child(self.profile_selector.clone())) .child(h_flex().gap_2().child(self.tool_selector.clone()))
.child( .child(
h_flex().gap_1().child(self.model_selector.clone()).child( h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("submit-message") ButtonLike::new("submit-message")

View File

@@ -1,172 +0,0 @@
use std::sync::Arc;
use assistant_settings::{AgentProfile, AssistantSettings};
use fs::Fs;
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
use indexmap::IndexMap;
use settings::{Settings as _, SettingsStore, update_settings_file};
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle,
prelude::*,
};
use util::ResultExt as _;
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
pub struct ProfileSelector {
profiles: IndexMap<Arc<str>, AgentProfile>,
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
menu_handle: PopoverMenuHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
this.refresh_profiles(cx);
});
let mut this = Self {
profiles: IndexMap::default(),
fs,
thread_store,
focus_handle,
menu_handle: PopoverMenuHandle::default(),
_subscriptions: vec![settings_subscription],
};
this.refresh_profiles(cx);
this
}
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
let settings = AssistantSettings::get_global(cx);
self.profiles = settings.profiles.clone();
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AssistantSettings::get_global(cx);
let icon_position = IconPosition::End;
menu = menu.header("Profiles");
for (profile_id, profile) in self.profiles.clone() {
menu = menu.toggleable_entry(
profile.name.clone(),
profile_id == settings.default_profile,
icon_position,
None,
{
let fs = self.fs.clone();
let thread_store = self.thread_store.clone();
move |_window, cx| {
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings.set_profile(profile_id.clone());
}
});
thread_store
.update(cx, |this, cx| {
this.load_profile_by_id(&profile_id, cx);
})
.log_err();
}
},
);
}
menu = menu.separator();
menu = menu.header("Customize Current Profile");
menu = menu.item(ContextMenuEntry::new("Tools…").handler({
let profile_id = settings.default_profile.clone();
move |window, cx| {
window.dispatch_action(
ManageProfiles::customize_tools(profile_id.clone()).boxed_clone(),
cx,
);
}
}));
menu = menu.separator();
menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
move |window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
},
));
menu
})
}
}
impl Render for ProfileSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let profile_id = &settings.default_profile;
let profile = settings.profiles.get(profile_id);
let selected_profile = profile
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let icon = match profile_id.as_ref() {
"write" => IconName::Pencil,
"ask" => IconName::MessageBubbles,
_ => IconName::UserRoundPen,
};
let this = cx.entity().clone();
let focus_handle = self.focus_handle.clone();
PopoverMenu::new("profile-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger(
ButtonLike::new("profile-selector-button").child(
h_flex()
.gap_1()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(
Label::new(selected_profile)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Icon::new(IconName::ChevronDown)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(div().opacity(0.5).children({
let focus_handle = focus_handle.clone();
KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.)))
})),
),
)
.anchor(gpui::Corner::BottomLeft)
.with_handle(self.menu_handle.clone())
}
}

View File

@@ -1,8 +1,8 @@
use crate::inline_prompt_editor::CodegenStatus; use crate::inline_prompt_editor::CodegenStatus;
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
use futures::{SinkExt, StreamExt, channel::mpsc}; use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task}; use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
use language_model::{LanguageModelRegistry, LanguageModelRequest, report_assistant_event}; use language_model::{report_assistant_event, LanguageModelRegistry, LanguageModelRequest};
use std::{sync::Arc, time::Instant}; use std::{sync::Arc, time::Instant};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal; use terminal::Terminal;

View File

@@ -3,18 +3,18 @@ use crate::context_store::ContextStore;
use crate::inline_prompt_editor::{ use crate::inline_prompt_editor::{
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId, CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
}; };
use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen}; use crate::terminal_codegen::{CodegenEvent, TerminalCodegen, CLEAR_INPUT};
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use client::telemetry::Telemetry; use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque}; use collections::{HashMap, VecDeque};
use editor::{MultiBuffer, actions::SelectAll}; use editor::{actions::SelectAll, MultiBuffer};
use fs::Fs; use fs::Fs;
use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity}; use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity};
use language::Buffer; use language::Buffer;
use language_model::{ use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
report_assistant_event, LanguageModelRequestMessage, Role,
}; };
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use std::sync::Arc; use std::sync::Arc;
@@ -22,7 +22,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::TerminalView; use terminal_view::TerminalView;
use ui::prelude::*; use ui::prelude::*;
use util::ResultExt; use util::ResultExt;
use workspace::{Toast, Workspace, notifications::NotificationId}; use workspace::{notifications::NotificationId, Toast, Workspace};
pub fn init( pub fn init(
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@@ -252,8 +252,7 @@ impl TerminalInlineAssistant {
attach_context_to_message( attach_context_to_message(
&mut request_message, &mut request_message,
assist.context_store.read(cx).context().iter(), assist.context_store.read(cx).snapshot(cx),
cx,
); );
request_message.content.push(prompt.into()); request_message.content.push(prompt.into());

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
use assistant_context_editor::SavedContextMetadata; use assistant_context_editor::SavedContextMetadata;
use gpui::{ use gpui::{
App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle, WeakEntity, uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle,
uniform_list, WeakEntity,
}; };
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{IconButtonShape, ListItem, ListItemSpacing, Tooltip, prelude::*}; use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
use crate::history_store::{HistoryEntry, HistoryStore}; use crate::history_store::{HistoryEntry, HistoryStore};
use crate::thread_store::SerializedThreadMetadata; use crate::thread_store::SerializedThreadMetadata;

View File

@@ -1,29 +1,26 @@
use std::borrow::Cow;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use assistant_settings::{AgentProfile, AssistantSettings}; use assistant_tool::{ToolId, ToolWorkingSet};
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use collections::HashMap; use collections::HashMap;
use context_server::manager::ContextServerManager; use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool}; use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use futures::FutureExt as _;
use futures::future::{self, BoxFuture, Shared}; use futures::future::{self, BoxFuture, Shared};
use futures::FutureExt as _;
use gpui::{ use gpui::{
App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task, prelude::*, prelude::*, App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task,
}; };
use heed::types::{SerdeBincode, SerdeJson};
use heed::Database; use heed::Database;
use heed::types::SerdeBincode; use language_model::{LanguageModelToolUseId, Role};
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
use project::Project; use project::Project;
use prompt_store::PromptBuilder; use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings as _;
use util::ResultExt as _; use util::ResultExt as _;
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId}; use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadId};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
ThreadsDatabase::init(cx); ThreadsDatabase::init(cx);
@@ -59,7 +56,6 @@ impl ThreadStore {
context_server_tool_ids: HashMap::default(), context_server_tool_ids: HashMap::default(),
threads: Vec::new(), threads: Vec::new(),
}; };
this.load_default_profile(cx);
this.register_context_server_handlers(cx); this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx); this.reload(cx).detach_and_log_err(cx);
@@ -117,7 +113,7 @@ impl ThreadStore {
.await? .await?
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?; .ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
let thread = this.update(cx, |this, cx| { this.update(cx, |this, cx| {
cx.new(|cx| { cx.new(|cx| {
Thread::deserialize( Thread::deserialize(
id.clone(), id.clone(),
@@ -128,19 +124,7 @@ impl ThreadStore {
cx, cx,
) )
}) })
})?; })
let (system_prompt_context, load_error) = thread
.update(cx, |thread, cx| thread.load_system_prompt_context(cx))?
.await;
thread.update(cx, |thread, cx| {
thread.set_system_prompt_context(system_prompt_context);
if let Some(load_error) = load_error {
cx.emit(ThreadEvent::ShowError(load_error));
}
})?;
Ok(thread)
}) })
} }
@@ -187,45 +171,6 @@ impl ThreadStore {
}) })
} }
fn load_default_profile(&self, cx: &Context<Self>) {
let assistant_settings = AssistantSettings::get_global(cx);
self.load_profile_by_id(&assistant_settings.default_profile, cx);
}
pub fn load_profile_by_id(&self, profile_id: &Arc<str>, cx: &Context<Self>) {
let assistant_settings = AssistantSettings::get_global(cx);
if let Some(profile) = assistant_settings.profiles.get(profile_id) {
self.load_profile(profile);
}
}
pub fn load_profile(&self, profile: &AgentProfile) {
self.tools.disable_all_tools();
self.tools.enable(
ToolSource::Native,
&profile
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
);
for (context_server_id, preset) in &profile.context_servers {
self.tools.enable(
ToolSource::ContextServer {
id: context_server_id.clone().into(),
},
&preset
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
)
}
}
fn register_context_server_handlers(&self, cx: &mut Context<Self>) { fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
cx.subscribe( cx.subscribe(
&self.context_server_manager.clone(), &self.context_server_manager.clone(),
@@ -302,65 +247,24 @@ pub struct SerializedThreadMetadata {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct SerializedThread { pub struct SerializedThread {
pub version: String,
pub summary: SharedString, pub summary: SharedString,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub messages: Vec<SerializedMessage>, pub messages: Vec<SerializedMessage>,
#[serde(default)] #[serde(default)]
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>, pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
#[serde(default)]
pub cumulative_token_usage: TokenUsage,
}
impl SerializedThread {
pub const VERSION: &'static str = "0.1.0";
pub fn from_json(json: &[u8]) -> Result<Self> {
let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?;
match saved_thread_json.get("version") {
Some(serde_json::Value::String(version)) => match version.as_str() {
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
saved_thread_json,
)?),
_ => Err(anyhow!(
"unrecognized serialized thread version: {}",
version
)),
},
None => {
let saved_thread =
serde_json::from_value::<LegacySerializedThread>(saved_thread_json)?;
Ok(saved_thread.upgrade())
}
version => Err(anyhow!(
"unrecognized serialized thread version: {:?}",
version
)),
}
}
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct SerializedMessage { pub struct SerializedMessage {
pub id: MessageId, pub id: MessageId,
pub role: Role, pub role: Role,
#[serde(default)] pub text: String,
pub segments: Vec<SerializedMessageSegment>,
#[serde(default)] #[serde(default)]
pub tool_uses: Vec<SerializedToolUse>, pub tool_uses: Vec<SerializedToolUse>,
#[serde(default)] #[serde(default)]
pub tool_results: Vec<SerializedToolResult>, pub tool_results: Vec<SerializedToolResult>,
} }
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "thinking")]
Thinking { text: String },
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct SerializedToolUse { pub struct SerializedToolUse {
pub id: LanguageModelToolUseId, pub id: LanguageModelToolUseId,
@@ -375,51 +279,6 @@ pub struct SerializedToolResult {
pub content: Arc<str>, pub content: Arc<str>,
} }
#[derive(Serialize, Deserialize)]
struct LegacySerializedThread {
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
pub messages: Vec<LegacySerializedMessage>,
#[serde(default)]
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
}
impl LegacySerializedThread {
pub fn upgrade(self) -> SerializedThread {
SerializedThread {
version: SerializedThread::VERSION.to_string(),
summary: self.summary,
updated_at: self.updated_at,
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
initial_project_snapshot: self.initial_project_snapshot,
cumulative_token_usage: TokenUsage::default(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct LegacySerializedMessage {
pub id: MessageId,
pub role: Role,
pub text: String,
#[serde(default)]
pub tool_uses: Vec<SerializedToolUse>,
#[serde(default)]
pub tool_results: Vec<SerializedToolResult>,
}
impl LegacySerializedMessage {
fn upgrade(self) -> SerializedMessage {
SerializedMessage {
id: self.id,
role: self.role,
segments: vec![SerializedMessageSegment::Text { text: self.text }],
tool_uses: self.tool_uses,
tool_results: self.tool_results,
}
}
}
struct GlobalThreadsDatabase( struct GlobalThreadsDatabase(
Shared<BoxFuture<'static, Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>, Shared<BoxFuture<'static, Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>,
); );
@@ -429,25 +288,7 @@ impl Global for GlobalThreadsDatabase {}
pub(crate) struct ThreadsDatabase { pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor, executor: BackgroundExecutor,
env: heed::Env, env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerializedThread>, threads: Database<SerdeBincode<ThreadId>, SerdeJson<SerializedThread>>,
}
impl heed::BytesEncode<'_> for SerializedThread {
type EItem = SerializedThread;
fn bytes_encode(item: &Self::EItem) -> Result<Cow<[u8]>, heed::BoxedError> {
serde_json::to_vec(item).map(Cow::Owned).map_err(Into::into)
}
}
impl<'a> heed::BytesDecode<'a> for SerializedThread {
type DItem = SerializedThread;
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
// We implement this type manually because we want to call `SerializedThread::from_json`,
// instead of the Deserialize trait implementation for `SerializedThread`.
SerializedThread::from_json(bytes).map_err(Into::into)
}
} }
impl ThreadsDatabase { impl ThreadsDatabase {

View File

@@ -0,0 +1,156 @@
use std::sync::Arc;
use assistant_tool::{ToolSource, ToolWorkingSet};
use gpui::Entity;
use scripting_tool::ScriptingTool;
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use crate::agent_profile::AgentProfile;
pub struct ToolSelector {
profiles: Vec<AgentProfile>,
tools: Arc<ToolWorkingSet>,
}
impl ToolSelector {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
Self {
profiles: vec![AgentProfile::read_only(), AgentProfile::code_writer()],
tools,
}
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let profiles = self.profiles.clone();
let tool_set = self.tools.clone();
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
let icon_position = IconPosition::End;
menu = menu.header("Profiles");
for profile in profiles.clone() {
menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
let tools = tool_set.clone();
move |_window, cx| {
tools.disable_source(ToolSource::Native, cx);
tools.enable(
ToolSource::Native,
&profile
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
);
}
});
}
menu = menu.separator();
let tools_by_source = tool_set.tools_by_source(cx);
let all_tools_enabled = tool_set.are_all_tools_enabled();
menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, {
let tools = tool_set.clone();
move |_window, cx| {
if all_tools_enabled {
tools.disable_all_tools(cx);
} else {
tools.enable_all_tools();
}
}
});
for (source, tools) in tools_by_source {
let mut tools = tools
.into_iter()
.map(|tool| {
let source = tool.source();
let name = tool.name().into();
let is_enabled = tool_set.is_enabled(&source, &name);
(source, name, is_enabled)
})
.collect::<Vec<_>>();
if ToolSource::Native == source {
tools.push((
ToolSource::Native,
ScriptingTool::NAME.into(),
tool_set.is_scripting_tool_enabled(),
));
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
}
menu = match &source {
ToolSource::Native => menu.separator().header("Zed Tools"),
ToolSource::ContextServer { id } => {
let all_tools_from_source_enabled =
tool_set.are_all_tools_from_source_enabled(&source);
menu.separator().header(id).toggleable_entry(
"All Tools",
all_tools_from_source_enabled,
icon_position,
None,
{
let tools = tool_set.clone();
let source = source.clone();
move |_window, cx| {
if all_tools_from_source_enabled {
tools.disable_source(source.clone(), cx);
} else {
tools.enable_source(&source);
}
}
},
)
}
};
for (source, name, is_enabled) in tools {
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
let tools = tool_set.clone();
move |_window, _cx| {
if name.as_ref() == ScriptingTool::NAME {
if is_enabled {
tools.disable_scripting_tool();
} else {
tools.enable_scripting_tool();
}
} else {
if is_enabled {
tools.disable(source.clone(), &[name.clone()]);
} else {
tools.enable(source.clone(), &[name.clone()]);
}
}
}
});
}
}
menu
})
}
}
impl Render for ToolSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let this = cx.entity().clone();
PopoverMenu::new("tool-selector")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.trigger_with_tooltip(
IconButton::new("tool-selector-button", IconName::SettingsAlt)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
Tooltip::text("Customize Tools"),
)
.anchor(gpui::Corner::BottomLeft)
}
}

View File

@@ -1,16 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use assistant_tool::{Tool, ToolWorkingSet};
use collections::HashMap; use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared; use futures::future::Shared;
use gpui::{App, SharedString, Task}; use futures::FutureExt as _;
use gpui::{SharedString, Task};
use language_model::{ use language_model::{
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role, LanguageModelToolUseId, MessageContent, Role,
}; };
use ui::IconName;
use crate::thread::MessageId; use crate::thread::MessageId;
use crate::thread_store::SerializedMessage; use crate::thread_store::SerializedMessage;
@@ -19,16 +17,12 @@ use crate::thread_store::SerializedMessage;
pub struct ToolUse { pub struct ToolUse {
pub id: LanguageModelToolUseId, pub id: LanguageModelToolUseId,
pub name: SharedString, pub name: SharedString,
pub ui_text: SharedString,
pub status: ToolUseStatus, pub status: ToolUseStatus,
pub input: serde_json::Value, pub input: serde_json::Value,
pub icon: ui::IconName,
pub needs_confirmation: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ToolUseStatus { pub enum ToolUseStatus {
NeedsConfirmation,
Pending, Pending,
Running, Running,
Finished(SharedString), Finished(SharedString),
@@ -36,7 +30,6 @@ pub enum ToolUseStatus {
} }
pub struct ToolUseState { pub struct ToolUseState {
tools: Arc<ToolWorkingSet>,
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>, tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>, tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>, tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
@@ -44,9 +37,8 @@ pub struct ToolUseState {
} }
impl ToolUseState { impl ToolUseState {
pub fn new(tools: Arc<ToolWorkingSet>) -> Self { pub fn new() -> Self {
Self { Self {
tools,
tool_uses_by_assistant_message: HashMap::default(), tool_uses_by_assistant_message: HashMap::default(),
tool_uses_by_user_message: HashMap::default(), tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(), tool_results: HashMap::default(),
@@ -58,11 +50,10 @@ impl ToolUseState {
/// ///
/// Accepts a function to filter the tools that should be used to populate the state. /// Accepts a function to filter the tools that should be used to populate the state.
pub fn from_serialized_messages( pub fn from_serialized_messages(
tools: Arc<ToolWorkingSet>,
messages: &[SerializedMessage], messages: &[SerializedMessage],
mut filter_by_tool_name: impl FnMut(&str) -> bool, mut filter_by_tool_name: impl FnMut(&str) -> bool,
) -> Self { ) -> Self {
let mut this = Self::new(tools); let mut this = Self::new();
let mut tool_names_by_id = HashMap::default(); let mut tool_names_by_id = HashMap::default();
for message in messages { for message in messages {
@@ -113,7 +104,6 @@ impl ToolUseState {
tool_use_id.clone(), tool_use_id.clone(),
LanguageModelToolResult { LanguageModelToolResult {
tool_use_id, tool_use_id,
tool_name: tool_use.clone(),
is_error: tool_result.is_error, is_error: tool_result.is_error,
content: tool_result.content.clone(), content: tool_result.content.clone(),
}, },
@@ -135,7 +125,6 @@ impl ToolUseState {
tool_use_id.clone(), tool_use_id.clone(),
LanguageModelToolResult { LanguageModelToolResult {
tool_use_id, tool_use_id,
tool_name: tool_use.name.clone(),
content: "Tool canceled by user".into(), content: "Tool canceled by user".into(),
is_error: true, is_error: true,
}, },
@@ -149,7 +138,7 @@ impl ToolUseState {
self.pending_tool_uses_by_id.values().collect() self.pending_tool_uses_by_id.values().collect()
} }
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> { pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else { let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
return Vec::new(); return Vec::new();
}; };
@@ -169,55 +158,29 @@ impl ToolUseState {
} }
if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) { if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
match pending_tool_use.status { return match pending_tool_use.status {
PendingToolUseStatus::Idle => ToolUseStatus::Pending, PendingToolUseStatus::Idle => ToolUseStatus::Pending,
PendingToolUseStatus::NeedsConfirmation { .. } => {
ToolUseStatus::NeedsConfirmation
}
PendingToolUseStatus::Running { .. } => ToolUseStatus::Running, PendingToolUseStatus::Running { .. } => ToolUseStatus::Running,
PendingToolUseStatus::Error(ref err) => { PendingToolUseStatus::Error(ref err) => {
ToolUseStatus::Error(err.clone().into()) ToolUseStatus::Error(err.clone().into())
} }
} };
} else {
ToolUseStatus::Pending
} }
})();
let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx) ToolUseStatus::Pending
{ })();
(tool.icon(), tool.needs_confirmation())
} else {
(IconName::Cog, false)
};
tool_uses.push(ToolUse { tool_uses.push(ToolUse {
id: tool_use.id.clone(), id: tool_use.id.clone(),
name: tool_use.name.clone().into(), name: tool_use.name.clone().into(),
ui_text: self.tool_ui_label(&tool_use.name, &tool_use.input, cx),
input: tool_use.input.clone(), input: tool_use.input.clone(),
status, status,
icon,
needs_confirmation,
}) })
} }
tool_uses tool_uses
} }
pub fn tool_ui_label(
&self,
tool_name: &str,
input: &serde_json::Value,
cx: &App,
) -> SharedString {
if let Some(tool) = self.tools.tool(tool_name, cx) {
tool.ui_text(input).into()
} else {
format!("Unknown tool {tool_name:?}").into()
}
}
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> { pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
let empty = Vec::new(); let empty = Vec::new();
@@ -246,7 +209,6 @@ impl ToolUseState {
&mut self, &mut self,
assistant_message_id: MessageId, assistant_message_id: MessageId,
tool_use: LanguageModelToolUse, tool_use: LanguageModelToolUse,
cx: &App,
) { ) {
self.tool_uses_by_assistant_message self.tool_uses_by_assistant_message
.entry(assistant_message_id) .entry(assistant_message_id)
@@ -266,56 +228,24 @@ impl ToolUseState {
PendingToolUse { PendingToolUse {
assistant_message_id, assistant_message_id,
id: tool_use.id, id: tool_use.id,
name: tool_use.name.clone(), name: tool_use.name,
ui_text: self
.tool_ui_label(&tool_use.name, &tool_use.input, cx)
.into(),
input: tool_use.input, input: tool_use.input,
status: PendingToolUseStatus::Idle, status: PendingToolUseStatus::Idle,
}, },
); );
} }
pub fn run_pending_tool( pub fn run_pending_tool(&mut self, tool_use_id: LanguageModelToolUseId, task: Task<()>) {
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
task: Task<()>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) { if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.ui_text = ui_text.into();
tool_use.status = PendingToolUseStatus::Running { tool_use.status = PendingToolUseStatus::Running {
_task: task.shared(), _task: task.shared(),
}; };
} }
} }
pub fn confirm_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<Arc<str>>,
input: serde_json::Value,
messages: Arc<Vec<LanguageModelRequestMessage>>,
tool: Arc<dyn Tool>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
let ui_text = ui_text.into();
tool_use.ui_text = ui_text.clone();
let confirmation = Confirmation {
tool_use_id,
input,
messages,
tool,
ui_text,
};
tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
}
}
pub fn insert_tool_output( pub fn insert_tool_output(
&mut self, &mut self,
tool_use_id: LanguageModelToolUseId, tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<String>, output: Result<String>,
) -> Option<PendingToolUse> { ) -> Option<PendingToolUse> {
match output { match output {
@@ -324,7 +254,6 @@ impl ToolUseState {
tool_use_id.clone(), tool_use_id.clone(),
LanguageModelToolResult { LanguageModelToolResult {
tool_use_id: tool_use_id.clone(), tool_use_id: tool_use_id.clone(),
tool_name,
content: tool_result.into(), content: tool_result.into(),
is_error: false, is_error: false,
}, },
@@ -336,7 +265,6 @@ impl ToolUseState {
tool_use_id.clone(), tool_use_id.clone(),
LanguageModelToolResult { LanguageModelToolResult {
tool_use_id: tool_use_id.clone(), tool_use_id: tool_use_id.clone(),
tool_name,
content: err.to_string().into(), content: err.to_string().into(),
is_error: true, is_error: true,
}, },
@@ -384,7 +312,6 @@ impl ToolUseState {
request_message.content.push(MessageContent::ToolResult( request_message.content.push(MessageContent::ToolResult(
LanguageModelToolResult { LanguageModelToolResult {
tool_use_id: tool_use_id.clone(), tool_use_id: tool_use_id.clone(),
tool_name: tool_result.tool_name.clone(),
is_error: tool_result.is_error, is_error: tool_result.is_error,
content: if tool_result.content.is_empty() { content: if tool_result.content.is_empty() {
// Surprisingly, the API fails if we return an empty string here. // Surprisingly, the API fails if we return an empty string here.
@@ -408,24 +335,13 @@ pub struct PendingToolUse {
#[allow(unused)] #[allow(unused)]
pub assistant_message_id: MessageId, pub assistant_message_id: MessageId,
pub name: Arc<str>, pub name: Arc<str>,
pub ui_text: Arc<str>,
pub input: serde_json::Value, pub input: serde_json::Value,
pub status: PendingToolUseStatus, pub status: PendingToolUseStatus,
} }
#[derive(Debug, Clone)]
pub struct Confirmation {
pub tool_use_id: LanguageModelToolUseId,
pub input: serde_json::Value,
pub ui_text: Arc<str>,
pub messages: Arc<Vec<LanguageModelRequestMessage>>,
pub tool: Arc<dyn Tool>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum PendingToolUseStatus { pub enum PendingToolUseStatus {
Idle, Idle,
NeedsConfirmation(Arc<Confirmation>),
Running { _task: Shared<Task<()>> }, Running { _task: Shared<Task<()>> },
Error(#[allow(unused)] Arc<str>), Error(#[allow(unused)] Arc<str>),
} }
@@ -438,8 +354,4 @@ impl PendingToolUseStatus {
pub fn is_error(&self) -> bool { pub fn is_error(&self) -> bool {
matches!(self, PendingToolUseStatus::Error(_)) matches!(self, PendingToolUseStatus::Error(_))
} }
pub fn needs_confirmation(&self) -> bool {
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
}
} }

View File

@@ -1,5 +1,3 @@
mod agent_notification;
mod context_pill; mod context_pill;
pub use agent_notification::*;
pub use context_pill::*; pub use context_pill::*;

View File

@@ -1,164 +0,0 @@
use gpui::{
App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
linear_color_stop, linear_gradient, point,
};
use release_channel::ReleaseChannel;
use std::rc::Rc;
use theme;
use ui::{Render, prelude::*};
pub struct AgentNotification {
title: SharedString,
caption: SharedString,
icon: IconName,
}
impl AgentNotification {
pub fn new(
title: impl Into<SharedString>,
caption: impl Into<SharedString>,
icon: IconName,
) -> Self {
Self {
title: title.into(),
caption: caption.into(),
icon,
}
}
pub fn window_options(screen: Rc<dyn PlatformDisplay>, cx: &App) -> WindowOptions {
let size = Size {
width: px(450.),
height: px(72.),
};
let notification_margin_width = px(16.);
let notification_margin_height = px(-48.);
let bounds = gpui::Bounds::<Pixels> {
origin: screen.bounds().top_right()
- point(
size.width + notification_margin_width,
notification_margin_height,
),
size,
};
let app_id = ReleaseChannel::global(cx).app_id();
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
titlebar: None,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
display_id: Some(screen.id()),
window_background: WindowBackgroundAppearance::Transparent,
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
}
}
}
pub enum AgentNotificationEvent {
Accepted,
Dismissed,
}
impl EventEmitter<AgentNotificationEvent> for AgentNotification {}
impl Render for AgentNotification {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx);
let line_height = window.line_height();
let bg = cx.theme().colors().elevated_surface_background;
let gradient_overflow = || {
div()
.h_full()
.absolute()
.w_8()
.bottom_0()
.right_0()
.bg(linear_gradient(
90.,
linear_color_stop(bg, 1.),
linear_color_stop(bg.opacity(0.2), 0.),
))
};
h_flex()
.id("agent-notification")
.size_full()
.p_3()
.gap_4()
.justify_between()
.elevation_3(cx)
.text_ui(cx)
.font(ui_font)
.border_color(cx.theme().colors().border)
.rounded_xl()
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(AgentNotificationEvent::Accepted);
}))
.child(
h_flex()
.items_start()
.gap_2()
.flex_1()
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(self.icon)
.color(Color::Muted)
.size(IconSize::Small),
),
)
.child(
v_flex()
.child(
div()
.relative()
.text_size(px(14.))
.text_color(cx.theme().colors().text)
.max_w(px(300.))
.truncate()
.child(self.title.clone())
.child(gradient_overflow()),
)
.child(
div()
.relative()
.text_size(px(12.))
.text_color(cx.theme().colors().text_muted)
.max_w(px(340.))
.truncate()
.child(self.caption.clone())
.child(gradient_overflow()),
),
),
)
.child(
v_flex()
.gap_1()
.items_center()
.child(
Button::new("open", "View Panel")
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.on_click({
cx.listener(move |_this, _event, _, cx| {
cx.emit(AgentNotificationEvent::Accepted);
})
}),
)
.child(Button::new("dismiss", "Dismiss").full_width().on_click({
cx.listener(move |_, _event, _, cx| {
cx.emit(AgentNotificationEvent::Dismissed);
})
})),
)
}
}

View File

@@ -1,15 +1,14 @@
use std::rc::Rc; use std::rc::Rc;
use file_icons::FileIcons;
use gpui::ClickEvent; use gpui::ClickEvent;
use ui::{IconButtonShape, Tooltip, prelude::*}; use ui::{prelude::*, IconButtonShape, Tooltip};
use crate::context::{AssistantContext, ContextId, ContextKind}; use crate::context::{ContextKind, ContextSnapshot};
#[derive(IntoElement)] #[derive(IntoElement)]
pub enum ContextPill { pub enum ContextPill {
Added { Added {
context: AddedContext, context: ContextSnapshot,
dupe_name: bool, dupe_name: bool,
focused: bool, focused: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>, on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
@@ -26,7 +25,7 @@ pub enum ContextPill {
impl ContextPill { impl ContextPill {
pub fn added( pub fn added(
context: AddedContext, context: ContextSnapshot,
dupe_name: bool, dupe_name: bool,
focused: bool, focused: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>, on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
@@ -78,21 +77,17 @@ impl ContextPill {
pub fn icon(&self) -> Icon { pub fn icon(&self) -> Icon {
match self { match self {
Self::Added { context, .. } => match &context.icon_path {
Some(icon_path) => Icon::from_path(icon_path),
None => Icon::new(context.kind.icon()),
},
Self::Suggested { Self::Suggested {
icon_path: Some(icon_path), icon_path: Some(icon_path),
.. ..
}
| Self::Added {
context:
AddedContext {
icon_path: Some(icon_path),
..
},
..
} => Icon::from_path(icon_path), } => Icon::from_path(icon_path),
Self::Suggested { kind, .. } Self::Suggested {
| Self::Added { kind,
context: AddedContext { kind, .. }, icon_path: None,
.. ..
} => Icon::new(kind.icon()), } => Icon::new(kind.icon()),
} }
@@ -149,7 +144,7 @@ impl RenderOnce for ContextPill {
element element
} }
}) })
.when_some(context.tooltip.as_ref(), |element, tooltip| { .when_some(context.tooltip.clone(), |element, tooltip| {
element.tooltip(Tooltip::text(tooltip.clone())) element.tooltip(Tooltip::text(tooltip.clone()))
}), }),
) )
@@ -167,9 +162,7 @@ impl RenderOnce for ContextPill {
}) })
.when_some(on_click.as_ref(), |element, on_click| { .when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone(); let on_click = on_click.clone();
element element.on_click(move |event, window, cx| on_click(event, window, cx))
.cursor_pointer()
.on_click(move |event, window, cx| on_click(event, window, cx))
}), }),
ContextPill::Suggested { ContextPill::Suggested {
name, name,
@@ -180,14 +173,10 @@ impl RenderOnce for ContextPill {
} => base_pill } => base_pill
.cursor_pointer() .cursor_pointer()
.pr_1() .pr_1()
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.border_dashed()
.border_color(if *focused { .border_color(if *focused {
color.border_focused color.border_focused
} else { } else {
color.border color.border_variant.opacity(0.5)
}) })
.hover(|style| style.bg(color.element_hover.opacity(0.5))) .hover(|style| style.bg(color.element_hover.opacity(0.5)))
.child( .child(
@@ -201,10 +190,9 @@ impl RenderOnce for ContextPill {
.child( .child(
Label::new(match kind { Label::new(match kind {
ContextKind::File => "Active Tab", ContextKind::File => "Active Tab",
ContextKind::Thread ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
| ContextKind::Directory "Active"
| ContextKind::FetchedUrl }
| ContextKind::Symbol => "Active",
}) })
.size(LabelSize::XSmall) .size(LabelSize::XSmall)
.color(Color::Muted), .color(Color::Muted),
@@ -224,91 +212,3 @@ impl RenderOnce for ContextPill {
} }
} }
} }
pub struct AddedContext {
pub id: ContextId,
pub kind: ContextKind,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
}
impl AddedContext {
pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
match context {
AssistantContext::File(file_context) => {
let full_path = file_context.context_buffer.file.full_path(cx);
let full_path_string: SharedString =
full_path.to_string_lossy().into_owned().into();
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
id: file_context.id,
kind: ContextKind::File,
name,
parent,
tooltip: Some(full_path_string),
icon_path: FileIcons::get_icon(&full_path, cx),
}
}
AssistantContext::Directory(directory_context) => {
// TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
// handle renames?
let full_path = &directory_context.project_path.path;
let full_path_string: SharedString =
full_path.to_string_lossy().into_owned().into();
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
id: directory_context.id,
kind: ContextKind::Directory,
name,
parent,
tooltip: Some(full_path_string),
icon_path: None,
}
}
AssistantContext::Symbol(symbol_context) => AddedContext {
id: symbol_context.id,
kind: ContextKind::Symbol,
name: symbol_context.context_symbol.id.name.clone(),
parent: None,
tooltip: None,
icon_path: None,
},
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
id: fetched_url_context.id,
kind: ContextKind::FetchedUrl,
name: fetched_url_context.url.clone(),
parent: None,
tooltip: None,
icon_path: None,
},
AssistantContext::Thread(thread_context) => AddedContext {
id: thread_context.id,
kind: ContextKind::Thread,
name: thread_context.summary(cx),
parent: None,
tooltip: None,
icon_path: None,
},
}
}
}

View File

@@ -2,7 +2,7 @@
mod context_tests; mod context_tests;
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus}; use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{ use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection, SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
SlashCommandResult, SlashCommandWorkingSet, SlashCommandResult, SlashCommandWorkingSet,
@@ -12,17 +12,17 @@ use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use fs::{Fs, RemoveOptions}; use fs::{Fs, RemoveOptions};
use futures::{FutureExt, StreamExt, future::Shared}; use futures::{future::Shared, FutureExt, StreamExt};
use gpui::{ use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription, App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
Task, Task,
}; };
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset}; use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
use language_model::{ use language_model::{
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent, report_assistant_event, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRegistry, LanguageModelRequest,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError, LanguageModelRequestMessage, LanguageModelToolUseId, MaxMonthlySpendReachedError,
Role, StopReason, report_assistant_event, MessageContent, PaymentRequiredError, Role, StopReason,
}; };
use open_ai::Model as OpenAiModel; use open_ai::Model as OpenAiModel;
use paths::contexts_dir; use paths::contexts_dir;
@@ -31,7 +31,7 @@ use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
cmp::{Ordering, max}, cmp::{max, Ordering},
fmt::Debug, fmt::Debug,
iter, mem, iter, mem,
ops::Range, ops::Range,
@@ -43,7 +43,7 @@ use std::{
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use text::{BufferSnapshot, ToPoint}; use text::{BufferSnapshot, ToPoint};
use ui::IconName; use ui::IconName;
use util::{ResultExt, TryFutureExt, post_inc}; use util::{post_inc, ResultExt, TryFutureExt};
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
@@ -162,11 +162,6 @@ pub enum ContextOperation {
section: SlashCommandOutputSection<language::Anchor>, section: SlashCommandOutputSection<language::Anchor>,
version: clock::Global, version: clock::Global,
}, },
ThoughtProcessOutputSectionAdded {
timestamp: clock::Lamport,
section: ThoughtProcessOutputSection<language::Anchor>,
version: clock::Global,
},
BufferOperation(language::Operation), BufferOperation(language::Operation),
} }
@@ -264,20 +259,6 @@ impl ContextOperation {
version: language::proto::deserialize_version(&message.version), version: language::proto::deserialize_version(&message.version),
}) })
} }
proto::context_operation::Variant::ThoughtProcessOutputSectionAdded(message) => {
let section = message.section.context("missing section")?;
Ok(Self::ThoughtProcessOutputSectionAdded {
timestamp: language::proto::deserialize_timestamp(
message.timestamp.context("missing timestamp")?,
),
section: ThoughtProcessOutputSection {
range: language::proto::deserialize_anchor_range(
section.range.context("invalid range")?,
)?,
},
version: language::proto::deserialize_version(&message.version),
})
}
proto::context_operation::Variant::BufferOperation(op) => Ok(Self::BufferOperation( proto::context_operation::Variant::BufferOperation(op) => Ok(Self::BufferOperation(
language::proto::deserialize_operation( language::proto::deserialize_operation(
op.operation.context("invalid buffer operation")?, op.operation.context("invalid buffer operation")?,
@@ -389,27 +370,6 @@ impl ContextOperation {
}, },
)), )),
}, },
Self::ThoughtProcessOutputSectionAdded {
timestamp,
section,
version,
} => proto::ContextOperation {
variant: Some(
proto::context_operation::Variant::ThoughtProcessOutputSectionAdded(
proto::context_operation::ThoughtProcessOutputSectionAdded {
timestamp: Some(language::proto::serialize_timestamp(*timestamp)),
section: Some({
proto::ThoughtProcessOutputSection {
range: Some(language::proto::serialize_anchor_range(
section.range.clone(),
)),
}
}),
version: language::proto::serialize_version(version),
},
),
),
},
Self::BufferOperation(operation) => proto::ContextOperation { Self::BufferOperation(operation) => proto::ContextOperation {
variant: Some(proto::context_operation::Variant::BufferOperation( variant: Some(proto::context_operation::Variant::BufferOperation(
proto::context_operation::BufferOperation { proto::context_operation::BufferOperation {
@@ -427,8 +387,7 @@ impl ContextOperation {
Self::UpdateSummary { summary, .. } => summary.timestamp, Self::UpdateSummary { summary, .. } => summary.timestamp,
Self::SlashCommandStarted { id, .. } => id.0, Self::SlashCommandStarted { id, .. } => id.0,
Self::SlashCommandOutputSectionAdded { timestamp, .. } Self::SlashCommandOutputSectionAdded { timestamp, .. }
| Self::SlashCommandFinished { timestamp, .. } | Self::SlashCommandFinished { timestamp, .. } => *timestamp,
| Self::ThoughtProcessOutputSectionAdded { timestamp, .. } => *timestamp,
Self::BufferOperation(_) => { Self::BufferOperation(_) => {
panic!("reading the timestamp of a buffer operation is not supported") panic!("reading the timestamp of a buffer operation is not supported")
} }
@@ -443,8 +402,7 @@ impl ContextOperation {
| Self::UpdateSummary { version, .. } | Self::UpdateSummary { version, .. }
| Self::SlashCommandStarted { version, .. } | Self::SlashCommandStarted { version, .. }
| Self::SlashCommandOutputSectionAdded { version, .. } | Self::SlashCommandOutputSectionAdded { version, .. }
| Self::SlashCommandFinished { version, .. } | Self::SlashCommandFinished { version, .. } => version,
| Self::ThoughtProcessOutputSectionAdded { version, .. } => version,
Self::BufferOperation(_) => { Self::BufferOperation(_) => {
panic!("reading the version of a buffer operation is not supported") panic!("reading the version of a buffer operation is not supported")
} }
@@ -460,8 +418,6 @@ pub enum ContextEvent {
MessagesEdited, MessagesEdited,
SummaryChanged, SummaryChanged,
StreamedCompletion, StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
EndedThoughtProcess(language::Anchor),
PatchesUpdated { PatchesUpdated {
removed: Vec<Range<language::Anchor>>, removed: Vec<Range<language::Anchor>>,
updated: Vec<Range<language::Anchor>>, updated: Vec<Range<language::Anchor>>,
@@ -542,17 +498,6 @@ impl MessageMetadata {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThoughtProcessOutputSection<T> {
pub range: Range<T>,
}
impl ThoughtProcessOutputSection<language::Anchor> {
pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool {
self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Message { pub struct Message {
pub offset_range: Range<usize>, pub offset_range: Range<usize>,
@@ -635,7 +580,6 @@ pub struct AssistantContext {
edits_since_last_parse: language::Subscription, edits_since_last_parse: language::Subscription,
slash_commands: Arc<SlashCommandWorkingSet>, slash_commands: Arc<SlashCommandWorkingSet>,
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>, slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
thought_process_output_sections: Vec<ThoughtProcessOutputSection<language::Anchor>>,
message_anchors: Vec<MessageAnchor>, message_anchors: Vec<MessageAnchor>,
contents: Vec<Content>, contents: Vec<Content>,
messages_metadata: HashMap<MessageId, MessageMetadata>, messages_metadata: HashMap<MessageId, MessageMetadata>,
@@ -738,7 +682,6 @@ impl AssistantContext {
parsed_slash_commands: Vec::new(), parsed_slash_commands: Vec::new(),
invoked_slash_commands: HashMap::default(), invoked_slash_commands: HashMap::default(),
slash_command_output_sections: Vec::new(), slash_command_output_sections: Vec::new(),
thought_process_output_sections: Vec::new(),
edits_since_last_parse: edits_since_last_slash_command_parse, edits_since_last_parse: edits_since_last_slash_command_parse,
summary: None, summary: None,
pending_summary: Task::ready(None), pending_summary: Task::ready(None),
@@ -821,18 +764,6 @@ impl AssistantContext {
} }
}) })
.collect(), .collect(),
thought_process_output_sections: self
.thought_process_output_sections
.iter()
.filter_map(|section| {
if section.is_valid(buffer) {
let range = section.range.to_offset(buffer);
Some(ThoughtProcessOutputSection { range })
} else {
None
}
})
.collect(),
} }
} }
@@ -1026,16 +957,6 @@ impl AssistantContext {
cx.emit(ContextEvent::SlashCommandOutputSectionAdded { section }); cx.emit(ContextEvent::SlashCommandOutputSectionAdded { section });
} }
} }
ContextOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
let buffer = self.buffer.read(cx);
if let Err(ix) = self
.thought_process_output_sections
.binary_search_by(|probe| probe.range.cmp(&section.range, buffer))
{
self.thought_process_output_sections
.insert(ix, section.clone());
}
}
ContextOperation::SlashCommandFinished { ContextOperation::SlashCommandFinished {
id, id,
error_message, error_message,
@@ -1099,9 +1020,6 @@ impl AssistantContext {
ContextOperation::SlashCommandOutputSectionAdded { section, .. } => { ContextOperation::SlashCommandOutputSectionAdded { section, .. } => {
self.has_received_operations_for_anchor_range(section.range.clone(), cx) self.has_received_operations_for_anchor_range(section.range.clone(), cx)
} }
ContextOperation::ThoughtProcessOutputSectionAdded { section, .. } => {
self.has_received_operations_for_anchor_range(section.range.clone(), cx)
}
ContextOperation::SlashCommandFinished { .. } => true, ContextOperation::SlashCommandFinished { .. } => true,
ContextOperation::BufferOperation(_) => { ContextOperation::BufferOperation(_) => {
panic!("buffer operations should always be applied") panic!("buffer operations should always be applied")
@@ -1210,12 +1128,6 @@ impl AssistantContext {
&self.slash_command_output_sections &self.slash_command_output_sections
} }
pub fn thought_process_output_sections(
&self,
) -> &[ThoughtProcessOutputSection<language::Anchor>] {
&self.thought_process_output_sections
}
pub fn contains_files(&self, cx: &App) -> bool { pub fn contains_files(&self, cx: &App) -> bool {
let buffer = self.buffer.read(cx); let buffer = self.buffer.read(cx);
self.slash_command_output_sections.iter().any(|section| { self.slash_command_output_sections.iter().any(|section| {
@@ -2256,35 +2168,6 @@ impl AssistantContext {
); );
} }
fn insert_thought_process_output_section(
&mut self,
section: ThoughtProcessOutputSection<language::Anchor>,
cx: &mut Context<Self>,
) {
let buffer = self.buffer.read(cx);
let insertion_ix = match self
.thought_process_output_sections
.binary_search_by(|probe| probe.range.cmp(&section.range, buffer))
{
Ok(ix) | Err(ix) => ix,
};
self.thought_process_output_sections
.insert(insertion_ix, section.clone());
// cx.emit(ContextEvent::ThoughtProcessOutputSectionAdded {
// section: section.clone(),
// });
let version = self.version.clone();
let timestamp = self.next_timestamp();
self.push_op(
ContextOperation::ThoughtProcessOutputSectionAdded {
timestamp,
section,
version,
},
cx,
);
}
pub fn completion_provider_changed(&mut self, cx: &mut Context<Self>) { pub fn completion_provider_changed(&mut self, cx: &mut Context<Self>) {
self.count_remaining_tokens(cx); self.count_remaining_tokens(cx);
} }
@@ -2337,10 +2220,6 @@ impl AssistantContext {
let request_start = Instant::now(); let request_start = Instant::now();
let mut events = stream.await?; let mut events = stream.await?;
let mut stop_reason = StopReason::EndTurn; let mut stop_reason = StopReason::EndTurn;
let mut thought_process_stack = Vec::new();
const THOUGHT_PROCESS_START_MARKER: &str = "<think>\n";
const THOUGHT_PROCESS_END_MARKER: &str = "\n</think>";
while let Some(event) = events.next().await { while let Some(event) = events.next().await {
if response_latency.is_none() { if response_latency.is_none() {
@@ -2348,9 +2227,6 @@ impl AssistantContext {
} }
let event = event?; let event = event?;
let mut context_event = None;
let mut thought_process_output_section = None;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
let message_ix = this let message_ix = this
.message_anchors .message_anchors
@@ -2369,50 +2245,7 @@ impl AssistantContext {
LanguageModelCompletionEvent::Stop(reason) => { LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason; stop_reason = reason;
} }
LanguageModelCompletionEvent::Thinking(chunk) => { LanguageModelCompletionEvent::Text(chunk) => {
if thought_process_stack.is_empty() {
let start =
buffer.anchor_before(message_old_end_offset);
thought_process_stack.push(start);
let chunk =
format!("{THOUGHT_PROCESS_START_MARKER}{chunk}{THOUGHT_PROCESS_END_MARKER}");
let chunk_len = chunk.len();
buffer.edit(
[(
message_old_end_offset..message_old_end_offset,
chunk,
)],
None,
cx,
);
let end = buffer
.anchor_before(message_old_end_offset + chunk_len);
context_event = Some(
ContextEvent::StartedThoughtProcess(start..end),
);
} else {
// This ensures that all the thinking chunks are inserted inside the thinking tag
let insertion_position =
message_old_end_offset - THOUGHT_PROCESS_END_MARKER.len();
buffer.edit(
[(insertion_position..insertion_position, chunk)],
None,
cx,
);
}
}
LanguageModelCompletionEvent::Text(mut chunk) => {
if let Some(start) = thought_process_stack.pop() {
let end = buffer.anchor_before(message_old_end_offset);
context_event =
Some(ContextEvent::EndedThoughtProcess(end));
thought_process_output_section =
Some(ThoughtProcessOutputSection {
range: start..end,
});
chunk.insert_str(0, "\n\n");
}
buffer.edit( buffer.edit(
[( [(
message_old_end_offset..message_old_end_offset, message_old_end_offset..message_old_end_offset,
@@ -2427,13 +2260,6 @@ impl AssistantContext {
} }
}); });
if let Some(section) = thought_process_output_section.take() {
this.insert_thought_process_output_section(section, cx);
}
if let Some(context_event) = context_event.take() {
cx.emit(context_event);
}
cx.emit(ContextEvent::StreamedCompletion); cx.emit(ContextEvent::StreamedCompletion);
Some(()) Some(())
@@ -3301,8 +3127,6 @@ pub struct SavedContext {
pub summary: String, pub summary: String,
pub slash_command_output_sections: pub slash_command_output_sections:
Vec<assistant_slash_command::SlashCommandOutputSection<usize>>, Vec<assistant_slash_command::SlashCommandOutputSection<usize>>,
#[serde(default)]
pub thought_process_output_sections: Vec<ThoughtProcessOutputSection<usize>>,
} }
impl SavedContext { impl SavedContext {
@@ -3404,20 +3228,6 @@ impl SavedContext {
version.observe(timestamp); version.observe(timestamp);
} }
for section in self.thought_process_output_sections {
let timestamp = next_timestamp.tick();
operations.push(ContextOperation::ThoughtProcessOutputSectionAdded {
timestamp,
section: ThoughtProcessOutputSection {
range: buffer.anchor_after(section.range.start)
..buffer.anchor_before(section.range.end),
},
version: version.clone(),
});
version.observe(timestamp);
}
let timestamp = next_timestamp.tick(); let timestamp = next_timestamp.tick();
operations.push(ContextOperation::UpdateSummary { operations.push(ContextOperation::UpdateSummary {
summary: ContextSummary { summary: ContextSummary {
@@ -3492,7 +3302,6 @@ impl SavedContextV0_3_0 {
.collect(), .collect(),
summary: self.summary, summary: self.summary,
slash_command_output_sections: self.slash_command_output_sections, slash_command_output_sections: self.slash_command_output_sections,
thought_process_output_sections: Vec::new(),
} }
} }
} }

View File

@@ -14,7 +14,7 @@ use futures::{
channel::mpsc, channel::mpsc,
stream::{self, StreamExt}, stream::{self, StreamExt},
}; };
use gpui::{App, Entity, SharedString, Task, TestAppContext, WeakEntity, prelude::*}; use gpui::{prelude::*, App, Entity, SharedString, Task, TestAppContext, WeakEntity};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate}; use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role}; use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex; use parking_lot::Mutex;
@@ -30,14 +30,14 @@ use std::{
ops::Range, ops::Range,
path::Path, path::Path,
rc::Rc, rc::Rc,
sync::{Arc, atomic::AtomicBool}, sync::{atomic::AtomicBool, Arc},
}; };
use text::{OffsetRangeExt as _, ReplicaId, ToOffset, network::Network}; use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
use ui::{IconName, Window}; use ui::{IconName, Window};
use unindent::Unindent; use unindent::Unindent;
use util::{ use util::{
RandomCharIter,
test::{generate_marked_text, marked_text_ranges}, test::{generate_marked_text, marked_text_ranges},
RandomCharIter,
}; };
use workspace::Workspace; use workspace::Workspace;

View File

@@ -2,36 +2,36 @@ use anyhow::Result;
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet}; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
use assistant_slash_commands::{ use assistant_slash_commands::{
DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand, selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs,
selections_creases, FileSlashCommand,
}; };
use client::{proto, zed_urls}; use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map}; use collections::{hash_map, BTreeSet, HashMap, HashSet};
use editor::{ use editor::{
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt}, actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
display_map::{ display_map::{
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint, CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
}, },
scroll::Autoscroll, scroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
}; };
use editor::{FoldPlaceholder, display_map::CreaseId}; use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs; use fs::Fs;
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry, actions, div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between,
size, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry,
ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage, Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage,
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
WeakEntity, actions, div, img, impl_internal_actions, percentage, point, prelude::*, WeakEntity,
pulsating_between, size,
}; };
use indexed_docs::IndexedDocsStore; use indexed_docs::IndexedDocsStore;
use language::{ use language::{
language_settings::{all_language_settings, SoftWrap},
BufferSnapshot, LspAdapterDelegate, ToOffset, BufferSnapshot, LspAdapterDelegate, ToOffset,
language_settings::{SoftWrap, all_language_settings},
}; };
use language_model::{ use language_model::{
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry, LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
@@ -46,33 +46,30 @@ use project::lsp_store::LocalLspAdapterDelegate;
use project::{Project, Worktree}; use project::{Project, Worktree};
use rope::Point; use rope::Point;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, update_settings_file}; use settings::{update_settings_file, Settings, SettingsStore};
use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration}; use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
use text::SelectionGoal; use text::SelectionGoal;
use ui::{ use ui::{
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*, ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor,
prelude::*, Tooltip,
}; };
use util::{ResultExt, maybe}; use util::{maybe, ResultExt};
use workspace::searchable::{Direction, SearchableItemHandle}; use workspace::searchable::{Direction, SearchableItemHandle};
use workspace::{ use workspace::{
Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
item::{self, FollowableItem, Item, ItemHandle}, item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId, notifications::NotificationId,
pane::{self, SaveIntent}, pane::{self, SaveIntent},
searchable::{SearchEvent, SearchableItem}, searchable::{SearchEvent, SearchableItem},
Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
}; };
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
use crate::{ use crate::{
AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent, AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent,
ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
MessageMetadata, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType, MessageMetadata, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
}; };
use crate::{
ThoughtProcessOutputSection, slash_command::SlashCommandCompletionProvider,
slash_command_picker,
};
actions!( actions!(
assistant, assistant,
@@ -123,11 +120,6 @@ enum AssistError {
Message(SharedString), Message(SharedString),
} }
pub enum ThoughtProcessStatus {
Pending,
Completed,
}
pub trait AssistantPanelDelegate { pub trait AssistantPanelDelegate {
fn active_context_editor( fn active_context_editor(
&self, &self,
@@ -186,7 +178,6 @@ pub struct ContextEditor {
project: Entity<Project>, project: Entity<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>, lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
editor: Entity<Editor>, editor: Entity<Editor>,
pending_thought_process: Option<(CreaseId, language::Anchor)>,
blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>, blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
image_blocks: HashSet<CustomBlockId>, image_blocks: HashSet<CustomBlockId>,
scroll_position: Option<ScrollPosition>, scroll_position: Option<ScrollPosition>,
@@ -262,8 +253,7 @@ impl ContextEditor {
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed), cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
]; ];
let slash_command_sections = context.read(cx).slash_command_output_sections().to_vec(); let sections = context.read(cx).slash_command_output_sections().to_vec();
let thought_process_sections = context.read(cx).thought_process_output_sections().to_vec();
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>(); let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
let slash_commands = context.read(cx).slash_commands().clone(); let slash_commands = context.read(cx).slash_commands().clone();
let mut this = Self { let mut this = Self {
@@ -275,7 +265,6 @@ impl ContextEditor {
image_blocks: Default::default(), image_blocks: Default::default(),
scroll_position: None, scroll_position: None,
remote_id: None, remote_id: None,
pending_thought_process: None,
fs: fs.clone(), fs: fs.clone(),
workspace, workspace,
project, project,
@@ -305,14 +294,7 @@ impl ContextEditor {
}; };
this.update_message_headers(cx); this.update_message_headers(cx);
this.update_image_blocks(cx); this.update_image_blocks(cx);
this.insert_slash_command_output_sections(slash_command_sections, false, window, cx); this.insert_slash_command_output_sections(sections, false, window, cx);
this.insert_thought_process_output_sections(
thought_process_sections
.into_iter()
.map(|section| (section, ThoughtProcessStatus::Completed)),
window,
cx,
);
this.patches_updated(&Vec::new(), &patch_ranges, window, cx); this.patches_updated(&Vec::new(), &patch_ranges, window, cx);
this this
} }
@@ -414,9 +396,12 @@ impl ContextEditor {
cursor..cursor cursor..cursor
}; };
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| { editor.change_selections(
selections.select_ranges([new_selection]) Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
}); window,
cx,
|selections| selections.select_ranges([new_selection]),
);
}); });
// Avoid scrolling to the new cursor position so the assistant's output is stable. // Avoid scrolling to the new cursor position so the assistant's output is stable.
cx.defer_in(window, |this, _, _| this.scroll_position = None); cx.defer_in(window, |this, _, _| this.scroll_position = None);
@@ -614,47 +599,6 @@ impl ContextEditor {
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
}); });
} }
ContextEvent::StartedThoughtProcess(range) => {
let creases = self.insert_thought_process_output_sections(
[(
ThoughtProcessOutputSection {
range: range.clone(),
},
ThoughtProcessStatus::Pending,
)],
window,
cx,
);
self.pending_thought_process = Some((creases[0], range.start));
}
ContextEvent::EndedThoughtProcess(end) => {
if let Some((crease_id, start)) = self.pending_thought_process.take() {
self.editor.update(cx, |editor, cx| {
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let (excerpt_id, _, _) = multi_buffer_snapshot.as_singleton().unwrap();
let start_anchor = multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, start)
.unwrap();
editor.display_map.update(cx, |display_map, cx| {
display_map.unfold_intersecting(
vec![start_anchor..start_anchor],
true,
cx,
);
});
editor.remove_creases(vec![crease_id], cx);
});
self.insert_thought_process_output_sections(
[(
ThoughtProcessOutputSection { range: start..*end },
ThoughtProcessStatus::Completed,
)],
window,
cx,
);
}
}
ContextEvent::StreamedCompletion => { ContextEvent::StreamedCompletion => {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
if let Some(scroll_position) = self.scroll_position { if let Some(scroll_position) = self.scroll_position {
@@ -1002,62 +946,6 @@ impl ContextEditor {
self.update_active_patch(window, cx); self.update_active_patch(window, cx);
} }
fn insert_thought_process_output_sections(
&mut self,
sections: impl IntoIterator<
Item = (
ThoughtProcessOutputSection<language::Anchor>,
ThoughtProcessStatus,
),
>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<CreaseId> {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
let excerpt_id = *buffer.as_singleton().unwrap().0;
let mut buffer_rows_to_fold = BTreeSet::new();
let mut creases = Vec::new();
for (section, status) in sections {
let start = buffer
.anchor_in_excerpt(excerpt_id, section.range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, section.range.end)
.unwrap();
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
creases.push(
Crease::inline(
start..end,
FoldPlaceholder {
render: render_thought_process_fold_icon_button(
cx.entity().downgrade(),
status,
),
merge_adjacent: false,
..Default::default()
},
render_slash_command_output_toggle,
|_, _, _, _| Empty.into_any_element(),
)
.with_metadata(CreaseMetadata {
icon: IconName::Ai,
label: "Thinking Process".into(),
}),
);
}
let creases = editor.insert_creases(creases, cx);
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
editor.fold_at(&FoldAt { buffer_row }, window, cx);
}
creases
})
}
fn insert_slash_command_output_sections( fn insert_slash_command_output_sections(
&mut self, &mut self,
sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>, sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
@@ -2764,52 +2652,6 @@ fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Opti
None None
} }
fn render_thought_process_fold_icon_button(
editor: WeakEntity<Editor>,
status: ThoughtProcessStatus,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new(move |fold_id, fold_range, _cx| {
let editor = editor.clone();
let button = ButtonLike::new(fold_id).layer(ElevationIndex::ElevatedSurface);
let button = match status {
ThoughtProcessStatus::Pending => button
.child(
Icon::new(IconName::Brain)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new("Thinking…").color(Color::Muted).with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
),
),
ThoughtProcessStatus::Completed => button
.style(ButtonStyle::Filled)
.child(Icon::new(IconName::Brain).size(IconSize::Small))
.child(Label::new("Thought Process").single_line()),
};
button
.on_click(move |_, window, cx| {
editor
.update(cx, |editor, cx| {
let buffer_start = fold_range
.start
.to_point(&editor.buffer().read(cx).read(cx));
let buffer_row = MultiBufferRow(buffer_start.row);
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
})
.ok();
})
.into_any_element()
})
}
fn render_fold_icon_button( fn render_fold_icon_button(
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
icon: IconName, icon: IconName,
@@ -3413,7 +3255,7 @@ impl ContextEditorToolbarItem {
pub fn render_remaining_tokens( pub fn render_remaining_tokens(
context_editor: &Entity<ContextEditor>, context_editor: &Entity<ContextEditor>,
cx: &App, cx: &App,
) -> Option<impl IntoElement + use<>> { ) -> Option<impl IntoElement> {
let context = &context_editor.read(cx).context; let context = &context_editor.read(cx).context;
let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)? let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?

View File

@@ -3,13 +3,13 @@ use std::sync::Arc;
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity}; use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::Project; use project::Project;
use ui::utils::{DateTimeType, format_distance_from_now}; use ui::utils::{format_distance_from_now, DateTimeType};
use ui::{Avatar, ListItem, ListItemSpacing, prelude::*}; use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
use workspace::{Item, Workspace}; use workspace::{Item, Workspace};
use crate::{ use crate::{
AssistantPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata, AssistantPanelDelegate, ContextStore, RemoteContextMetadata, SavedContextMetadata,
SavedContextMetadata, DEFAULT_TAB_TITLE,
}; };
#[derive(Clone)] #[derive(Clone)]
@@ -229,12 +229,10 @@ impl PickerDelegate for SavedContextPickerDelegate {
.into_any_element(), .into_any_element(),
] ]
} else { } else {
vec![ vec![Label::new("Shared by host")
Label::new("Shared by host") .color(Color::Muted)
.color(Color::Muted) .size(LabelSize::Small)
.size(LabelSize::Small) .into_any_element()]
.into_any_element(),
]
}), }),
) )
} }

View File

@@ -2,13 +2,13 @@ use crate::{
AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
SavedContextMetadata, SavedContextMetadata,
}; };
use anyhow::{Context as _, Result, anyhow}; use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet}; use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry}; use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
use clock::ReplicaId; use clock::ReplicaId;
use collections::HashMap; use collections::HashMap;
use context_server::ContextServerFactoryRegistry;
use context_server::manager::ContextServerManager; use context_server::manager::ContextServerManager;
use context_server::ContextServerFactoryRegistry;
use fs::{Fs, RemoveOptions}; use fs::{Fs, RemoveOptions};
use futures::StreamExt; use futures::StreamExt;
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
@@ -104,47 +104,52 @@ impl ContextStore {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100); const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await; let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this = cx.new(|cx: &mut Context<Self>| { let this =
let context_server_factory_registry = cx.new(|cx: &mut Context<Self>| {
ContextServerFactoryRegistry::default_global(cx); let context_server_factory_registry =
let context_server_manager = cx.new(|cx| { ContextServerFactoryRegistry::default_global(cx);
ContextServerManager::new(context_server_factory_registry, project.clone(), cx) let context_server_manager = cx.new(|cx| {
}); ContextServerManager::new(
let mut this = Self { context_server_factory_registry,
contexts: Vec::new(), project.clone(),
contexts_metadata: Vec::new(), cx,
context_server_manager, )
context_server_slash_command_ids: HashMap::default(), });
host_contexts: Vec::new(), let mut this = Self {
fs, contexts: Vec::new(),
languages, contexts_metadata: Vec::new(),
slash_commands, context_server_manager,
telemetry, context_server_slash_command_ids: HashMap::default(),
_watch_updates: cx.spawn(async move |this, cx| { host_contexts: Vec::new(),
async move { fs,
while events.next().await.is_some() { languages,
this.update(cx, |this, cx| this.reload(cx))?.await.log_err(); slash_commands,
telemetry,
_watch_updates: cx.spawn(async move |this, cx| {
async move {
while events.next().await.is_some() {
this.update(cx, |this, cx| this.reload(cx))?.await.log_err();
}
anyhow::Ok(())
} }
anyhow::Ok(()) .log_err()
} .await
.log_err() }),
.await client_subscription: None,
}), _project_subscriptions: vec![
client_subscription: None, cx.subscribe(&project, Self::handle_project_event)
_project_subscriptions: vec![ ],
cx.subscribe(&project, Self::handle_project_event), project_is_shared: false,
], client: project.read(cx).client(),
project_is_shared: false, project: project.clone(),
client: project.read(cx).client(), prompt_builder,
project: project.clone(), };
prompt_builder, this.handle_project_shared(project.clone(), cx);
}; this.synchronize_contexts(cx);
this.handle_project_shared(project.clone(), cx); this.register_context_server_handlers(cx);
this.synchronize_contexts(cx); this.reload(cx).detach_and_log_err(cx);
this.register_context_server_handlers(cx); this
this.reload(cx).detach_and_log_err(cx); })?;
this
})?;
Ok(this) Ok(this)
}) })

View File

@@ -1,7 +1,7 @@
use anyhow::{Context as _, Result, anyhow}; use anyhow::{anyhow, Context as _, Result};
use collections::HashMap; use collections::HashMap;
use editor::ProposedChangesEditor; use editor::ProposedChangesEditor;
use futures::{TryFutureExt as _, future}; use futures::{future, TryFutureExt as _};
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString}; use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString};
use language::{AutoindentMode, Buffer, BufferSnapshot}; use language::{AutoindentMode, Buffer, BufferSnapshot};
use project::{Project, ProjectPath}; use project::{Project, ProjectPath};
@@ -548,7 +548,7 @@ mod tests {
use super::*; use super::*;
use gpui::App; use gpui::App;
use language::{ use language::{
Language, LanguageConfig, LanguageMatcher, language_settings::AllLanguageSettings, language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
}; };
use settings::SettingsStore; use settings::SettingsStore;
use ui::BorrowAppContext; use ui::BorrowAppContext;

View File

@@ -2,20 +2,20 @@ use crate::context_editor::ContextEditor;
use anyhow::Result; use anyhow::Result;
pub use assistant_slash_command::SlashCommand; pub use assistant_slash_command::SlashCommand;
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet}; use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
use editor::{CompletionProvider, Editor, ExcerptId}; use editor::{CompletionProvider, Editor};
use fuzzy::{StringMatchCandidate, match_strings}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window}; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, ToPoint}; use language::{Anchor, Buffer, ToPoint};
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation}; use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource};
use rope::Point; use rope::Point;
use std::{ use std::{
cell::RefCell, cell::RefCell,
ops::Range, ops::Range,
rc::Rc, rc::Rc,
sync::{ sync::{
Arc,
atomic::{AtomicBool, Ordering::SeqCst}, atomic::{AtomicBool, Ordering::SeqCst},
Arc,
}, },
}; };
use workspace::Workspace; use workspace::Workspace;
@@ -126,7 +126,6 @@ impl SlashCommandCompletionProvider {
)), )),
new_text, new_text,
label: command.label(cx), label: command.label(cx),
icon_path: None,
confirm, confirm,
source: CompletionSource::Custom, source: CompletionSource::Custom,
}) })
@@ -224,7 +223,6 @@ impl SlashCommandCompletionProvider {
last_argument_range.clone() last_argument_range.clone()
}, },
label: new_argument.label, label: new_argument.label,
icon_path: None,
new_text, new_text,
documentation: None, documentation: None,
confirm, confirm,
@@ -243,7 +241,6 @@ impl SlashCommandCompletionProvider {
impl CompletionProvider for SlashCommandCompletionProvider { impl CompletionProvider for SlashCommandCompletionProvider {
fn completions( fn completions(
&self, &self,
_excerpt_id: ExcerptId,
buffer: &Entity<Buffer>, buffer: &Entity<Buffer>,
buffer_position: Anchor, buffer_position: Anchor,
_: editor::CompletionContext, _: editor::CompletionContext,

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use assistant_slash_command::SlashCommandWorkingSet; use assistant_slash_command::SlashCommandWorkingSet;
use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity}; use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
use picker::{Picker, PickerDelegate, PickerEditorPosition}; use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip, prelude::*}; use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use crate::context_editor::ContextEditor; use crate::context_editor::ContextEditor;

View File

@@ -21,7 +21,6 @@ clap.workspace = true
client.workspace = true client.workspace = true
collections.workspace = true collections.workspace = true
context_server.workspace = true context_server.workspace = true
dap.workspace = true
env_logger.workspace = true env_logger.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true

View File

@@ -6,6 +6,15 @@ fn main() {
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7"); println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
println!("cargo:rerun-if-env-changed=ZED_BUNDLE");
if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") {
// Find WebRTC.framework in the Frameworks folder when running as part of an application bundle.
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
} else {
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
}
// Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+. // Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+.
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit"); println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit");

View File

@@ -79,25 +79,10 @@ impl Eval {
let start_time = std::time::SystemTime::now(); let start_time = std::time::SystemTime::now();
let (system_prompt_context, load_error) = cx
.update(|cx| {
assistant
.read(cx)
.thread
.read(cx)
.load_system_prompt_context(cx)
})?
.await;
if let Some(load_error) = load_error {
return Err(anyhow!("{:?}", load_error));
};
assistant.update(cx, |assistant, cx| { assistant.update(cx, |assistant, cx| {
assistant.thread.update(cx, |thread, cx| { assistant.thread.update(cx, |thread, cx| {
let context = vec![]; let context = vec![];
thread.insert_user_message(self.user_prompt.clone(), context, None, cx); thread.insert_user_message(self.user_prompt.clone(), context, None, cx);
thread.set_system_prompt_context(system_prompt_context);
thread.send_to_model(model, RequestKind::Chat, cx); thread.send_to_model(model, RequestKind::Chat, cx);
}); });
})?; })?;
@@ -120,7 +105,7 @@ impl Eval {
.count(); .count();
Ok(EvalOutput { Ok(EvalOutput {
diff, diff,
last_message: last_message.to_string(), last_message: last_message.text.clone(),
elapsed_time, elapsed_time,
assistant_response_count, assistant_response_count,
tool_use_counts: assistant.tool_use_counts.clone(), tool_use_counts: assistant.tool_use_counts.clone(),

View File

@@ -1,11 +1,10 @@
use anyhow::anyhow; use anyhow::anyhow;
use assistant_tool::ToolWorkingSet;
use assistant2::{RequestKind, Thread, ThreadEvent, ThreadStore}; use assistant2::{RequestKind, Thread, ThreadEvent, ThreadStore};
use assistant_tool::ToolWorkingSet;
use client::{Client, UserStore}; use client::{Client, UserStore};
use collections::HashMap; use collections::HashMap;
use dap::DapRegistry;
use futures::StreamExt; use futures::StreamExt;
use gpui::{App, AsyncApp, Entity, SemanticVersion, Subscription, Task, prelude::*}; use gpui::{prelude::*, App, AsyncApp, Entity, SemanticVersion, Subscription, Task};
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
@@ -51,7 +50,6 @@ impl HeadlessAssistant {
app_state.node_runtime.clone(), app_state.node_runtime.clone(),
app_state.user_store.clone(), app_state.user_store.clone(),
app_state.languages.clone(), app_state.languages.clone(),
Arc::new(DapRegistry::default()),
app_state.fs.clone(), app_state.fs.clone(),
env, env,
cx, cx,
@@ -91,7 +89,7 @@ impl HeadlessAssistant {
ThreadEvent::DoneStreaming => { ThreadEvent::DoneStreaming => {
let thread = thread.read(cx); let thread = thread.read(cx);
if let Some(message) = thread.messages().last() { if let Some(message) = thread.messages().last() {
println!("Message: {}", message.to_string()); println!("Message: {}", message.text,);
} }
if thread.all_tools_finished() { if thread.all_tools_finished() {
self.done_tx.send_blocking(Ok(())).unwrap() self.done_tx.send_blocking(Ok(())).unwrap()
@@ -130,7 +128,12 @@ impl HeadlessAssistant {
} }
} }
} }
_ => {} ThreadEvent::StreamedCompletion
| ThreadEvent::SummaryChanged
| ThreadEvent::StreamedAssistantText(_, _)
| ThreadEvent::MessageAdded(_)
| ThreadEvent::MessageEdited(_)
| ThreadEvent::MessageDeleted(_) => {}
} }
} }
} }
@@ -151,10 +154,7 @@ pub fn init(cx: &mut App) -> Arc<HeadlessAppState> {
cx.set_http_client(client.http_client().clone()); cx.set_http_client(client.http_client().clone());
let git_binary_path = None; let git_binary_path = None;
let fs = Arc::new(RealFs::new( let fs = Arc::new(RealFs::new(git_binary_path));
git_binary_path,
cx.background_executor().clone(),
));
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));

View File

@@ -6,7 +6,7 @@ use clap::Parser;
use eval::{Eval, EvalOutput}; use eval::{Eval, EvalOutput};
use futures::future; use futures::future;
use gpui::{Application, AsyncApp}; use gpui::{Application, AsyncApp};
use headless_assistant::{HeadlessAppState, authenticate_model_provider, find_model}; use headless_assistant::{authenticate_model_provider, find_model, HeadlessAppState};
use itertools::Itertools; use itertools::Itertools;
use judge::Judge; use judge::Judge;
use language_model::{LanguageModel, LanguageModelRegistry}; use language_model::{LanguageModel, LanguageModelRegistry};

View File

@@ -16,7 +16,6 @@ anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true anyhow.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
gpui.workspace = true gpui.workspace = true
indexmap.workspace = true
language_model.workspace = true language_model.workspace = true
lmstudio = { workspace = true, features = ["schemars"] } lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true log.workspace = true

View File

@@ -1,18 +0,0 @@
use std::sync::Arc;
use gpui::SharedString;
use indexmap::IndexMap;
/// A profile for the Zed Agent that controls its behavior.
#[derive(Debug, Clone)]
pub struct AgentProfile {
/// The name of the profile.
pub name: SharedString,
pub tools: IndexMap<Arc<str>, bool>,
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
}
#[derive(Debug, Clone, Default)]
pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>,
}

View File

@@ -1,23 +1,17 @@
mod agent_profile;
use std::sync::Arc; use std::sync::Arc;
use ::open_ai::Model as OpenAiModel; use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel; use anthropic::Model as AnthropicModel;
use anyhow::{Result, bail};
use deepseek::Model as DeepseekModel; use deepseek::Model as DeepseekModel;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; use feature_flags::FeatureFlagAppExt;
use gpui::{App, Pixels}; use gpui::{App, Pixels};
use indexmap::IndexMap;
use language_model::{CloudModel, LanguageModel}; use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel; use lmstudio::Model as LmStudioModel;
use ollama::Model as OllamaModel; use ollama::Model as OllamaModel;
use schemars::{JsonSchema, schema::Schema}; use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; use settings::{Settings, SettingsSources};
pub use crate::agent_profile::*;
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition { pub enum AssistantDockPosition {
@@ -27,15 +21,6 @@ pub enum AssistantDockPosition {
Bottom, Bottom,
} }
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NotifyWhenAgentWaiting {
#[default]
PrimaryScreen,
AllScreens,
Never,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")] #[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContentV1 { pub enum AssistantProviderContentV1 {
@@ -81,18 +66,10 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>, pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool, pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool, pub enable_experimental_live_diffs: bool,
pub default_profile: Arc<str>,
pub profiles: IndexMap<Arc<str>, AgentProfile>,
pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
} }
impl AssistantSettings { impl AssistantSettings {
pub fn are_live_diffs_enabled(&self, cx: &App) -> bool { pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
if cx.has_flag::<Assistant2FeatureFlag>() {
return false;
}
cx.is_staff() || self.enable_experimental_live_diffs cx.is_staff() || self.enable_experimental_live_diffs
} }
} }
@@ -110,8 +87,8 @@ impl JsonSchema for AssistantSettingsContent {
VersionedAssistantSettingsContent::schema_name() VersionedAssistantSettingsContent::schema_name()
} }
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema { fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
VersionedAssistantSettingsContent::json_schema(r#gen) VersionedAssistantSettingsContent::json_schema(gen)
} }
fn is_referenceable() -> bool { fn is_referenceable() -> bool {
@@ -189,10 +166,6 @@ impl AssistantSettingsContent {
editor_model: None, editor_model: None,
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
}, },
VersionedAssistantSettingsContent::V2(settings) => settings.clone(), VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
}, },
@@ -214,10 +187,6 @@ impl AssistantSettingsContent {
editor_model: None, editor_model: None,
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
}, },
} }
} }
@@ -324,51 +293,6 @@ impl AssistantSettingsContent {
} }
} }
} }
pub fn set_profile(&mut self, profile_id: Arc<str>) {
let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
self
else {
return;
};
settings.default_profile = Some(profile_id);
}
pub fn create_profile(&mut self, profile_id: Arc<str>, profile: AgentProfile) -> Result<()> {
let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
self
else {
return Ok(());
};
let profiles = settings.profiles.get_or_insert_default();
if profiles.contains_key(&profile_id) {
bail!("profile with ID '{profile_id}' already exists");
}
profiles.insert(
profile_id,
AgentProfileContent {
name: profile.name.into(),
tools: profile.tools,
context_servers: profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
},
);
Ok(())
}
} }
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -392,10 +316,6 @@ impl Default for VersionedAssistantSettingsContent {
editor_model: None, editor_model: None,
inline_alternatives: None, inline_alternatives: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
}) })
} }
} }
@@ -432,19 +352,6 @@ pub struct AssistantSettingsContentV2 {
/// ///
/// Default: false /// Default: false
enable_experimental_live_diffs: Option<bool>, enable_experimental_live_diffs: Option<bool>,
#[schemars(skip)]
default_profile: Option<Arc<str>>,
#[schemars(skip)]
pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
/// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it.
///
/// Default: false
always_allow_tool_actions: Option<bool>,
/// Where to show a popup notification when the agent is waiting for user input.
///
/// Default: "primary_screen"
notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@@ -454,7 +361,7 @@ pub struct LanguageModelSelection {
pub model: String, pub model: String,
} }
fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject { schemars::schema::SchemaObject {
enum_values: Some(vec![ enum_values: Some(vec![
"anthropic".into(), "anthropic".into(),
@@ -481,19 +388,6 @@ impl Default for LanguageModelSelection {
} }
} }
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileContent {
pub name: Arc<str>,
pub tools: IndexMap<Arc<str>, bool>,
#[serde(default)]
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
}
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ContextServerPresetContent {
pub tools: IndexMap<Arc<str>, bool>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV1 { pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled. /// Whether the Assistant is enabled.
@@ -588,41 +482,6 @@ impl Settings for AssistantSettings {
&mut settings.enable_experimental_live_diffs, &mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs, value.enable_experimental_live_diffs,
); );
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
);
merge(
&mut settings.notify_when_agent_waiting,
value.notify_when_agent_waiting,
);
merge(&mut settings.default_profile, value.default_profile);
if let Some(profiles) = value.profiles {
settings
.profiles
.extend(profiles.into_iter().map(|(id, profile)| {
(
id,
AgentProfile {
name: profile.name.into(),
tools: profile.tools,
context_servers: profile
.context_servers
.into_iter()
.map(|(context_server_id, preset)| {
(
context_server_id,
ContextServerPreset {
tools: preset.tools.clone(),
},
)
})
.collect(),
},
)
}));
}
} }
Ok(settings) Ok(settings)
@@ -687,10 +546,6 @@ mod tests {
default_width: None, default_width: None,
default_height: None, default_height: None,
enable_experimental_live_diffs: None, enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
}), }),
) )
}, },

View File

@@ -6,17 +6,17 @@ pub use crate::extension_slash_command::*;
pub use crate::slash_command_registry::*; pub use crate::slash_command_registry::*;
pub use crate::slash_command_working_set::*; pub use crate::slash_command_working_set::*;
use anyhow::Result; use anyhow::Result;
use futures::StreamExt;
use futures::stream::{self, BoxStream}; use futures::stream::{self, BoxStream};
use futures::StreamExt;
use gpui::{App, SharedString, Task, WeakEntity, Window}; use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role; pub use language_model::Role;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
ops::Range, ops::Range,
sync::{Arc, atomic::AtomicBool}, sync::{atomic::AtomicBool, Arc},
}; };
use workspace::{Workspace, ui::IconName}; use workspace::{ui::IconName, Workspace};
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
SlashCommandRegistry::default_global(cx); SlashCommandRegistry::default_global(cx);

View File

@@ -1,5 +1,5 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, atomic::AtomicBool}; use std::sync::{atomic::AtomicBool, Arc};
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;

View File

@@ -29,9 +29,13 @@ html_to_markdown.workspace = true
http_client.workspace = true http_client.workspace = true
indexed_docs.workspace = true indexed_docs.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true
log.workspace = true
project.workspace = true project.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
rope.workspace = true rope.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
smol.workspace = true smol.workspace = true

View File

@@ -1,3 +1,4 @@
mod auto_command;
mod cargo_workspace_command; mod cargo_workspace_command;
mod context_server_command; mod context_server_command;
mod default_command; mod default_command;
@@ -7,7 +8,9 @@ mod docs_command;
mod fetch_command; mod fetch_command;
mod file_command; mod file_command;
mod now_command; mod now_command;
mod project_command;
mod prompt_command; mod prompt_command;
mod search_command;
mod selection_command; mod selection_command;
mod streaming_example_command; mod streaming_example_command;
mod symbols_command; mod symbols_command;
@@ -18,6 +21,7 @@ use gpui::App;
use language::{CodeLabel, HighlightId}; use language::{CodeLabel, HighlightId};
use ui::ActiveTheme as _; use ui::ActiveTheme as _;
pub use crate::auto_command::*;
pub use crate::cargo_workspace_command::*; pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*; pub use crate::context_server_command::*;
pub use crate::default_command::*; pub use crate::default_command::*;
@@ -27,7 +31,9 @@ pub use crate::docs_command::*;
pub use crate::fetch_command::*; pub use crate::fetch_command::*;
pub use crate::file_command::*; pub use crate::file_command::*;
pub use crate::now_command::*; pub use crate::now_command::*;
pub use crate::project_command::*;
pub use crate::prompt_command::*; pub use crate::prompt_command::*;
pub use crate::search_command::*;
pub use crate::selection_command::*; pub use crate::selection_command::*;
pub use crate::streaming_example_command::*; pub use crate::streaming_example_command::*;
pub use crate::symbols_command::*; pub use crate::symbols_command::*;

View File

@@ -0,0 +1,371 @@
use anyhow::{anyhow, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use feature_flags::FeatureFlag;
use futures::StreamExt;
use gpui::{App, AsyncApp, Task, WeakEntity, Window};
use language::{CodeLabel, LspAdapterDelegate};
use language_model::{
LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, Role,
};
use semantic_index::{FileSummary, SemanticDb};
use smol::channel;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, BorrowAppContext};
use util::ResultExt;
use workspace::Workspace;
use crate::create_label_for_command;
pub struct AutoSlashCommandFeatureFlag;
impl FeatureFlag for AutoSlashCommandFeatureFlag {
const NAME: &'static str = "auto-slash-command";
}
pub struct AutoCommand;
impl SlashCommand for AutoCommand {
fn name(&self) -> String {
"auto".into()
}
fn description(&self) -> String {
"Automatically infer what context to add".into()
}
fn icon(&self) -> IconName {
IconName::Wand
}
fn menu_text(&self) -> String {
self.description()
}
fn label(&self, cx: &App) -> CodeLabel {
create_label_for_command("auto", &["--prompt"], cx)
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
workspace: Option<WeakEntity<Workspace>>,
_window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<ArgumentCompletion>>> {
// There's no autocomplete for a prompt, since it's arbitrary text.
// However, we can use this opportunity to kick off a drain of the backlog.
// That way, it can hopefully be done resummarizing by the time we've actually
// typed out our prompt. This re-runs on every keystroke during autocomplete,
// but in the future, we could instead do it only once, when /auto is first entered.
let Some(workspace) = workspace.and_then(|ws| ws.upgrade()) else {
log::warn!("workspace was dropped or unavailable during /auto autocomplete");
return Task::ready(Ok(Vec::new()));
};
let project = workspace.read(cx).project().clone();
let Some(project_index) =
cx.update_global(|index: &mut SemanticDb, cx| index.project_index(project, cx))
else {
return Task::ready(Err(anyhow!("No project indexer, cannot use /auto")));
};
let cx: &mut App = cx;
cx.spawn(async move |cx| {
let task = project_index.read_with(cx, |project_index, cx| {
project_index.flush_summary_backlogs(cx)
})?;
cx.background_spawn(task).await;
anyhow::Ok(Vec::new())
})
}
fn requires_argument(&self) -> bool {
true
}
fn run(
self: Arc<Self>,
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakEntity<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
window: &mut Window,
cx: &mut App,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
if arguments.is_empty() {
return Task::ready(Err(anyhow!("missing prompt")));
};
let argument = arguments.join(" ");
let original_prompt = argument.to_string();
let project = workspace.read(cx).project().clone();
let Some(project_index) =
cx.update_global(|index: &mut SemanticDb, cx| index.project_index(project, cx))
else {
return Task::ready(Err(anyhow!("no project indexer")));
};
let task = window.spawn(cx, async move |cx| {
let summaries = project_index
.read_with(cx, |project_index, cx| project_index.all_summaries(cx))?
.await?;
commands_for_summaries(&summaries, &original_prompt, &cx).await
});
// As a convenience, append /auto's argument to the end of the prompt
// so you don't have to write it again.
let original_prompt = argument.to_string();
cx.background_spawn(async move {
let commands = task.await?;
let mut prompt = String::new();
log::info!(
"Translating this response into slash-commands: {:?}",
commands
);
for command in commands {
prompt.push('/');
prompt.push_str(&command.name);
prompt.push(' ');
prompt.push_str(&command.arg);
prompt.push('\n');
}
prompt.push('\n');
prompt.push_str(&original_prompt);
Ok(SlashCommandOutput {
text: prompt,
sections: Vec::new(),
run_commands_in_text: true,
}
.to_event_stream())
})
}
}
const PROMPT_INSTRUCTIONS_BEFORE_SUMMARY: &str = include_str!("prompt_before_summary.txt");
const PROMPT_INSTRUCTIONS_AFTER_SUMMARY: &str = include_str!("prompt_after_summary.txt");
fn summaries_prompt(summaries: &[FileSummary], original_prompt: &str) -> String {
let json_summaries = serde_json::to_string(summaries).unwrap();
format!("{PROMPT_INSTRUCTIONS_BEFORE_SUMMARY}\n{json_summaries}\n{PROMPT_INSTRUCTIONS_AFTER_SUMMARY}\n{original_prompt}")
}
/// The slash commands that the model is told about, and which we look for in the inference response.
const SUPPORTED_SLASH_COMMANDS: &[&str] = &["search", "file"];
#[derive(Debug, Clone)]
struct CommandToRun {
name: String,
arg: String,
}
/// Given the pre-indexed file summaries for this project, as well as the original prompt
/// string passed to `/auto`, get a list of slash commands to run, along with their arguments.
///
/// The prompt's output does not include the slashes (to reduce the chance that it makes a mistake),
/// so taking one of these returned Strings and turning it into a real slash-command-with-argument
/// involves prepending a slash to it.
///
/// This function will validate that each of the returned lines begins with one of SUPPORTED_SLASH_COMMANDS.
/// Any other lines it encounters will be discarded, with a warning logged.
async fn commands_for_summaries(
summaries: &[FileSummary],
original_prompt: &str,
cx: &AsyncApp,
) -> Result<Vec<CommandToRun>> {
if summaries.is_empty() {
log::warn!("Inferring no context because there were no summaries available.");
return Ok(Vec::new());
}
// Use the globally configured model to translate the summaries into slash-commands,
// because Qwen2-7B-Instruct has not done a good job at that task.
let Some(model) = cx.update(|cx| LanguageModelRegistry::read_global(cx).active_model())? else {
log::warn!("Can't infer context because there's no active model.");
return Ok(Vec::new());
};
// Only go up to 90% of the actual max token count, to reduce chances of
// exceeding the token count due to inaccuracies in the token counting heuristic.
let max_token_count = (model.max_token_count() * 9) / 10;
// Rather than recursing (which would require this async function use a pinned box),
// we use an explicit stack of arguments and answers for when we need to "recurse."
let mut stack = vec![summaries];
let mut final_response = Vec::new();
let mut prompts = Vec::new();
// TODO We only need to create multiple Requests because we currently
// don't have the ability to tell if a CompletionProvider::complete response
// was a "too many tokens in this request" error. If we had that, then
// we could try the request once, instead of having to make separate requests
// to check the token count and then afterwards to run the actual prompt.
let make_request = |prompt: String| LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
// Nothing in here will benefit from caching
cache: false,
}],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
};
while let Some(current_summaries) = stack.pop() {
// The split can result in one slice being empty and the other having one element.
// Whenever that happens, skip the empty one.
if current_summaries.is_empty() {
continue;
}
log::info!(
"Inferring prompt context using {} file summaries",
current_summaries.len()
);
let prompt = summaries_prompt(&current_summaries, original_prompt);
let start = std::time::Instant::now();
// Per OpenAI, 1 token ~= 4 chars in English (we go with 4.5 to overestimate a bit, because failed API requests cost a lot of perf)
// Verifying this against an actual model.count_tokens() confirms that it's usually within ~5% of the correct answer, whereas
// getting the correct answer from tiktoken takes hundreds of milliseconds (compared to this arithmetic being ~free).
// source: https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
let token_estimate = prompt.len() * 2 / 9;
let duration = start.elapsed();
log::info!(
"Time taken to count tokens for prompt of length {:?}B: {:?}",
prompt.len(),
duration
);
if token_estimate < max_token_count {
prompts.push(prompt);
} else if current_summaries.len() == 1 {
log::warn!("Inferring context for a single file's summary failed because the prompt's token length exceeded the model's token limit.");
} else {
log::info!(
"Context inference using file summaries resulted in a prompt containing {token_estimate} tokens, which exceeded the model's max of {max_token_count}. Retrying as two separate prompts, each including half the number of summaries.",
);
let (left, right) = current_summaries.split_at(current_summaries.len() / 2);
stack.push(right);
stack.push(left);
}
}
let all_start = std::time::Instant::now();
let (tx, rx) = channel::bounded(1024);
let completion_streams = prompts
.into_iter()
.map(|prompt| {
let request = make_request(prompt.clone());
let model = model.clone();
let tx = tx.clone();
let stream = model.stream_completion(request, &cx);
(stream, tx)
})
.collect::<Vec<_>>();
cx.background_spawn(async move {
let futures = completion_streams
.into_iter()
.enumerate()
.map(|(ix, (stream, tx))| async move {
let start = std::time::Instant::now();
let events = stream.await?;
log::info!("Time taken for awaiting /await chunk stream #{ix}: {:?}", start.elapsed());
let completion: String = events
.filter_map(|event| async {
if let Ok(LanguageModelCompletionEvent::Text(text)) = event {
Some(text)
} else {
None
}
})
.collect()
.await;
log::info!("Time taken for all /auto chunks to come back for #{ix}: {:?}", start.elapsed());
for line in completion.split('\n') {
if let Some(first_space) = line.find(' ') {
let command = &line[..first_space].trim();
let arg = &line[first_space..].trim();
tx.send(CommandToRun {
name: command.to_string(),
arg: arg.to_string(),
})
.await?;
} else if !line.trim().is_empty() {
// All slash-commands currently supported in context inference need a space for the argument.
log::warn!(
"Context inference returned a non-blank line that contained no spaces (meaning no argument for the slash command): {:?}",
line
);
}
}
anyhow::Ok(())
})
.collect::<Vec<_>>();
let _ = futures::future::try_join_all(futures).await.log_err();
let duration = all_start.elapsed();
eprintln!("All futures completed in {:?}", duration);
})
.await;
drop(tx); // Close the channel so that rx.collect() won't hang. This is safe because all futures have completed.
let results = rx.collect::<Vec<_>>().await;
eprintln!(
"Finished collecting from the channel with {} results",
results.len()
);
for command in results {
// Don't return empty or duplicate commands
if !command.name.is_empty()
&& !final_response
.iter()
.any(|cmd: &CommandToRun| cmd.name == command.name && cmd.arg == command.arg)
{
if SUPPORTED_SLASH_COMMANDS
.iter()
.any(|supported| &command.name == supported)
{
final_response.push(command);
} else {
log::warn!(
"Context inference returned an unrecognized slash command: {:?}",
command
);
}
}
}
// Sort the commands by name (reversed just so that /search appears before /file)
final_response.sort_by(|cmd1, cmd2| cmd1.name.cmp(&cmd2.name).reverse());
Ok(final_response)
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow}; use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult, SlashCommandResult,
@@ -10,7 +10,7 @@ use project::{Project, ProjectPath};
use std::{ use std::{
fmt::Write, fmt::Write,
path::Path, path::Path,
sync::{Arc, atomic::AtomicBool}, sync::{atomic::AtomicBool, Arc},
}; };
use ui::prelude::*; use ui::prelude::*;
use workspace::Workspace; use workspace::Workspace;

View File

@@ -1,4 +1,4 @@
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use assistant_slash_command::{ use assistant_slash_command::{
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput, AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
SlashCommandOutputSection, SlashCommandResult, SlashCommandOutputSection, SlashCommandResult,
@@ -10,8 +10,8 @@ use context_server::{
}; };
use gpui::{App, Entity, Task, WeakEntity, Window}; use gpui::{App, Entity, Task, WeakEntity, Window};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use text::LineEnding; use text::LineEnding;
use ui::{IconName, SharedString}; use ui::{IconName, SharedString};
use workspace::Workspace; use workspace::Workspace;

View File

@@ -1,4 +1,4 @@
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use assistant_slash_command::{ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult, SlashCommandResult,
@@ -8,7 +8,7 @@ use language::{BufferSnapshot, LspAdapterDelegate};
use prompt_store::PromptStore; use prompt_store::PromptStore;
use std::{ use std::{
fmt::Write, fmt::Write,
sync::{Arc, atomic::AtomicBool}, sync::{atomic::AtomicBool, Arc},
}; };
use ui::prelude::*; use ui::prelude::*;
use workspace::Workspace; use workspace::Workspace;

View File

@@ -1,5 +1,5 @@
use crate::file_command::{FileCommandMetadata, FileSlashCommand}; use crate::file_command::{FileCommandMetadata, FileSlashCommand};
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use assistant_slash_command::{ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult, SlashCommandResult,
@@ -8,7 +8,7 @@ use collections::HashSet;
use futures::future; use futures::future;
use gpui::{App, Task, WeakEntity, Window}; use gpui::{App, Task, WeakEntity, Window};
use language::{BufferSnapshot, LspAdapterDelegate}; use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::{Arc, atomic::AtomicBool}; use std::sync::{atomic::AtomicBool, Arc};
use text::OffsetRangeExt; use text::OffsetRangeExt;
use ui::prelude::*; use ui::prelude::*;
use workspace::Workspace; use workspace::Workspace;

View File

@@ -1,4 +1,4 @@
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use assistant_slash_command::{ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult, SlashCommandResult,
@@ -14,11 +14,11 @@ use rope::Point;
use std::{ use std::{
fmt::Write, fmt::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, atomic::AtomicBool}, sync::{atomic::AtomicBool, Arc},
}; };
use ui::prelude::*; use ui::prelude::*;
use util::ResultExt;
use util::paths::PathMatcher; use util::paths::PathMatcher;
use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
use crate::create_label_for_command; use crate::create_label_for_command;

View File

@@ -1,9 +1,9 @@
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use anyhow::{Result, anyhow, bail}; use anyhow::{anyhow, bail, Result};
use assistant_slash_command::{ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult, SlashCommandResult,
@@ -16,7 +16,7 @@ use indexed_docs::{
use language::{BufferSnapshot, LspAdapterDelegate}; use language::{BufferSnapshot, LspAdapterDelegate};
use project::{Project, ProjectPath}; use project::{Project, ProjectPath};
use ui::prelude::*; use ui::prelude::*;
use util::{ResultExt, maybe}; use util::{maybe, ResultExt};
use workspace::Workspace; use workspace::Workspace;
pub struct DocsSlashCommand; pub struct DocsSlashCommand;

View File

@@ -1,16 +1,16 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult, SlashCommandResult,
}; };
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::{Task, WeakEntity}; use gpui::{Task, WeakEntity};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{BufferSnapshot, LspAdapterDelegate}; use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*; use ui::prelude::*;

View File

@@ -1,10 +1,10 @@
use anyhow::{Context as _, Result, anyhow}; use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{ use assistant_slash_command::{
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult, SlashCommandOutput, SlashCommandOutputSection, SlashCommandResult,
}; };
use futures::Stream;
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::Stream;
use fuzzy::PathMatch; use fuzzy::PathMatch;
use gpui::{App, Entity, Task, WeakEntity}; use gpui::{App, Entity, Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate}; use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
@@ -15,7 +15,7 @@ use std::{
fmt::Write, fmt::Write,
ops::{Range, RangeInclusive}, ops::{Range, RangeInclusive},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, atomic::AtomicBool}, sync::{atomic::AtomicBool, Arc},
}; };
use ui::prelude::*; use ui::prelude::*;
use util::ResultExt; use util::ResultExt;
@@ -221,7 +221,7 @@ fn collect_files(
project: Entity<Project>, project: Entity<Project>,
glob_inputs: &[String], glob_inputs: &[String],
cx: &mut App, cx: &mut App,
) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> { ) -> impl Stream<Item = Result<SlashCommandEvent>> {
let Ok(matchers) = glob_inputs let Ok(matchers) = glob_inputs
.into_iter() .into_iter()
.map(|glob_input| { .map(|glob_input| {
@@ -694,21 +694,15 @@ mod test {
assert_eq!(result.sections.len(), 7); assert_eq!(result.sections.len(), 7);
// Ensure that full file paths are included in the real output // Ensure that full file paths are included in the real output
assert!( assert!(result
result .text
.text .contains(separator!("zed/assets/themes/andromeda/LICENSE")));
.contains(separator!("zed/assets/themes/andromeda/LICENSE")) assert!(result
); .text
assert!( .contains(separator!("zed/assets/themes/ayu/LICENSE")));
result assert!(result
.text .text
.contains(separator!("zed/assets/themes/ayu/LICENSE")) .contains(separator!("zed/assets/themes/summercamp/LICENSE")));
);
assert!(
result
.text
.contains(separator!("zed/assets/themes/summercamp/LICENSE"))
);
assert_eq!(result.sections[5].label, "summercamp"); assert_eq!(result.sections[5].label, "summercamp");
@@ -788,12 +782,7 @@ mod test {
assert_eq!(result.sections[6].label, "summercamp"); assert_eq!(result.sections[6].label, "summercamp");
assert_eq!(result.sections[7].label, separator!("zed/assets/themes")); assert_eq!(result.sections[7].label, separator!("zed/assets/themes"));
assert_eq!( assert_eq!(result.text, separator!("zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"));
result.text,
separator!(
"zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"
)
);
// Ensure that the project lasts until after the last await // Ensure that the project lasts until after the last await
drop(project); drop(project);

Some files were not shown because too many files have changed in this diff Show More