Compare commits

..

1 Commits

Author SHA1 Message Date
Cole Miller
73e479d8e9 Create parent of .zed/debug.json if needed 2025-05-12 10:48:49 +02:00
621 changed files with 9525 additions and 20240 deletions

View File

@@ -13,6 +13,12 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-args=-all_load"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-all_load"]
[target.'cfg(target_os = "windows")']
rustflags = [
"--cfg",

View File

@@ -30,7 +30,3 @@ ffdda588b41f7d9d270ffe76cab116f828ad545e
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
# https://github.com/zed-industries/zed/pull/13887
813cc3f5e537372fc86720b5e71b6e1c815440ab
# 2024-07-24 docs: Format docs
# https://github.com/zed-industries/zed/pull/15352
3a44a59f8ec114ac1ba22f7da1652717ef7e4e5c

View File

@@ -29,8 +29,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -29,8 +29,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -28,8 +28,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -1,35 +0,0 @@
name: Bug Report (Debugger)
description: Zed Debugger-Related Bugs
type: "Bug"
labels: ["debugger"]
title: "Debugger: <a short description of the Debugger bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -49,8 +49,8 @@ body:
attributes:
label: Zed Version and System Specs
description: |
Open Zed, from the command palette select "zed: copy system specs into clipboard"
Open Zed, from the command palette select "zed: Copy System Specs Into Clipboard"
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -26,9 +26,9 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true
- type: textarea

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
**/cargo-target
**/target
**/venv
**/.direnv
*.wasm
*.xcodeproj
.DS_Store

View File

@@ -19,8 +19,6 @@ amtoaer <amtoaer@gmail.com>
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
Andrei Zvonimir Crnković <andrei@0x7f.dev>
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
Angelk90 <angelo.k90@hotmail.it>
Angelk90 <angelo.k90@hotmail.it> <20476002+Angelk90@users.noreply.github.com>
Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Ben Kunkle <ben@zed.dev>
@@ -40,8 +38,6 @@ Dairon Medina <dairon.medina@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Edwin Aronsson <75266237+4teapo@users.noreply.github.com>
Elvis Pranskevichus <elvis@geldata.com>
Elvis Pranskevichus <elvis@geldata.com> <elvis@magic.io>
Evren Sen <nervenes@icloud.com>
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
@@ -73,8 +69,6 @@ Lilith Iris <itslirissama@gmail.com> <83819417+Irilith@users.noreply.github.com>
LoganDark <contact@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marko Kungla <marko.kungla@gmail.com>
Marko Kungla <marko.kungla@gmail.com> <marko@mkungla.dev>
Marshall Bowers <git@maxdeviant.com>
Marshall Bowers <git@maxdeviant.com> <elliott.codes@gmail.com>
Marshall Bowers <git@maxdeviant.com> <marshall@zed.dev>
@@ -90,7 +84,6 @@ Michael Sloan <michael@zed.dev> <mgsloan@google.com>
Mikayla Maki <mikayla@zed.dev>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
Morgan Krey <morgan@zed.dev>
Muhammad Talal Anwar <mail@talal.io>
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
Nate Butler <iamnbutler@gmail.com>
@@ -123,18 +116,11 @@ Shish <webmaster@shishnet.org>
Shish <webmaster@shishnet.org> <shish@shishnet.org>
Smit Barmase <0xtimsb@gmail.com>
Smit Barmase <0xtimsb@gmail.com> <smit@zed.dev>
Thomas <github.thomaub@gmail.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry94@gmail.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry@paylead.fr>
Thomas Heartman <thomasheartman+github@gmail.com>
Thomas Heartman <thomasheartman+github@gmail.com> <thomas@getunleash.io>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com> <thomas@zed.dev>
Thorben Kröger <dev@thorben.net>
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
Thorsten Ball <mrnugget@gmail.com>
Thorsten Ball <mrnugget@gmail.com> <me@thorstenball.com>
Thorsten Ball <mrnugget@gmail.com> <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
Tristan Hume <tris.hume@gmail.com>
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
Uladzislau Kaminski <i@uladkaminski.com>

2
.rules
View File

@@ -115,7 +115,7 @@ Other entities can then register a callback to handle these events by doing `cx.
GPUI has had some changes to its APIs. Always write code using the new APIs:
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which no longer exist and should NEVER be used.
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which longer exists and should NEVER be used.
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.

View File

@@ -2,14 +2,16 @@
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
"program": "target/debug/zed",
"request": "launch"
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "Debug Zed (GDB)",
"adapter": "GDB",
"program": "target/debug/zed",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",
"initialize_args": {
"stopAtBeginningOfMainSubprogram": true
}

1523
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,6 @@ members = [
"crates/dap",
"crates/dap_adapters",
"crates/db",
"crates/debug_adapter_extension",
"crates/debugger_tools",
"crates/debugger_ui",
"crates/deepseek",
@@ -74,12 +73,11 @@ members = [
"crates/inline_completion",
"crates/inline_completion_button",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_model_selector",
"crates/language_models",
"crates/language_selector",
"crates/language_tools",
@@ -245,7 +243,6 @@ credentials_provider = { path = "crates/credentials_provider" }
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
debug_adapter_extension = { path = "crates/debug_adapter_extension" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }
@@ -280,12 +277,11 @@ indexed_docs = { path = "crates/indexed_docs" }
inline_completion = { path = "crates/inline_completion" }
inline_completion_button = { path = "crates/inline_completion_button" }
install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_model_selector = { path = "crates/language_model_selector" }
language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
@@ -428,7 +424,6 @@ convert_case = "0.8.0"
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
dashmap = "6.0"
@@ -461,7 +456,6 @@ indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
jj-lib = { git = "https://github.com/jj-vcs/jj", rev = "e18eb8e05efaa153fad5ef46576af145bba1807f" }
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -469,12 +463,12 @@ jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,r
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
linkme = "0.3.31"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
markup5ever_rcdom = "0.3.0"
metal = "0.29"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -553,9 +547,9 @@ syn = { version = "1.0.72", features = ["full", "extra-traits"] }
sys-locale = "0.3.1"
sysinfo = "0.31.0"
take-until = "0.2.0"
tempfile = "3.20.0"
tempfile = "3.9.0"
thiserror = "2.0.12"
tiktoken-rs = "0.7.0"
tiktoken-rs = "0.6.0"
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -599,7 +593,6 @@ url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
wasi-preview1-component-adapter-provider = "29"
wasm-encoder = "0.221"
wasmparser = "0.221"
wasmtime = { version = "29", default-features = false, features = [
@@ -608,8 +601,6 @@ wasmtime = { version = "29", default-features = false, features = [
"runtime",
"cranelift",
"component-model",
"incremental-cache",
"parallel-compilation",
] }
wasmtime-wasi = "29"
which = "6.0.0"
@@ -795,9 +786,6 @@ let_underscore_future = "allow"
# running afoul of the borrow checker.
too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",
@@ -805,6 +793,7 @@ ignored = [
"prost_build",
"serde",
"component",
"linkme",
"documented",
"workspace-hack",
]

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.87-bookworm as builder
FROM rust:1.86-bookworm as builder
WORKDIR app
COPY . .

View File

@@ -8,6 +8,10 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of
### Installation
<a href="https://repology.org/project/zed-editor/versions">
<img src="https://repology.org/badge/vertical-allrepos/zed-editor.svg?minversion=0.143.5" alt="Packaging status" align="right">
</a>
On macOS and Linux you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
Other platforms are not yet available:

View File

@@ -244,7 +244,7 @@
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-o": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus"
}
@@ -512,8 +512,6 @@
"alt-ctrl-o": "projects::OpenRecent",
"alt-shift-open": "projects::OpenRemote",
"alt-ctrl-shift-o": "projects::OpenRemote",
// Change to open path modal for existing remote connection by setting the parameter
// "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
"alt-ctrl-shift-b": "branches::OpenRecent",
"alt-shift-enter": "toast::RunAction",
"ctrl-~": "workspace::NewTerminal",
@@ -768,7 +766,7 @@
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"
@@ -930,7 +928,6 @@
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
@@ -981,12 +978,5 @@
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
},
{
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
}
}
]

View File

@@ -290,7 +290,7 @@
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-o": "agent::ToggleNavigationMenu",
"cmd-shift-i": "agent::ToggleOptionsMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"shift-escape": "agent::ExpandMessageEditor",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus"
}
@@ -588,7 +588,6 @@
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
"alt-cmd-o": "projects::OpenRecent",
"ctrl-cmd-o": "projects::OpenRemote",
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true }],
"alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
@@ -826,7 +825,7 @@
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
"cmd-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"
@@ -1012,7 +1011,7 @@
"alt-right": ["terminal::SendText", "\u001bf"],
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
"alt-.": ["terminal::SendText", "\u001b."],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": ["terminal::SendKeystroke", "up"],
@@ -1085,12 +1084,5 @@
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
},
{
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
}
}
]

View File

@@ -72,9 +72,7 @@
"alt-left": "editor::SelectToPreviousWordStart",
"alt-right": "editor::SelectToNextWordEnd",
"pagedown": "editor::SelectPageDown",
"ctrl-v": "editor::SelectPageDown",
"pageup": "editor::SelectPageUp",
"alt-v": "editor::SelectPageUp",
"ctrl-f": "editor::SelectRight",
"ctrl-b": "editor::SelectLeft",
"ctrl-n": "editor::SelectDown",

View File

@@ -51,7 +51,9 @@
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"f3": "editor::FindNextMatch",
"shift-f3": "editor::FindPreviousMatch"
}
},
{

View File

@@ -72,9 +72,7 @@
"alt-left": "editor::SelectToPreviousWordStart",
"alt-right": "editor::SelectToNextWordEnd",
"pagedown": "editor::SelectPageDown",
"ctrl-v": "editor::SelectPageDown",
"pageup": "editor::SelectPageUp",
"alt-v": "editor::SelectPageUp",
"ctrl-f": "editor::SelectRight",
"ctrl-b": "editor::SelectLeft",
"ctrl-n": "editor::SelectDown",

View File

@@ -53,7 +53,9 @@
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cmd-g": "editor::FindNextMatch",
"cmd-shift-g": "editor::FindPreviousMatch"
}
},
{

View File

@@ -152,7 +152,6 @@
"g end": ["vim::EndOfLine", { "display_lines": true }],
"g 0": ["vim::StartOfLine", { "display_lines": true }],
"g home": ["vim::StartOfLine", { "display_lines": true }],
"g shift-m": ["vim::MiddleOfLine", { "display_lines": true }],
"g ^": ["vim::FirstNonWhitespace", { "display_lines": true }],
"g v": "vim::RestoreVisualSelection",
"g ]": "editor::GoToDiagnostic",

View File

@@ -113,8 +113,8 @@
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Time to wait in milliseconds before showing the informational hover box.
"hover_popover_delay": 300,
// Time to wait before showing the informational hover box
"hover_popover_delay": 350,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
@@ -328,16 +328,10 @@
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
"show_branch_icon": false,
// Whether to show the branch name button in the titlebar.
"show_branch_name": true,
// Whether to show the project host and name in the titlebar.
"show_project_items": true,
// Whether to show onboarding banners in the titlebar.
"show_onboarding_banner": true,
// Whether to show user picture in the titlebar.
"show_user_picture": true,
// Whether to show the sign in button in the titlebar.
"show_sign_in": true
"show_user_picture": true
},
// Scrollbar related settings
"scrollbar": {
@@ -476,8 +470,6 @@
"search_wrap": true,
// Search options to enable by default when opening new project and buffer searches.
"search": {
// Whether to show the project search button in the status bar.
"button": true,
"whole_word": false,
"case_sensitive": false,
"include_ignored": false,
@@ -758,8 +750,6 @@
"stream_edits": false,
// When enabled, agent edits will be displayed in single-file editors for review
"single_file_review": true,
// When enabled, show voting thumbs for feedback on agent edits.
"enable_feedback": true,
"default_profile": "write",
"profiles": {
"write": {
@@ -1012,8 +1002,6 @@
"auto_update": true,
// Diagnostics configuration.
"diagnostics": {
// Whether to show the project diagnostics button in the status bar.
"button": true,
// Whether to show warnings or not by default.
"include_warnings": true,
// Settings for inline diagnostics
@@ -1715,8 +1703,6 @@
// }
// ]
"ssh_connections": [],
// Whether to read ~/.ssh/config for ssh connection sources.
"read_ssh_config": true,
// Configures context servers for use by the agent.
"context_servers": {},
"debugger": {

View File

@@ -47,11 +47,12 @@ heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
inventory.workspace = true
itertools.workspace = true
jsonschema.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
linkme.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true

View File

@@ -33,9 +33,7 @@ use language_model::{
LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, Role, StopReason,
};
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
use markdown::{
HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, PathWithRange,
};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
use project::{ProjectEntryId, ProjectItem as _};
use rope::Point;
use settings::{Settings as _, SettingsStore, update_settings_file};
@@ -185,14 +183,12 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = window.text_style();
let line_height = buffer_font_size * 1.75;
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
line_height: Some(line_height.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
@@ -333,6 +329,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
}
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 10;
fn render_markdown_code_block(
message_id: MessageId,
@@ -345,20 +342,17 @@ fn render_markdown_code_block(
_window: &Window,
cx: &App,
) -> Div {
let label_size = rems(0.8125);
let label = match kind {
CodeBlockKind::Indented => None,
CodeBlockKind::Fenced => Some(
h_flex()
.px_1()
.gap_1()
.child(
Icon::new(IconName::Code)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(div().text_size(label_size).child("Plain Text"))
.child(Label::new("untitled").size(LabelSize::Small))
.into_any_element(),
),
CodeBlockKind::FencedLang(raw_language_name) => Some(render_code_language(
@@ -387,36 +381,28 @@ fn render_markdown_code_block(
)
} else {
let content = if let Some(parent) = path_range.path.parent() {
let file_name = file_name.to_string_lossy().to_string();
let path = parent.to_string_lossy().to_string();
let path_and_file = format!("{}/{}", path, file_name);
h_flex()
.id(("code-block-header-label", ix))
.ml_1()
.gap_1()
.child(div().text_size(label_size).child(file_name))
.child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Jump to File",
None,
path_and_file.clone(),
window,
cx,
)
})
.child(
Label::new(file_name.to_string_lossy().to_string())
.size(LabelSize::Small),
)
.child(
Label::new(parent.to_string_lossy().to_string())
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any_element()
} else {
div()
Label::new(path_range.path.to_string_lossy().to_string())
.size(LabelSize::Small)
.ml_1()
.text_size(label_size)
.child(path_range.path.to_string_lossy().to_string())
.into_any_element()
};
h_flex()
.id(("code-block-header-button", ix))
.id(("code-block-header-label", ix))
.w_full()
.max_w_full()
.px_1()
@@ -424,6 +410,7 @@ fn render_markdown_code_block(
.cursor_pointer()
.rounded_sm()
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
.tooltip(Tooltip::text("Jump to File"))
.child(
h_flex()
.gap_0p5()
@@ -443,8 +430,49 @@ fn render_markdown_code_block(
let path_range = path_range.clone();
move |_, window, cx| {
workspace
.update(cx, |workspace, cx| {
open_path(&path_range, window, workspace, cx)
.update(cx, {
|workspace, cx| {
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
else {
return;
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task = workspace.open_path(
project_path,
None,
true,
window,
cx,
);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) =
item.downcast::<Editor>()
{
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
target, window, cx,
);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
}
@@ -459,157 +487,124 @@ fn render_markdown_code_block(
.copied_code_block_ids
.contains(&(message_id, ix));
let is_expanded = active_thread.read(cx).is_codeblock_expanded(message_id, ix);
let can_expand = metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
let is_expanded = if can_expand {
active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(false)
} else {
false
};
let codeblock_header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let control_buttons = h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.absolute()
.top_0()
.right_0()
.h_full()
.bg(codeblock_header_bg)
.rounded_tr_md()
.px_1()
.gap_1()
.child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code = parsed_markdown.source()[code_block_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
})
.ok();
})
.detach();
});
}
}),
)
.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
);
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
let codeblock_header = h_flex()
.relative()
.p_1()
.py_1()
.pl_1p5()
.pr_1()
.gap_1()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(codeblock_header_bg)
.map(|this| {
if !is_expanded {
this.rounded_md()
} else {
this.rounded_t_md()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
}
})
.rounded_t_md()
.children(label)
.child(control_buttons);
.child(
h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.gap_1()
.child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code =
parsed_markdown.source()[code_block_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
})
.ok();
})
.detach();
});
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
let is_expanded = this
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(true);
*is_expanded = !*is_expanded;
cx.notify();
});
}
}),
)
}),
);
v_flex()
.group(CODEBLOCK_CONTAINER_GROUP)
.my_2()
.overflow_hidden()
.rounded_md()
.rounded_lg()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(cx.theme().colors().editor_background)
.child(codeblock_header)
.when(!is_expanded, |this| this.h(rems_from_px(31.)))
}
fn open_path(
path_range: &PathWithRange,
window: &mut Window,
workspace: &mut Workspace,
cx: &mut Context<'_, Workspace>,
) {
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
else {
return; // TODO instead of just bailing out, open that path in a buffer.
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task = workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(target, window, cx);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
.when(can_expand && !is_expanded, |this| this.max_h_80())
}
fn render_code_language(
@@ -631,13 +626,10 @@ fn render_code_language(
.map(|language| language.name().into())
.unwrap_or(name_fallback);
let label_size = rems(0.8125);
h_flex()
.px_1()
.gap_1p5()
.children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)))
.child(div().text_size(label_size).child(language_label))
.gap_1()
.children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::Small)))
.child(Label::new(language_label).size(LabelSize::Small))
.into_any_element()
}
@@ -1018,7 +1010,6 @@ impl ActiveThread {
self.push_message(message_id, &message_segments, window, cx);
}
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
@@ -1032,7 +1023,6 @@ impl ActiveThread {
self.edited_message(message_id, &message_segments, window, cx);
}
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
@@ -1546,15 +1536,11 @@ impl ActiveThread {
let project = self.thread.read(cx).project().clone();
let prompt_store = self.thread_store.read(cx).prompt_store().clone();
let git_store = project.read(cx).git_store().clone();
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let load_context_task =
crate::context::load_context(new_context, &project, &prompt_store, cx);
self._load_edited_message_context_task =
Some(cx.spawn_in(window, async move |this, cx| {
let (context, checkpoint) =
futures::future::join(load_context_task, checkpoint).await;
let context = load_context_task.await;
let _ = this
.update_in(cx, |this, window, cx| {
this.thread.update(cx, |thread, cx| {
@@ -1563,7 +1549,6 @@ impl ActiveThread {
Role::User,
vec![MessageSegment::Text(edited_text)],
Some(context.loaded_context),
checkpoint.ok(),
cx,
);
for message_id in this.messages_after(message_id) {
@@ -1733,11 +1718,10 @@ impl ActiveThread {
.on_action(cx.listener(Self::confirm_editing_message))
.capture_action(cx.listener(Self::paste))
.min_h_6()
.w_full()
.flex_grow()
.w_full()
.gap_2()
.child(state.context_strip.clone())
.child(div().pt(px(-3.)).px_neg_0p5().child(EditorElement::new(
.child(EditorElement::new(
&state.editor,
EditorStyle {
background: colors.editor_background,
@@ -1746,7 +1730,8 @@ impl ActiveThread {
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)))
))
.child(state.context_strip.clone())
}
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
@@ -1874,8 +1859,7 @@ impl ActiveThread {
.child(open_as_markdown),
)
.into_any_element(),
None if AssistantSettings::get_global(cx).enable_feedback =>
feedback_container
None => feedback_container
.child(
div().visible_on_hover("feedback_container").child(
Label::new(
@@ -1918,9 +1902,6 @@ impl ActiveThread {
.child(open_as_markdown),
)
.into_any_element(),
None => feedback_container
.child(h_flex().child(open_as_markdown))
.into_any_element(),
};
let message_is_empty = message.should_display_content();
@@ -1934,6 +1915,16 @@ impl ActiveThread {
v_flex()
.w_full()
.gap_1()
.when(!message_is_empty, |parent| {
parent.child(div().min_h_6().child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
workspace.clone(),
window,
cx,
)))
})
.when(!added_context.is_empty(), |parent| {
parent.child(h_flex().flex_wrap().gap_1().children(
added_context.into_iter().map(|added_context| {
@@ -1952,16 +1943,6 @@ impl ActiveThread {
}),
))
})
.when(!message_is_empty, |parent| {
parent.child(div().pt_0p5().min_h_6().child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
workspace.clone(),
window,
cx,
)))
})
.into_any_element()
}
});
@@ -1987,7 +1968,6 @@ impl ActiveThread {
h_flex()
.p_2p5()
.gap_1()
.items_end()
.children(message_content)
.when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone();
@@ -2001,7 +1981,6 @@ impl ActiveThread {
)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2019,12 +1998,11 @@ impl ActiveThread {
.child(
IconButton::new(
"confirm-edit-message",
IconName::Return,
IconName::Check,
)
.disabled(state.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.icon_color(Color::Success)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2044,6 +2022,9 @@ impl ActiveThread {
)
}),
)
.when(editing_message_state.is_none(), |this| {
this.tooltip(Tooltip::text("Click To Edit"))
})
.on_click(cx.listener({
let message_segments = message.segments.clone();
move |this, _, window, cx| {
@@ -2084,16 +2065,6 @@ impl ActiveThread {
let panel_background = cx.theme().colors().panel_background;
let backdrop = div()
.id("backdrop")
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8)
.on_click(cx.listener(Self::handle_cancel_click));
v_flex()
.w_full()
.map(|parent| {
@@ -2263,7 +2234,15 @@ impl ActiveThread {
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(backdrop)
parent.relative().child(
div()
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
})
.into_any()
}
@@ -2373,17 +2352,42 @@ impl ActiveThread {
}),
transform: Some(Arc::new({
let active_thread = cx.entity();
let editor_bg = cx.theme().colors().editor_background;
move |element, range, _, _, cx| {
let is_expanded = active_thread
.read(cx)
.is_codeblock_expanded(message_id, range.start);
if is_expanded {
return element;
move |el, range, metadata, _, cx| {
let can_expand = metadata.line_count
> MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
if !can_expand {
return el;
}
element
let is_expanded = active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, range.start))
.copied()
.unwrap_or(false);
if is_expanded {
return el;
}
el.child(
div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_1_4()
.rounded_b_lg()
.bg(linear_gradient(
0.,
linear_color_stop(editor_bg, 0.),
linear_color_stop(
editor_bg.opacity(0.),
1.,
),
)),
)
}
})),
},
@@ -3380,26 +3384,6 @@ impl ActiveThread {
.log_err();
}))
}
pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool {
self.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(true)
}
pub fn toggle_codeblock_expanded(&mut self, message_id: MessageId, ix: usize) {
let is_expanded = self
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(true);
*is_expanded = !*is_expanded;
}
pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
self.list_state.reset(self.messages.len());
cx.notify();
}
}
pub enum ActiveThreadEvent {
@@ -3413,7 +3397,6 @@ impl Render for ActiveThread {
v_flex()
.size_full()
.relative()
.bg(cx.theme().colors().panel_background)
.on_mouse_move(cx.listener(|this, _, _, cx| {
this.show_scrollbar = true;
this.hide_scrollbar_later(cx);

View File

@@ -49,7 +49,7 @@ pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
use crate::slash_command_settings::SlashCommandSettings;
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread_store::{SerializedThread, TextThreadStore, ThreadStore};
pub use crate::thread_store::{TextThreadStore, ThreadStore};
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use context_store::ContextStore;
pub use ui::preview::{all_agent_previews, get_agent_preview};
@@ -85,7 +85,6 @@ actions!(
KeepAll,
Follow,
ResetTrialUpsell,
ResetTrialEndUpsell,
]
);
@@ -217,6 +216,7 @@ fn register_slash_commands(cx: &mut App) {
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
slash_command_registry.register_command(assistant_slash_commands::TerminalSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
slash_command_registry
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);

View File

@@ -18,8 +18,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use settings::{Settings, update_settings_file};
use ui::{
Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip,
prelude::*,
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
Switch, SwitchColor, Tooltip, prelude::*,
};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -36,7 +36,6 @@ pub struct AgentConfiguration {
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>,
expanded_context_server_tools: HashMap<ContextServerId, bool>,
expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
tools: Entity<ToolWorkingSet>,
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
@@ -79,7 +78,6 @@ impl AgentConfiguration {
configuration_views_by_provider: HashMap::default(),
context_server_store,
expanded_context_server_tools: HashMap::default(),
expanded_provider_configurations: HashMap::default(),
tools,
_registry_subscription: registry_subscription,
scroll_handle,
@@ -98,7 +96,6 @@ impl AgentConfiguration {
fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views_by_provider.remove(provider_id);
self.expanded_provider_configurations.remove(provider_id);
}
fn add_provider_configuration_view(
@@ -138,14 +135,9 @@ impl AgentConfiguration {
.get(&provider.id())
.cloned();
let is_expanded = self
.expanded_provider_configurations
.get(&provider.id())
.copied()
.unwrap_or(false);
v_flex()
.pt_3()
.pb_1()
.gap_1p5()
.border_t_1()
.border_color(cx.theme().colors().border.opacity(0.6))
@@ -160,63 +152,36 @@ impl AgentConfiguration {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new(provider_name.clone()).size(LabelSize::Large))
.when(provider.is_authenticated(cx) && !is_expanded, |parent| {
parent.child(Icon::new(IconName::Check).color(Color::Success))
}),
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
)
.child(
h_flex()
.gap_1()
.when(provider.is_authenticated(cx), |parent| {
parent.child(
Button::new(
SharedString::from(format!("new-thread-{provider_id}")),
"Start New Thread",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.layer(ElevationIndex::ModalSurface)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let provider = provider.clone();
move |_this, _event, _window, cx| {
cx.emit(AssistantConfigurationEvent::NewThread(
provider.clone(),
))
}
})),
)
})
.child(
Disclosure::new(
SharedString::from(format!(
"provider-disclosure-{provider_id}"
)),
is_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
let provider_id = provider.id().clone();
move |this, _event, _window, _cx| {
let is_expanded = this
.expanded_provider_configurations
.entry(provider_id.clone())
.or_insert(false);
*is_expanded = !*is_expanded;
}
})),
),
),
.when(provider.is_authenticated(cx), |parent| {
parent.child(
Button::new(
SharedString::from(format!("new-thread-{provider_id}")),
"Start New Thread",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let provider = provider.clone();
move |_this, _event, _window, cx| {
cx.emit(AssistantConfigurationEvent::NewThread(
provider.clone(),
))
}
})),
)
}),
)
.when(is_expanded, |parent| match configuration_view {
.map(|parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(Label::new(format!(
None => parent.child(div().child(Label::new(format!(
"No configuration view for {provider_name}",
))),
)))),
})
}
@@ -230,8 +195,7 @@ impl AgentConfiguration {
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_4()
.border_b_1()
.border_color(cx.theme().colors().border)
.flex_1()
.child(
v_flex()
.gap_0p5()
@@ -332,8 +296,7 @@ impl AgentConfiguration {
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.flex_1()
.child(Headline::new("General Settings"))
.child(self.render_command_permission(cx))
.child(self.render_single_file_review(cx))
@@ -346,17 +309,18 @@ impl AgentConfiguration {
) -> impl IntoElement {
let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.flex_1()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)),
.child(Label::new(SUBHEADING).color(Color::Muted)),
)
.children(
context_server_ids.into_iter().map(|context_server_id| {
@@ -423,7 +387,6 @@ impl AgentConfiguration {
.unwrap_or(ContextServerStatus::Stopped);
let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone());
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error)
@@ -445,38 +408,9 @@ impl AgentConfiguration {
let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let success_color = Color::Success.color(cx);
let (status_indicator, tooltip_text) = match server_status {
ContextServerStatus::Starting => (
Indicator::dot()
.color(Color::Success)
.with_animation(
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| this.color(success_color.alpha(delta).into()),
)
.into_any_element(),
"Server is starting.",
),
ContextServerStatus::Running => (
Indicator::dot().color(Color::Success).into_any_element(),
"Server is running.",
),
ContextServerStatus::Error(_) => (
Indicator::dot().color(Color::Error).into_any_element(),
"Server has an error.",
),
ContextServerStatus::Stopped => (
Indicator::dot().color(Color::Muted).into_any_element(),
"Server is stopped.",
),
};
v_flex()
.id(item_id.clone())
.id(SharedString::from(context_server_id.0.clone()))
.border_1()
.rounded_md()
.border_color(border_color)
@@ -511,12 +445,35 @@ impl AgentConfiguration {
}
})),
)
.child(
div()
.id(item_id.clone())
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
.child(match server_status {
ContextServerStatus::Starting => {
let color = Color::Success.color(cx);
Indicator::dot()
.color(Color::Success)
.with_animation(
SharedString::from(format!(
"{}-starting",
context_server_id.0.clone(),
)),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| {
this.color(color.alpha(delta).into())
},
)
.into_any_element()
}
ContextServerStatus::Running => {
Indicator::dot().color(Color::Success).into_any_element()
}
ContextServerStatus::Error(_) => {
Indicator::dot().color(Color::Error).into_any_element()
}
ContextServerStatus::Stopped => {
Indicator::dot().color(Color::Muted).into_any_element()
}
})
.child(Label::new(context_server_id.0.clone()).ml_0p5())
.when(is_running, |this| {
this.child(
@@ -631,7 +588,9 @@ impl Render for AgentConfiguration {
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(window, cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_provider_configuration_section(cx)),
)
.child(

View File

@@ -30,6 +30,7 @@ pub(crate) struct ConfigureContextServerModal {
context_server_store: Entity<ContextServerStore>,
}
#[allow(clippy::large_enum_variant)]
enum Configuration {
NotAvailable,
Required(ConfigurationRequiredState),

View File

@@ -3,10 +3,10 @@ use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use crate::Thread;
use assistant_context_editor::language_model_selector::{
use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use language_model::{ConfiguredModel, LanguageModelRegistry};
use settings::update_settings_file;
use std::sync::Arc;
use ui::{PopoverMenuHandle, Tooltip, prelude::*};

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use db::kvp::KEY_VALUE_STORE;
use markdown::Markdown;
use serde::{Deserialize, Serialize};
@@ -17,7 +17,6 @@ use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::{UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
@@ -31,6 +30,7 @@ use language::LanguageRegistry;
use language_model::{
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::ToggleModelSelector;
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use proto::Plan;
@@ -66,8 +66,8 @@ use crate::ui::AgentOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
ToggleNavigationMenu, ToggleOptionsMenu,
};
const AGENT_PANEL_KEY: &str = "agent_panel";
@@ -157,10 +157,7 @@ pub fn init(cx: &mut App) {
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
TrialUpsell::set_dismissed(false, cx);
})
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
TrialEndUpsell::set_dismissed(false, cx);
set_trial_upsell_dismissed(false, cx);
});
},
)
@@ -570,15 +567,6 @@ impl AgentPanel {
menu = menu.header("Recently Opened");
for entry in recently_opened.iter() {
if let RecentEntry::Context(context) = entry {
if context.read(cx).path().is_none() {
log::error!(
"bug: text thread in recent history list was never saved"
);
continue;
}
}
let summary = entry.summary(cx);
menu = menu.entry_with_end_slot_on_hover(
@@ -1212,7 +1200,12 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
let Some(workspace) = self
.workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))
.log_err()
else {
return;
};
@@ -1297,26 +1290,14 @@ impl AgentPanel {
let new_is_history = matches!(new_view, ActiveView::History);
match &self.active_view {
ActiveView::Thread { thread, .. } => {
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
if let Some(thread) = thread.upgrade() {
if thread.read(cx).is_empty() {
let id = thread.read(cx).id().clone();
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_thread(id, cx);
});
store.remove_recently_opened_thread(id, cx);
}
}
}
ActiveView::PromptEditor { context_editor, .. } => {
let context = context_editor.read(cx).context();
// When switching away from an unsaved text thread, delete its entry.
if context.read(cx).path().is_none() {
let context = context.clone();
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
});
}
}
}),
_ => {}
}
@@ -1667,12 +1648,6 @@ impl AgentPanel {
}),
);
let zoom_in_label = if self.is_zoomed(window, cx) {
"Zoom Out"
} else {
"Zoom In"
};
let agent_extra_menu = PopoverMenu::new("agent-options-menu")
.trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis)
@@ -1759,8 +1734,7 @@ impl AgentPanel {
menu = menu
.action("Rules…", Box::new(OpenRulesLibrary::default()))
.action("Settings", Box::new(OpenConfiguration))
.action(zoom_in_label, Box::new(ToggleZoom));
.action("Settings", Box::new(OpenConfiguration));
menu
}))
});
@@ -1930,23 +1904,12 @@ impl AgentPanel {
}
}
fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
if TrialEndUpsell::dismissed() {
return false;
}
let plan = self.user_store.read(cx).current_plan();
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
matches!(plan, Some(Plan::Free)) && has_previous_trial
}
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
if !matches!(self.active_view, ActiveView::Thread { .. }) {
return false;
}
if self.hide_trial_upsell || TrialUpsell::dismissed() {
if self.hide_trial_upsell || dismissed_trial_upsell() {
return false;
}
@@ -1992,115 +1955,125 @@ impl AgentPanel {
move |toggle_state, _window, cx| {
let toggle_state_bool = toggle_state.selected();
TrialUpsell::set_dismissed(toggle_state_bool, cx);
set_trial_upsell_dismissed(toggle_state_bool, cx);
},
);
let contents = div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(
Label::new("Try Zed Pro for free for 14 days - no credit card required.")
.size(LabelSize::Small),
)
.child(
Label::new(
"Use your own API keys or enable usage-based billing once you hit the cap.",
)
.color(Color::Muted),
)
.child(
h_flex()
Some(
div().p_2().child(
v_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.child(
h_flex()
div()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |this, cx| {
this.hide_trial_upsell = true;
cx.notify();
});
}
}),
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
)
)
.child(
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
),
),
);
Some(self.render_upsell_container(cx, contents))
}
fn render_trial_end_upsell(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if !self.should_render_trial_end_upsell(cx) {
return None;
}
Some(
self.render_upsell_container(
cx,
div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
)
.child(
Label::new("You've been automatically reset to the free plan.")
.size(LabelSize::Small),
)
.child(
h_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(div())
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
)
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
))
)
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
.child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
.child(
h_flex()
.gap_2()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.child(
Button::new("dismiss-button", "Stay on Free")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |_this, cx| {
TrialEndUpsell::set_dismissed(true, cx);
cx.notify();
});
}
}),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(
cx,
|this, cx| {
let hidden =
this.hide_trial_upsell;
println!("hidden: {}", hidden);
this.hide_trial_upsell = true;
let new_hidden =
this.hide_trial_upsell;
println!(
"new_hidden: {}",
new_hidden
);
cx.notify();
},
);
}
}),
)
.child(
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
),
),
),
),
@@ -2108,91 +2081,6 @@ impl AgentPanel {
)
}
fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
div().p_2().child(
v_flex()
.w_full()
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.child(
div()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(
VectorName::Grid,
rems_from_px(441.),
rems_from_px(167.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(
VectorName::AiGrid,
rems_from_px(400.),
rems_from_px(92.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
),
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(
cx.theme().colors().panel_background.alpha(0.01),
1.0,
),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
)),
)
.child(content),
),
)
}
fn render_active_thread_or_empty_state(
&self,
window: &mut Window,
@@ -2240,7 +2128,6 @@ impl AgentPanel {
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(recent_history.is_empty(), |this| {
let configuration_error_ref = &configuration_error;
this.child(
@@ -2545,6 +2432,9 @@ impl AgentPanel {
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::ModelRequestLimitReached { plan } => {
self.render_model_request_limit_reached_error(plan, cx)
}
@@ -2604,6 +2494,56 @@ impl AgentPanel {
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(ERROR_MESSAGE))
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_model_request_limit_reached_error(
&self,
plan: Plan,
@@ -2911,7 +2851,6 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::toggle_zoom))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.children(self.render_trial_end_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.relative()
@@ -3099,14 +3038,25 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
}
}
struct TrialUpsell;
const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
impl Dismissable for TrialUpsell {
const KEY: &'static str = "dismissed-trial-upsell";
fn dismissed_trial_upsell() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
struct TrialEndUpsell;
impl Dismissable for TrialEndUpsell {
const KEY: &'static str = "dismissed-trial-end-upsell";
fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
.await
}
})
}

View File

@@ -1,7 +1,7 @@
use crate::context::ContextLoadResult;
use crate::inline_prompt_editor::CodegenStatus;
use crate::{context::load_context, context_store::ContextStore};
use anyhow::{Context as _, Result};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::HashSet;
@@ -419,16 +419,16 @@ impl CodegenAlternative {
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
anyhow::bail!("invalid transformation range");
return Err(anyhow::anyhow!("invalid transformation range"));
}
} else {
anyhow::bail!("invalid transformation range");
return Err(anyhow::anyhow!("invalid transformation range"));
};
let prompt = self
.builder
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.context("generating content prompt")?;
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let context_task = self.context_store.as_ref().map(|context_store| {
if let Some(project) = self.project.upgrade() {

View File

@@ -942,8 +942,8 @@ impl MentionLink {
format!("[@{}]({}:{})", title, Self::THREAD, id)
}
ThreadContextEntry::Context { path, title } => {
let filename = path.file_name().unwrap_or_default().to_string_lossy();
let escaped_filename = urlencoding::encode(&filename);
let filename = path.file_name().unwrap_or_default();
let escaped_filename = urlencoding::encode(&filename.to_string_lossy()).to_string();
format!(
"[@{}]({}:{}{})",
title,

View File

@@ -2,7 +2,7 @@ use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_context_editor::AssistantContext;
use collections::{HashSet, IndexSet};
use futures::{self, FutureExt};
@@ -142,12 +142,17 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Result<Option<AgentContextHandle>> {
let project = self.project.upgrade().context("failed to read project")?;
let entry_id = project
let Some(project) = self.project.upgrade() else {
return Err(anyhow!("failed to read project"));
};
let Some(entry_id) = project
.read(cx)
.entry_for_path(project_path, cx)
.map(|entry| entry.id)
.context("no entry found for directory context")?;
else {
return Err(anyhow!("no entry found for directory context"));
};
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Directory(DirectoryContextHandle {

View File

@@ -84,12 +84,6 @@ impl ContextStrip {
}
}
/// Whether or not the context strip has items to display
pub fn has_context_items(&self, cx: &App) -> bool {
self.context_store.read(cx).context().next().is_some()
|| self.suggested_context(cx).is_some()
}
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
if let Some(workspace) = self.workspace.upgrade() {
let project = workspace.read(cx).project().read(cx);
@@ -110,14 +104,14 @@ impl ContextStrip {
}
}
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::File => self.suggested_file(cx),
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_file(&self, cx: &App) -> Option<SuggestedContext> {
fn suggested_file(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_item = workspace.read(cx).active_item(cx)?;
@@ -144,7 +138,7 @@ impl ContextStrip {
})
}
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
fn suggested_thread(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
}

View File

@@ -1,6 +1,6 @@
use std::{collections::VecDeque, path::Path, sync::Arc};
use anyhow::Context as _;
use anyhow::{Context as _, anyhow};
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
use chrono::{DateTime, Utc};
use futures::future::{TryFutureExt as _, join_all};
@@ -130,10 +130,7 @@ impl HistoryStore {
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no thread store");
}
.boxed()
async { Err(anyhow!("no thread store")) }.boxed()
}),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
@@ -143,10 +140,7 @@ impl HistoryStore {
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no context store");
}
.boxed()
async { Err(anyhow!("no context store")) }.boxed()
}),
});
let entries = join_all(entries)

View File

@@ -338,27 +338,13 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) {
let (snapshot, initial_selections, newest_selection) = editor.update(cx, |editor, cx| {
let selections = editor.selections.all::<Point>(cx);
let newest_selection = editor.selections.newest::<Point>(cx);
(editor.snapshot(window, cx), selections, newest_selection)
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
(
editor.snapshot(window, cx),
editor.selections.all::<Point>(cx),
)
});
// Check if there is already an inline assistant that contains the
// newest selection, if there is, focus it
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
let range = assist.range.to_point(&snapshot.buffer_snapshot);
if range.start.row <= newest_selection.start.row
&& newest_selection.end.row <= range.end.row
{
self.focus_assist(*assist_id, window, cx);
return;
}
}
}
let mut selections = Vec::<Selection<Point>>::new();
let mut newest_selection = None;
for mut selection in initial_selections {

View File

@@ -9,10 +9,8 @@ use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::ErrorExt;
use collections::VecDeque;
use db::kvp::Dismissable;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
@@ -25,6 +23,7 @@ use gpui::{
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use parking_lot::Mutex;
use settings::Settings;
use std::cmp;
@@ -34,6 +33,7 @@ use ui::utils::WithRemSize;
use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
};
use util::ResultExt;
use workspace::Workspace;
pub struct PromptEditor<T> {
@@ -451,7 +451,7 @@ impl<T: 'static> PromptEditor<T> {
editor.move_to_end(&Default::default(), window, cx)
});
}
} else if self.context_strip.read(cx).has_context_items(cx) {
} else {
self.context_strip.focus_handle(cx).focus(window);
}
}
@@ -722,7 +722,7 @@ impl<T: 'static> PromptEditor<T> {
.child(CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again"),
if RateLimitNotice::dismissed() {
if dismissed_rate_limit_notice() {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
@@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
ui::ToggleState::Selected => true,
};
RateLimitNotice::set_dismissed(is_dismissed, cx);
set_rate_limit_notice_dismissed(is_dismissed, cx)
},
))
.child(
@@ -974,7 +974,7 @@ impl PromptEditor<BufferCodegen> {
CodegenStatus::Error(error) => {
if cx.has_flag::<ZedProFeatureFlag>()
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
&& !RateLimitNotice::dismissed()
&& !dismissed_rate_limit_notice()
{
self.show_rate_limit_notice = true;
cx.notify();
@@ -1180,10 +1180,27 @@ impl PromptEditor<TerminalCodegen> {
}
}
struct RateLimitNotice;
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
impl Dismissable for RateLimitNotice {
const KEY: &'static str = "dismissed-rate-limit-notice";
fn dismissed_rate_limit_notice() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
.await
}
})
}
pub enum CodegenStatus {

View File

@@ -8,7 +8,6 @@ use crate::ui::{
AnimatedLabel, MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use assistant_settings::{AssistantSettings, CompletionMode};
use buffer_diff::BufferDiff;
use client::UserStore;
@@ -31,6 +30,7 @@ use language_model::{
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;
@@ -401,7 +401,7 @@ impl MessageEditor {
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.context_picker_menu_handle.is_deployed() {
cx.propagate();
} else if self.context_strip.read(cx).has_context_items(cx) {
} else {
self.context_strip.focus_handle(cx).focus(window);
}
}

View File

@@ -191,7 +191,7 @@ impl TerminalInlineAssistant {
};
self.prompt_history.retain(|prompt| *prompt != user_prompt);
self.prompt_history.push_back(user_prompt);
self.prompt_history.push_back(user_prompt.clone());
if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
self.prompt_history.pop_front();
}

View File

@@ -22,9 +22,9 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
StopReason, TokenUsage, WrappedTextContent,
StopReason, TokenUsage,
};
use postage::stream::Stream as _;
use project::Project;
@@ -214,7 +214,7 @@ pub struct GitState {
pub diff: Option<String>,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct ThreadCheckpoint {
message_id: MessageId,
git_checkpoint: GitStoreCheckpoint,
@@ -458,7 +458,7 @@ impl Thread {
tools: Entity<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
project_context: SharedProjectContext,
window: Option<&mut Window>, // None in headless mode
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
@@ -880,16 +880,7 @@ impl Thread {
}
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
match &self.tool_use.tool_result(id)?.content {
LanguageModelToolResultContent::Text(text)
| LanguageModelToolResultContent::WrappedText(WrappedTextContent { text, .. }) => {
Some(text)
}
LanguageModelToolResultContent::Image(_) => {
// TODO: We should display image
None
}
}
Some(&self.tool_use.tool_result(id)?.content)
}
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
@@ -999,7 +990,6 @@ impl Thread {
new_role: Role,
new_segments: Vec<MessageSegment>,
loaded_context: Option<LoadedContext>,
checkpoint: Option<GitStoreCheckpoint>,
cx: &mut Context<Self>,
) -> bool {
let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
@@ -1010,15 +1000,6 @@ impl Thread {
if let Some(context) = loaded_context {
message.loaded_context = context;
}
if let Some(git_checkpoint) = checkpoint {
self.checkpoints_by_message.insert(
id,
ThreadCheckpoint {
message_id: id,
git_checkpoint,
},
);
}
self.touch_updated_at();
cx.emit(ThreadEvent::MessageEdited(id));
true
@@ -1630,7 +1611,7 @@ impl Thread {
CompletionRequestStatus::Failed {
code, message, request_id
} => {
anyhow::bail!("completion request failed. request_id: {request_id}, code: {code}, message: {message}");
return Err(anyhow!("completion request failed. request_id: {request_id}, code: {code}, message: {message}"));
}
CompletionRequestStatus::UsageUpdated {
amount, limit
@@ -1701,6 +1682,10 @@ impl Thread {
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ThreadEvent::ShowError(
ThreadError::MaxMonthlySpendReached,
));
} else if let Some(error) =
error.downcast_ref::<ModelRequestLimitReachedError>()
{
@@ -2256,7 +2241,7 @@ impl Thread {
.read(cx)
.enabled_tools(cx)
.iter()
.map(|tool| tool.name())
.map(|tool| tool.name().to_string())
.collect();
self.message_feedback.insert(message_id, feedback);
@@ -2517,19 +2502,7 @@ impl Thread {
}
writeln!(markdown, "**\n")?;
match &tool_result.content {
LanguageModelToolResultContent::Text(text)
| LanguageModelToolResultContent::WrappedText(WrappedTextContent {
text,
..
}) => {
writeln!(markdown, "{text}")?;
}
LanguageModelToolResultContent::Image(image) => {
writeln!(markdown, "![Image](data:base64,{})", image.source)?;
}
}
writeln!(markdown, "{}", tool_result.content)?;
if let Some(output) = tool_result.output.as_ref() {
writeln!(
markdown,
@@ -2600,7 +2573,7 @@ impl Thread {
.read(cx)
.current_user()
.map(|user| user.github_login.clone());
let client = self.project.read(cx).client();
let client = self.project.read(cx).client().clone();
let serialize_task = self.serialize(cx);
cx.background_executor()
@@ -2719,6 +2692,8 @@ impl Thread {
pub enum ThreadError {
#[error("Payment required")]
PaymentRequired,
#[error("Max monthly spend reached")]
MaxMonthlySpendReached,
#[error("Model request limit reached")]
ModelRequestLimitReached { plan: Plan },
#[error("Message {header}: {message}")]

View File

@@ -260,7 +260,10 @@ impl ThreadHistory {
}
});
self.search_state = SearchState::Searching { query, _task: task };
self.search_state = SearchState::Searching {
query: query.clone(),
_task: task,
};
cx.notify();
}

View File

@@ -19,7 +19,7 @@ use gpui::{
};
use heed::Database;
use heed::types::SerdeBincode;
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
@@ -386,25 +386,6 @@ impl ThreadStore {
})
}
pub fn create_thread_from_serialized(
&mut self,
serialized: SerializedThread,
cx: &mut Context<Self>,
) -> Entity<Thread> {
cx.new(|cx| {
Thread::deserialize(
ThreadId::new(),
serialized,
self.project.clone(),
self.tools.clone(),
self.prompt_builder.clone(),
self.project_context.clone(),
None,
cx,
)
})
}
pub fn open_thread(
&self,
id: &ThreadId,
@@ -419,7 +400,7 @@ impl ThreadStore {
let thread = database
.try_find_thread(id.clone())
.await?
.with_context(|| format!("no thread found with ID: {id:?}"))?;
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
let thread = this.update_in(cx, |this, window, cx| {
cx.new(|cx| {
@@ -430,7 +411,7 @@ impl ThreadStore {
this.tools.clone(),
this.prompt_builder.clone(),
this.project_context.clone(),
Some(window),
window,
cx,
)
})
@@ -505,8 +486,8 @@ impl ThreadStore {
ToolSource::Native,
&profile
.tools
.into_iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool))
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
cx,
);
@@ -530,32 +511,32 @@ impl ThreadStore {
});
}
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
for (context_server_id, preset) in profile.context_servers {
for (context_server_id, preset) in &profile.context_servers {
self.tools.update(cx, |tools, cx| {
tools.disable(
ToolSource::ContextServer {
id: context_server_id.into(),
id: context_server_id.clone().into(),
},
&preset
.tools
.into_iter()
.filter_map(|(tool, enabled)| (!enabled).then(|| tool))
.iter()
.filter_map(|(tool, enabled)| (!enabled).then(|| tool.clone()))
.collect::<Vec<_>>(),
cx,
)
})
}
} else {
for (context_server_id, preset) in profile.context_servers {
for (context_server_id, preset) in &profile.context_servers {
self.tools.update(cx, |tools, cx| {
tools.enable(
ToolSource::ContextServer {
id: context_server_id.into(),
id: context_server_id.clone().into(),
},
&preset
.tools
.into_iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool))
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.collect::<Vec<_>>(),
cx,
)
@@ -699,14 +680,20 @@ impl SerializedThread {
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
saved_thread_json,
)?),
_ => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
_ => Err(anyhow!(
"unrecognized serialized thread version: {}",
version
)),
},
None => {
let saved_thread =
serde_json::from_value::<LegacySerializedThread>(saved_thread_json)?;
Ok(saved_thread.upgrade())
}
version => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
version => Err(anyhow!(
"unrecognized serialized thread version: {:?}",
version
)),
}
}
}
@@ -788,7 +775,7 @@ pub struct SerializedToolUse {
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
pub content: LanguageModelToolResultContent,
pub content: Arc<str>,
pub output: Option<serde_json::Value>,
}

View File

@@ -1,16 +1,14 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
};
use assistant_tool::{AnyToolCard, Tool, ToolResultOutput, ToolUseStatus, ToolWorkingSet};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, Entity, SharedString, Task};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
LanguageModelToolUse, LanguageModelToolUseId, Role,
};
use project::Project;
use ui::{IconName, Window};
@@ -54,19 +52,15 @@ impl ToolUseState {
/// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
///
/// Accepts a function to filter the tools that should be used to populate the state.
///
/// If `window` is `None` (e.g., when in headless mode or when running evals),
/// tool cards won't be deserialized
pub fn from_serialized_messages(
tools: Entity<ToolWorkingSet>,
messages: &[SerializedMessage],
project: Entity<Project>,
window: Option<&mut Window>, // None in headless mode
window: &mut Window,
cx: &mut App,
) -> Self {
let mut this = Self::new(tools);
let mut tool_names_by_id = HashMap::default();
let mut window = window;
for message in messages {
match message.role {
@@ -111,17 +105,12 @@ impl ToolUseState {
},
);
if let Some(window) = &mut window {
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
if let Some(output) = tool_result.output.clone() {
if let Some(card) = tool.deserialize_card(
output,
project.clone(),
window,
cx,
) {
this.tool_result_cards.insert(tool_use_id, card);
}
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
if let Some(output) = tool_result.output.clone() {
if let Some(card) =
tool.deserialize_card(output, project.clone(), window, cx)
{
this.tool_result_cards.insert(tool_use_id, card);
}
}
}
@@ -176,16 +165,10 @@ impl ToolUseState {
let status = (|| {
if let Some(tool_result) = tool_result {
let content = tool_result
.content
.to_str()
.map(|str| str.to_owned().into())
.unwrap_or_default();
return if tool_result.is_error {
ToolUseStatus::Error(content)
ToolUseStatus::Error(tool_result.content.clone().into())
} else {
ToolUseStatus::Finished(content)
ToolUseStatus::Finished(tool_result.content.clone().into())
};
}
@@ -416,45 +399,21 @@ impl ToolUseState {
let tool_result = output.content;
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id);
// Protect from overly large output
// Protect from clearly large output
let tool_output_limit = configured_model
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let content = match tool_result {
ToolResultContent::Text(text) => {
let text = if text.len() < tool_output_limit {
text
} else {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
};
LanguageModelToolResultContent::Text(text.into())
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {
LanguageModelToolResultContent::Image(language_model_image)
} else {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: "Tool responded with an image that would exceeded the remaining tokens".into(),
is_error: true,
output: None,
},
);
let tool_result = if tool_result.len() <= tool_output_limit {
tool_result
} else {
let truncated = truncate_lines_to_byte_limit(&tool_result, tool_output_limit);
return old_use;
}
}
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
};
self.tool_results.insert(
@@ -462,13 +421,12 @@ impl ToolUseState {
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content,
content: tool_result.into(),
is_error: false,
output: output.output,
},
);
old_use
self.pending_tool_uses_by_id.remove(&tool_use_id)
}
Err(err) => {
self.tool_results.insert(
@@ -476,7 +434,7 @@ impl ToolUseState {
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: LanguageModelToolResultContent::Text(err.to_string().into()),
content: err.to_string().into(),
is_error: true,
output: None,
},

View File

@@ -1,8 +1,8 @@
use std::sync::OnceLock;
use collections::HashMap;
use component::ComponentId;
use gpui::{App, Entity, WeakEntity};
use linkme::distributed_slice;
use std::sync::OnceLock;
use ui::{AnyElement, Component, ComponentScope, Window};
use workspace::Workspace;
@@ -12,15 +12,9 @@ use crate::ActiveThread;
pub type PreviewFn =
fn(WeakEntity<Workspace>, Entity<ActiveThread>, &mut Window, &mut App) -> Option<AnyElement>;
pub struct AgentPreviewFn(fn() -> (ComponentId, PreviewFn));
impl AgentPreviewFn {
pub const fn new(f: fn() -> (ComponentId, PreviewFn)) -> Self {
Self(f)
}
}
inventory::collect!(AgentPreviewFn);
/// Distributed slice for preview registration functions
#[distributed_slice]
pub static __ALL_AGENT_PREVIEWS: [fn() -> (ComponentId, PreviewFn)] = [..];
/// Trait that must be implemented by components that provide agent previews.
pub trait AgentPreview: Component + Sized {
@@ -42,14 +36,16 @@ pub trait AgentPreview: Component + Sized {
#[macro_export]
macro_rules! register_agent_preview {
($type:ty) => {
inventory::submit! {
$crate::ui::preview::AgentPreviewFn::new(|| {
(
<$type as component::Component>::id(),
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
)
})
}
#[linkme::distributed_slice($crate::ui::preview::__ALL_AGENT_PREVIEWS)]
static __REGISTER_AGENT_PREVIEW: fn() -> (
component::ComponentId,
$crate::ui::preview::PreviewFn,
) = || {
(
<$type as component::Component>::id(),
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
)
};
};
}
@@ -60,8 +56,8 @@ static AGENT_PREVIEW_REGISTRY: OnceLock<HashMap<ComponentId, PreviewFn>> = OnceL
fn get_or_init_registry() -> &'static HashMap<ComponentId, PreviewFn> {
AGENT_PREVIEW_REGISTRY.get_or_init(|| {
let mut map = HashMap::default();
for register_fn in inventory::iter::<AgentPreviewFn>() {
let (id, preview_fn) = (register_fn.0)();
for register_fn in __ALL_AGENT_PREVIEWS.iter() {
let (id, preview_fn) = register_fn();
map.insert(id, preview_fn);
}
map

View File

@@ -90,7 +90,7 @@ impl Model {
} else if id.starts_with("claude-3-haiku") {
Ok(Self::Claude3Haiku)
} else {
anyhow::bail!("invalid model id {id}");
Err(anyhow!("invalid model id"))
}
}
@@ -385,10 +385,10 @@ impl RateLimitInfo {
}
}
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
Ok(headers
.get(key)
.with_context(|| format!("missing header `{key}`"))?
.ok_or_else(|| anyhow!("missing header `{key}`"))?
.to_str()?)
}
@@ -534,26 +534,12 @@ pub enum RequestContent {
ToolResult {
tool_use_id: String,
is_error: bool,
content: ToolResultContent,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Plain(String),
Multipart(Vec<ToolResultPart>),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolResultPart {
Text { text: String },
Image { source: ImageSource },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ResponseContent {

View File

@@ -163,10 +163,8 @@ impl AskPassSession {
#[cfg(unix)]
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
let zed_path = std::env::current_exe()
.context("Failed to determine current executable path for use in askpass")?
.context("Failed to figure out current executable path for use in askpass")?
.to_string_lossy()
// see https://github.com/rust-lang/rust/issues/69343
.trim_end_matches(" (deleted)")
.to_string();
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have

View File

@@ -1,6 +1,6 @@
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
use anyhow::anyhow;
use anyhow::Context as _;
use gpui::{App, AssetSource, Result, SharedString};
use rust_embed::RustEmbed;
@@ -21,7 +21,7 @@ impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
Self::get(path)
.map(|f| Some(f.data))
.with_context(|| format!("loading asset at path {path:?}"))
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
@@ -39,7 +39,7 @@ impl AssetSource for Assets {
impl Assets {
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
pub fn load_fonts(&self, cx: &App) -> gpui::Result<()> {
let font_paths = self.list("fonts")?;
let mut embedded_fonts = Vec::new();
for font_path in font_paths {

View File

@@ -22,7 +22,6 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -30,16 +29,15 @@ gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
log.workspace = true
multi_buffer.workspace = true
open_ai.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
regex.workspace = true
rope.workspace = true
rpc.workspace = true

View File

@@ -2,7 +2,6 @@ mod context;
mod context_editor;
mod context_history;
mod context_store;
pub mod language_model_selector;
mod slash_command;
mod slash_command_picker;

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod context_tests;
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_settings::AssistantSettings;
use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
@@ -21,8 +21,8 @@ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, P
use language_model::{
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelToolUseId, MessageContent, PaymentRequiredError, Role, StopReason,
report_assistant_event,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason, report_assistant_event,
};
use open_ai::Model as OpenAiModel;
use paths::contexts_dir;
@@ -447,6 +447,7 @@ impl ContextOperation {
pub enum ContextEvent {
ShowAssistError(SharedString),
ShowPaymentRequiredError,
ShowMaxMonthlySpendReachedError,
MessagesEdited,
SummaryChanged,
SummaryGenerated,
@@ -2154,6 +2155,12 @@ impl AssistantContext {
metadata.status = MessageStatus::Canceled;
});
Some(error.to_string())
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ContextEvent::ShowMaxMonthlySpendReachedError);
this.update_metadata(assistant_message_id, cx, |metadata| {
metadata.status = MessageStatus::Canceled;
});
Some(error.to_string())
} else {
let error_message = error
.chain()
@@ -3011,7 +3018,7 @@ impl SavedContext {
let saved_context_json = serde_json::from_str::<serde_json::Value>(json)?;
match saved_context_json
.get("version")
.context("version not found")?
.ok_or_else(|| anyhow!("version not found"))?
{
serde_json::Value::String(version) => match version.as_str() {
SavedContext::VERSION => {
@@ -3032,9 +3039,9 @@ impl SavedContext {
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
Ok(saved_context.upgrade())
}
_ => anyhow::bail!("unrecognized saved context version: {version:?}"),
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
},
_ => anyhow::bail!("version not found on saved context"),
_ => Err(anyhow!("version not found on saved context")),
}
}

View File

@@ -1,6 +1,3 @@
use crate::language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
@@ -39,6 +36,9 @@ use language_model::{
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
};
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use multi_buffer::MultiBufferRow;
use picker::Picker;
use project::{Project, Worktree};
@@ -114,6 +114,7 @@ type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
}
@@ -731,6 +732,9 @@ impl ContextEditor {
ContextEvent::ShowPaymentRequiredError => {
self.last_error = Some(AssistError::PaymentRequired);
}
ContextEvent::ShowMaxMonthlySpendReachedError => {
self.last_error = Some(AssistError::MaxMonthlySpendReached);
}
}
}
@@ -1590,7 +1594,7 @@ impl ContextEditor {
&mut self,
cx: &mut Context<Self>,
) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
let (selection, creases) = self.editor.update(cx, |editor, cx| {
let mut selection = editor.selections.newest_adjusted(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -1648,18 +1652,7 @@ impl ContextEditor {
} else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end);
if range.is_empty() {
let snapshot = context.buffer().read(cx).snapshot();
let point = snapshot.offset_to_point(range.start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot.point_to_offset(cmp::min(
Point::new(point.row + 1, 0),
snapshot.max_point(),
));
for chunk in context.buffer().read(cx).text_for_range(selection.range()) {
text.push_str(chunk);
}
} else {
if !range.is_empty() {
for chunk in context.buffer().read(cx).text_for_range(range) {
text.push_str(chunk);
}
@@ -2114,6 +2107,9 @@ impl ContextEditor {
.occlude()
.child(match last_error {
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
AssistError::Message(error_message) => {
self.render_assist_error(error_message, cx)
}
@@ -2162,6 +2158,48 @@ impl ContextEditor {
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, _window, cx| {
this.last_error = None;
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _window, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_assist_error(
&self,
error_message: &SharedString,
@@ -3044,7 +3082,7 @@ fn invoked_slash_command_fold_placeholder(
.gap_2()
.bg(cx.theme().colors().surface_background)
.rounded_sm()
.child(Label::new(format!("/{}", command.name)))
.child(Label::new(format!("/{}", command.name.clone())))
.map(|parent| match &command.status {
InvokedSlashCommandStatus::Running(_) => {
parent.child(Icon::new(IconName::ArrowCircle).with_animation(
@@ -3213,77 +3251,9 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use language::{Buffer, LanguageRegistry};
use prompt_store::PromptBuilder;
use gpui::App;
use language::Buffer;
use unindent::Unindent;
use util::path;
#[gpui::test]
async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
cx.update(init_test);
let fs = FakeFs::new(cx.executor());
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
)
});
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let cx = &mut VisualTestContext::from_window(*window, cx);
let context_editor = window
.update(cx, |_, window, cx| {
cx.new(|cx| {
ContextEditor::for_context(
context,
fs,
workspace.downgrade(),
project,
None,
window,
cx,
)
})
})
.unwrap();
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.set_text("abc\ndef\nghi", window, cx);
editor.move_to_beginning(&Default::default(), window, cx);
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.copy(&Default::default(), window, cx);
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.cut(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\ndef\nghi");
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
}
#[gpui::test]
fn test_find_code_blocks(cx: &mut App) {
@@ -3358,17 +3328,4 @@ mod tests {
assert_eq!(range, expected, "unexpected result on row {:?}", row);
}
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
prompt_store::init(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
language::init(cx);
assistant_settings::init(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
workspace::init_settings(cx);
editor::init_settings(cx);
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
SavedContextMetadata,
};
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
use clock::ReplicaId;
@@ -164,18 +164,16 @@ impl ContextStore {
) -> Result<proto::OpenContextResponse> {
let context_id = ContextId::from_proto(envelope.payload.context_id);
let operations = this.update(&mut cx, |this, cx| {
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"only the host contexts can be opened"
);
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host contexts can be opened"));
}
let context = this
.loaded_context_for_id(&context_id, cx)
.context("context not found")?;
anyhow::ensure!(
context.read(cx).replica_id() == ReplicaId::default(),
"context must be opened via the host"
);
if context.read(cx).replica_id() != ReplicaId::default() {
return Err(anyhow!("context must be opened via the host"));
}
anyhow::Ok(
context
@@ -195,10 +193,9 @@ impl ContextStore {
mut cx: AsyncApp,
) -> Result<proto::CreateContextResponse> {
let (context_id, operations) = this.update(&mut cx, |this, cx| {
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"can only create contexts as the host"
);
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("can only create contexts as the host"));
}
let context = this.create(cx);
let context_id = context.read(cx).id().clone();
@@ -240,10 +237,9 @@ impl ContextStore {
mut cx: AsyncApp,
) -> Result<proto::SynchronizeContextsResponse> {
this.update(&mut cx, |this, cx| {
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"only the host can synchronize contexts"
);
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host can synchronize contexts"));
}
let mut local_versions = Vec::new();
for remote_version_proto in envelope.payload.contexts {
@@ -374,7 +370,7 @@ impl ContextStore {
) -> Task<Result<Entity<AssistantContext>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
return Task::ready(Err(anyhow!("project was not remote")));
};
let replica_id = project.replica_id();
@@ -537,7 +533,7 @@ impl ContextStore {
) -> Task<Result<Entity<AssistantContext>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
return Task::ready(Err(anyhow!("project was not remote")));
};
if let Some(context) = self.loaded_context_for_id(&context_id, cx) {

View File

@@ -278,8 +278,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
let arguments = call
.arguments
.into_iter()
.filter_map(|argument| Some(line.get(argument)?.to_string()))
.iter()
.filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
.collect::<Vec<_>>();
let argument_range = first_arg_start..buffer_position;
(

View File

@@ -23,7 +23,6 @@ log.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
deepseek = { workspace = true, features = ["schemars"] }
mistral = { workspace = true, features = ["schemars"] }
schemars.workspace = true
serde.workspace = true
settings.workspace = true

View File

@@ -10,7 +10,6 @@ use deepseek::Model as DeepseekModel;
use gpui::{App, Pixels, SharedString};
use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel;
use mistral::Model as MistralModel;
use ollama::Model as OllamaModel;
use schemars::{JsonSchema, schema::Schema};
use serde::{Deserialize, Serialize};
@@ -42,7 +41,6 @@ pub enum NotifyWhenAgentWaiting {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
#[schemars(deny_unknown_fields)]
pub enum AssistantProviderContentV1 {
#[serde(rename = "zed.dev")]
ZedDotDev { default_model: Option<CloudModel> },
@@ -72,11 +70,6 @@ pub enum AssistantProviderContentV1 {
default_model: Option<DeepseekModel>,
api_url: Option<String>,
},
#[serde(rename = "mistral")]
Mistral {
default_model: Option<MistralModel>,
api_url: Option<String>,
},
}
#[derive(Default, Clone, Debug)]
@@ -100,7 +93,6 @@ pub struct AssistantSettings {
pub single_file_review: bool,
pub model_parameters: Vec<LanguageModelParameters>,
pub preferred_completion_mode: CompletionMode,
pub enable_feedback: bool,
}
impl AssistantSettings {
@@ -255,12 +247,6 @@ impl AssistantSettingsContent {
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Mistral { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "mistral".into(),
model: model.id().to_string(),
})
}
}),
inline_assistant_model: None,
commit_message_model: None,
@@ -274,7 +260,6 @@ impl AssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
},
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
},
@@ -305,7 +290,6 @@ impl AssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
},
None => AssistantSettingsContentV2::default(),
}
@@ -559,7 +543,6 @@ impl AssistantSettingsContent {
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(tag = "version")]
#[schemars(deny_unknown_fields)]
pub enum VersionedAssistantSettingsContent {
#[serde(rename = "1")]
V1(AssistantSettingsContentV1),
@@ -588,13 +571,11 @@ impl Default for VersionedAssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
})
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[schemars(deny_unknown_fields)]
pub struct AssistantSettingsContentV2 {
/// Whether the Assistant is enabled.
///
@@ -663,10 +644,6 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: normal
preferred_completion_mode: Option<CompletionMode>,
/// Whether to show thumb buttons for feedback in the agent panel.
///
/// Default: true
enable_feedback: Option<bool>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -704,7 +681,7 @@ impl JsonSchema for LanguageModelProviderSetting {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"amazon-bedrock".into(),
"bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
@@ -712,7 +689,6 @@ impl JsonSchema for LanguageModelProviderSetting {
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
"mistral".into(),
]),
..Default::default()
}
@@ -758,7 +734,6 @@ pub struct ContextServerPresetContent {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled.
///
@@ -788,7 +763,6 @@ pub struct AssistantSettingsContentV1 {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct LegacyAssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///
@@ -874,7 +848,6 @@ impl Settings for AssistantSettings {
&mut settings.preferred_completion_mode,
value.preferred_completion_mode,
);
merge(&mut settings.enable_feedback, value.enable_feedback);
settings
.model_parameters
@@ -1011,7 +984,6 @@ mod tests {
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
enable_feedback: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},

View File

@@ -9,7 +9,6 @@ use anyhow::Result;
use futures::StreamExt;
use futures::stream::{self, BoxStream};
use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::HighlightId;
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
@@ -17,7 +16,6 @@ use std::{
ops::Range,
sync::{Arc, atomic::AtomicBool},
};
use ui::ActiveTheme;
use workspace::{Workspace, ui::IconName};
pub fn init(cx: &mut App) {
@@ -327,18 +325,6 @@ impl SlashCommandLine {
}
}
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

View File

@@ -35,6 +35,7 @@ rope.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
terminal_view.workspace = true
text.workspace = true
toml.workspace = true
ui.workspace = true

View File

@@ -12,6 +12,11 @@ mod selection_command;
mod streaming_example_command;
mod symbols_command;
mod tab_command;
mod terminal_command;
use gpui::App;
use language::{CodeLabel, HighlightId};
use ui::ActiveTheme as _;
pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*;
@@ -27,5 +32,16 @@ pub use crate::selection_command::*;
pub use crate::streaming_example_command::*;
pub use crate::symbols_command::*;
pub use crate::tab_command::*;
pub use crate::terminal_command::*;
use assistant_slash_command::create_label_for_command;
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_slash_command::{
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
SlashCommandOutputSection, SlashCommandResult,
@@ -84,7 +84,9 @@ impl SlashCommand for ContextServerSlashCommand {
if let Some(server) = self.store.read(cx).get_running_server(&server_id) {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let completion_result = protocol
.completion(
@@ -137,16 +139,21 @@ impl SlashCommand for ContextServerSlashCommand {
let store = self.store.read(cx);
if let Some(server) = store.get_running_server(&server_id) {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
anyhow::ensure!(
result
.messages
.iter()
.all(|msg| matches!(msg.role, context_server::types::Role::User)),
"Prompt contains non-user roles, which is not supported"
);
// Check that there are only user roles
if result
.messages
.iter()
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
{
return Err(anyhow!(
"Prompt contains non-user roles, which is not supported"
));
}
// Extract text from user messages into a single prompt string
let mut prompt = result
@@ -185,7 +192,9 @@ impl SlashCommand for ContextServerSlashCommand {
}
fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String, String)> {
anyhow::ensure!(!arguments.is_empty(), "No arguments given");
if arguments.is_empty() {
return Err(anyhow!("No arguments given"));
}
match &prompt.arguments {
Some(args) if args.len() == 1 => {
@@ -193,16 +202,16 @@ fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String,
let arg_value = arguments.join(" ");
Ok((arg_name, arg_value))
}
Some(_) => anyhow::bail!("Prompt must have exactly one argument"),
None => anyhow::bail!("Prompt has no arguments"),
Some(_) => Err(anyhow!("Prompt must have exactly one argument")),
None => Err(anyhow!("Prompt has no arguments")),
}
}
fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<String, String>> {
match &prompt.arguments {
Some(args) if args.len() > 1 => {
anyhow::bail!("Prompt has more than one argument, which is not supported");
}
Some(args) if args.len() > 1 => Err(anyhow!(
"Prompt has more than one argument, which is not supported"
)),
Some(args) if args.len() == 1 => {
if !arguments.is_empty() {
let mut map = HashMap::default();
@@ -211,15 +220,15 @@ fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<Str
} else if arguments.is_empty() && args[0].required == Some(false) {
Ok(HashMap::default())
} else {
anyhow::bail!("Prompt expects argument but none given");
Err(anyhow!("Prompt expects argument but none given"))
}
}
Some(_) | None => {
anyhow::ensure!(
arguments.is_empty(),
"Prompt expects no arguments but some were given"
);
Ok(HashMap::default())
if arguments.is_empty() {
Ok(HashMap::default())
} else {
Err(anyhow!("Prompt expects no arguments but some were given"))
}
}
}
}

View File

@@ -118,7 +118,10 @@ impl SlashCommand for DeltaSlashCommand {
}
}
anyhow::ensure!(changes_detected, "no new changes detected");
if !changes_detected {
return Err(anyhow!("no new changes detected"));
}
Ok(output.to_event_stream())
})
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
@@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
window.spawn(cx, async move |_| {
task.await?
.map(|output| output.to_event_stream())
.context("No diagnostics found")
.ok_or_else(|| anyhow!("No diagnostics found"))
})
}
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Result, anyhow, bail};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
@@ -52,16 +52,15 @@ impl DocsSlashCommand {
.is_none()
{
let index_provider_deps = maybe!({
let workspace = workspace.clone().ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.as_ref()
.context("no workspace")?
.upgrade()
.context("workspace dropped")?;
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
.context("no Cargo workspace root found")?;
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
anyhow::Ok((fs, cargo_workspace_root))
});
@@ -79,11 +78,10 @@ impl DocsSlashCommand {
.is_none()
{
let http_client = maybe!({
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.as_ref()
.context("no workspace")?
.upgrade()
.context("workspace was dropped")?;
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().clone();
anyhow::Ok(project.read(cx).client().http_client())
});
@@ -176,7 +174,7 @@ impl SlashCommand for DocsSlashCommand {
let args = DocsSlashCommandArgs::parse(arguments);
let store = args
.provider()
.context("no docs provider specified")
.ok_or_else(|| anyhow!("no docs provider specified"))
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
cx.background_spawn(async move {
fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
@@ -289,7 +287,7 @@ impl SlashCommand for DocsSlashCommand {
let task = cx.background_spawn({
let store = args
.provider()
.context("no docs provider specified")
.ok_or_else(|| anyhow!("no docs provider specified"))
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
async move {
let (provider, key) = match args.clone() {

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Context, Result, anyhow, bail};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,

View File

@@ -230,10 +230,7 @@ fn collect_files(
})
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
else {
return futures::stream::once(async {
anyhow::bail!("invalid path");
})
.boxed();
return futures::stream::once(async { Err(anyhow!("invalid path")) }).boxed();
};
let project_handle = project.downgrade();

View File

@@ -1,7 +1,6 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::{TerminalView, terminal_panel::TerminalPanel};
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
@@ -9,10 +8,11 @@ use assistant_slash_command::{
};
use gpui::{App, Entity, Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::prelude::*;
use workspace::{Workspace, dock::Panel};
use assistant_slash_command::create_label_for_command;
use super::create_label_for_command;
pub struct TerminalSlashCommand;

View File

@@ -49,37 +49,6 @@ impl ActionLog {
is_created: bool,
cx: &mut Context<Self>,
) -> &mut TrackedBuffer {
let status = if is_created {
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
match tracked.status {
TrackedBufferStatus::Created {
existing_file_content,
} => TrackedBufferStatus::Created {
existing_file_content,
},
TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
TrackedBufferStatus::Created {
existing_file_content: Some(tracked.diff_base),
}
}
}
} else if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state().exists())
{
TrackedBufferStatus::Created {
existing_file_content: Some(buffer.read(cx).as_rope().clone()),
}
} else {
TrackedBufferStatus::Created {
existing_file_content: None,
}
}
} else {
TrackedBufferStatus::Modified
};
let tracked_buffer = self
.tracked_buffers
.entry(buffer.clone())
@@ -91,21 +60,36 @@ impl ActionLog {
let text_snapshot = buffer.read(cx).text_snapshot();
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
let diff_base;
let base_text;
let status;
let unreviewed_changes;
if is_created {
diff_base = Rope::default();
let existing_file_content = if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state().exists())
{
Some(text_snapshot.as_rope().clone())
} else {
None
};
base_text = Rope::default();
status = TrackedBufferStatus::Created {
existing_file_content,
};
unreviewed_changes = Patch::new(vec![Edit {
old: 0..1,
new: 0..text_snapshot.max_point().row + 1,
}])
} else {
diff_base = buffer.read(cx).as_rope().clone();
base_text = buffer.read(cx).as_rope().clone();
status = TrackedBufferStatus::Modified;
unreviewed_changes = Patch::default();
}
TrackedBuffer {
buffer: buffer.clone(),
diff_base,
base_text,
unreviewed_changes,
snapshot: text_snapshot.clone(),
status,
@@ -200,7 +184,7 @@ impl ActionLog {
.context("buffer not tracked")?;
let rebase = cx.background_spawn({
let mut base_text = tracked_buffer.diff_base.clone();
let mut base_text = tracked_buffer.base_text.clone();
let old_snapshot = tracked_buffer.snapshot.clone();
let new_snapshot = buffer_snapshot.clone();
let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
@@ -226,7 +210,7 @@ impl ActionLog {
))
})??;
let (new_base_text, new_diff_base) = rebase.await;
let (new_base_text, new_base_text_rope) = rebase.await;
let diff_snapshot = BufferDiff::update_diff(
diff.clone(),
buffer_snapshot.clone(),
@@ -245,23 +229,24 @@ impl ActionLog {
.background_spawn({
let diff_snapshot = diff_snapshot.clone();
let buffer_snapshot = buffer_snapshot.clone();
let new_diff_base = new_diff_base.clone();
let new_base_text_rope = new_base_text_rope.clone();
async move {
let mut unreviewed_changes = Patch::default();
for hunk in diff_snapshot.hunks_intersecting_range(
Anchor::MIN..Anchor::MAX,
&buffer_snapshot,
) {
let old_range = new_diff_base
let old_range = new_base_text_rope
.offset_to_point(hunk.diff_base_byte_range.start)
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
..new_base_text_rope
.offset_to_point(hunk.diff_base_byte_range.end);
let new_range = hunk.range.start..hunk.range.end;
unreviewed_changes.push(point_to_row_edit(
Edit {
old: old_range,
new: new_range,
},
&new_diff_base,
&new_base_text_rope,
&buffer_snapshot.as_rope(),
));
}
@@ -279,7 +264,7 @@ impl ActionLog {
.tracked_buffers
.get_mut(&buffer)
.context("buffer not tracked")?;
tracked_buffer.diff_base = new_diff_base;
tracked_buffer.base_text = new_base_text_rope;
tracked_buffer.snapshot = buffer_snapshot;
tracked_buffer.unreviewed_changes = unreviewed_changes;
cx.notify();
@@ -298,6 +283,7 @@ impl ActionLog {
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
self.tracked_buffers.remove(&buffer);
self.track_buffer_internal(buffer.clone(), true, cx);
}
@@ -360,11 +346,11 @@ impl ActionLog {
true
} else {
let old_range = tracked_buffer
.diff_base
.base_text
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.diff_base.point_to_offset(cmp::min(
..tracked_buffer.base_text.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.diff_base.max_point(),
tracked_buffer.base_text.max_point(),
));
let new_range = tracked_buffer
.snapshot
@@ -373,7 +359,7 @@ impl ActionLog {
Point::new(edit.new.end, 0),
tracked_buffer.snapshot.max_point(),
));
tracked_buffer.diff_base.replace(
tracked_buffer.base_text.replace(
old_range,
&tracked_buffer
.snapshot
@@ -431,7 +417,7 @@ impl ActionLog {
}
TrackedBufferStatus::Deleted => {
buffer.update(cx, |buffer, cx| {
buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
buffer.set_text(tracked_buffer.base_text.to_string(), cx)
});
let save = self
.project
@@ -478,14 +464,14 @@ impl ActionLog {
if revert {
let old_range = tracked_buffer
.diff_base
.base_text
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.diff_base.point_to_offset(cmp::min(
..tracked_buffer.base_text.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.diff_base.max_point(),
tracked_buffer.base_text.max_point(),
));
let old_text = tracked_buffer
.diff_base
.base_text
.chunks_in_range(old_range)
.collect::<String>();
edits_to_revert.push((new_range, old_text));
@@ -506,7 +492,7 @@ impl ActionLog {
TrackedBufferStatus::Deleted => false,
_ => {
tracked_buffer.unreviewed_changes.clear();
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
true
}
@@ -669,7 +655,7 @@ enum TrackedBufferStatus {
struct TrackedBuffer {
buffer: Entity<Buffer>,
diff_base: Rope,
base_text: Rope,
unreviewed_changes: Patch<u32>,
status: TrackedBufferStatus,
version: clock::Global,
@@ -1108,86 +1094,6 @@ mod tests {
);
}
#[gpui::test(iterations = 10)]
async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({
"file1": "Lorem ipsum dolor"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 37),
diff_status: DiffHunkStatusKind::Modified,
old_text: "Lorem ipsum dolor".into(),
}],
)]
);
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 9),
diff_status: DiffHunkStatusKind::Added,
old_text: "".into(),
}],
)]
);
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
assert_eq!(
buffer.read_with(cx, |buffer, _cx| buffer.text()),
"Lorem ipsum dolor"
);
}
#[gpui::test(iterations = 10)]
async fn test_deleting_files(cx: &mut TestAppContext) {
init_test(cx);
@@ -1695,7 +1601,7 @@ mod tests {
cx.run_until_parked();
action_log.update(cx, |log, cx| {
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
let mut old_text = tracked_buffer.diff_base.clone();
let mut old_text = tracked_buffer.base_text.clone();
let new_text = buffer.read(cx).as_rope();
for edit in tracked_buffer.unreviewed_changes.edits() {
let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));

View File

@@ -19,7 +19,6 @@ use gpui::Window;
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use icons::IconName;
use language_model::LanguageModel;
use language_model::LanguageModelImage;
use language_model::LanguageModelRequest;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
@@ -66,50 +65,21 @@ impl ToolUseStatus {
#[derive(Debug)]
pub struct ToolResultOutput {
pub content: ToolResultContent,
pub content: String,
pub output: Option<serde_json::Value>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ToolResultContent {
Text(String),
Image(LanguageModelImage),
}
impl ToolResultContent {
pub fn len(&self) -> usize {
match self {
ToolResultContent::Text(str) => str.len(),
ToolResultContent::Image(image) => image.len(),
}
}
pub fn is_empty(&self) -> bool {
match self {
ToolResultContent::Text(str) => str.is_empty(),
ToolResultContent::Image(image) => image.is_empty(),
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
ToolResultContent::Text(str) => Some(str),
ToolResultContent::Image(_) => None,
}
}
}
impl From<String> for ToolResultOutput {
fn from(value: String) -> Self {
ToolResultOutput {
content: ToolResultContent::Text(value),
content: value,
output: None,
}
}
}
impl Deref for ToolResultOutput {
type Target = ToolResultContent;
type Target = String;
fn deref(&self) -> &Self::Target {
&self.content

View File

@@ -1,5 +1,5 @@
use crate::ActionLog;
use anyhow::{Context as _, Result};
use anyhow::{Result, anyhow};
use gpui::{AsyncApp, Entity};
use language::{OutlineItem, ParseStatus};
use project::Project;
@@ -22,7 +22,7 @@ pub async fn file_outline(
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&path, cx)
.with_context(|| format!("Path {path} not found in project"))
.ok_or_else(|| anyhow!("Path {path} not found in project"))
})??;
project
@@ -41,9 +41,9 @@ pub async fn file_outline(
}
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let outline = snapshot
.outline(None)
.context("No outline information available for this file at path {path}")?;
let Some(outline) = snapshot.outline(None) else {
return Err(anyhow!("No outline information available for this file."));
};
render_outline(
outline

View File

@@ -27,10 +27,12 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
const UNSUPPORTED_KEYS: [&str; 4] = ["if", "then", "else", "$ref"];
for key in UNSUPPORTED_KEYS {
anyhow::ensure!(
!obj.contains_key(key),
"Schema cannot be made compatible because it contains \"{key}\""
);
if obj.contains_key(key) {
return Err(anyhow::anyhow!(
"Schema cannot be made compatible because it contains \"{}\" ",
key
));
}
}
const KEYS_TO_REMOVE: [&str; 5] = [

View File

@@ -35,13 +35,13 @@ indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
linkme.workspace = true
log.workspace = true
markdown.workspace = true
open.workspace = true
paths.workspace = true
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
regex.workspace = true
rust-embed.workspace = true
schemars.workspace = true

View File

@@ -42,7 +42,7 @@ use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::thinking_tool::ThinkingTool;
pub use edit_file_tool::{EditFileMode, EditFileToolInput};
pub use edit_file_tool::EditFileToolInput;
pub use find_path_tool::FindPathToolInput;
pub use open_tool::OpenTool;
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
@@ -107,13 +107,17 @@ impl Tool for CopyPathTool {
});
cx.background_spawn(async move {
let _ = copy_task.await.with_context(|| {
format!(
"Copying {} to {}",
input.source_path, input.destination_path
)
})?;
Ok(format!("Copied {} to {}", input.source_path, input.destination_path).into())
match copy_task.await {
Ok(_) => Ok(
format!("Copied {} to {}", input.source_path, input.destination_path).into(),
),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",
input.source_path,
input.destination_path,
err
)),
}
})
.into()
}

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
@@ -86,7 +86,7 @@ impl Tool for CreateDirectoryTool {
project.create_entry(project_path.clone(), true, cx)
})?
.await
.with_context(|| format!("Creating directory {destination_path}"))?;
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok(format!("Created directory {destination_path}").into())
})

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
@@ -122,17 +122,19 @@ impl Tool for DeletePathTool {
}
}
let deletion_task = project
.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?
.with_context(|| {
format!("Couldn't delete {path_str} because that path isn't in this project.")
})?;
deletion_task
.await
.with_context(|| format!("Deleting {path_str}"))?;
Ok(format!("Deleted {path_str}").into())
let delete = project.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?;
match delete {
Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}").into()),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
},
None => Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)),
}
})
.into()
}

View File

@@ -24,7 +24,6 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
use streaming_diff::{CharOperation, StreamingDiff};
use util::debug_panic;
#[derive(Serialize)]
struct CreateFilePromptTemplate {
@@ -544,11 +543,6 @@ impl EditAgent {
if last_message.content.is_empty() {
conversation.messages.pop();
}
} else {
debug_panic!(
"Last message must be an Assistant tool calling! Got {:?}",
last_message.content
);
}
}

View File

@@ -1,11 +1,7 @@
use super::*;
use crate::{
ReadFileToolInput,
edit_file_tool::{EditFileMode, EditFileToolInput},
grep_tool::GrepToolInput,
list_directory_tool::ListDirectoryToolInput,
};
use crate::{ReadFileToolInput, edit_file_tool::EditFileToolInput, grep_tool::GrepToolInput};
use Role::*;
use anyhow::anyhow;
use assistant_tool::ToolRegistry;
use client::{Client, UserStore};
use collections::HashMap;
@@ -14,11 +10,10 @@ use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext};
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId,
};
use project::Project;
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
use rand::prelude::*;
use reqwest_client::ReqwestClient;
use serde_json::json;
@@ -26,7 +21,6 @@ use std::{
cmp::Reverse,
fmt::{self, Display},
io::Write as _,
str::FromStr,
sync::mpsc,
};
use util::path;
@@ -41,8 +35,8 @@ fn eval_extract_handle_command_output() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -77,14 +71,16 @@ fn eval_extract_handle_command_output() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
},
);
}
@@ -98,8 +94,8 @@ fn eval_delete_run_git_blame() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -131,14 +127,16 @@ fn eval_delete_run_git_blame() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
},
);
}
@@ -151,8 +149,8 @@ fn eval_translate_doc_comments() {
eval(
200,
1.,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -184,14 +182,16 @@ fn eval_translate_doc_comments() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff("Doc comments were translated to Italian"),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff("Doc comments were translated to Italian"),
},
);
}
@@ -205,8 +205,8 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -297,17 +297,19 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- The compile_parser_to_wasm method has been changed to use wasi-sdk
- ureq is used to download the SDK for current platform and architecture
"}),
),
},
);
}
@@ -318,10 +320,10 @@ fn eval_disable_cursor_blinking() {
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
let edit_description = "Comment out the call to `BlinkManager::enable`";
eval(
100,
200,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(User, [text("Let's research how to cursor blinking works.")]),
message(
Assistant,
@@ -370,18 +372,20 @@ fn eval_disable_cursor_blinking() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- Calls to BlinkManager in `observe_window_activation` were commented out
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
- All the edits have valid indentation
"}),
),
},
);
}
@@ -394,8 +398,8 @@ fn eval_from_pixels_constructor() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(indoc! {"
@@ -562,17 +566,19 @@ fn eval_from_pixels_constructor() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
},
);
}
@@ -580,13 +586,12 @@ fn eval_from_pixels_constructor() {
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_zode() {
let input_file_path = "root/zode.py";
let input_content = None;
let edit_description = "Create the main Zode CLI script";
eval(
200,
1.,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
message(
Assistant,
@@ -638,14 +643,16 @@ fn eval_zode() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Create,
create_or_overwrite: true,
},
),
],
),
],
input_content,
EvalAssertion::new(async move |sample, _, _cx| {
input_path: input_file_path.into(),
input_content: None,
edit_description: edit_description.into(),
assertion: EvalAssertion::new(async move |sample, _, _cx| {
let invalid_starts = [' ', '`', '\n'];
let mut message = String::new();
for start in invalid_starts {
@@ -669,7 +676,7 @@ fn eval_zode() {
})
}
}),
),
},
);
}
@@ -682,8 +689,8 @@ fn eval_add_overwrite_test() {
eval(
200,
0.5, // TODO: make this eval better
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(indoc! {"
@@ -881,99 +888,19 @@ fn eval_add_overwrite_test() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
},
),
],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(
"A new test for overwritten files was created, without changing any previous test",
),
),
);
}
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_create_empty_file() {
// Check that Edit Agent can create a file without writing its
// thoughts into it. This issue is not specific to empty files, but
// it's easier to reproduce with them.
//
//
// Model | Pass rate
// ============================================
//
// --------------------------------------------
// Prompt version: 2025-05-21
// --------------------------------------------
//
// claude-3.7-sonnet | 1.00
// gemini-2.5-pro-preview-03-25 | 1.00
// gemini-2.5-flash-preview-04-17 | 1.00
// gpt-4.1 | 1.00
//
//
// TODO: gpt-4.1-mini errored 38 times:
// "data did not match any variant of untagged enum ResponseStreamResult"
//
let input_file_content = None;
let expected_output_content = String::new();
eval(
100,
0.99,
EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
message(
Assistant,
[
text(formatdoc! {"
I'll help you create a second empty todo file.
First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one.
"}),
tool_use(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
ListDirectoryToolInput {
path: "root".to_string(),
},
),
],
),
message(
User,
[tool_result(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
"root/TODO\nroot/TODO2\nroot/new.txt\n",
)],
),
message(
Assistant,
[
text(formatdoc! {"
I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory:
"}),
tool_use(
"toolu_01Tb3iQ9griqSYMmVuykQPWU",
"edit_file",
EditFileToolInput {
display_description: "Create empty TODO3 file".to_string(),
mode: EditFileMode::Create,
path: "root/TODO3".into(),
},
),
],
),
],
input_file_content,
// Bad behavior is to write something like
// "I'll create an empty TODO3 file as requested."
EvalAssertion::assert_eq(expected_output_content),
),
},
);
}
@@ -1024,7 +951,7 @@ fn tool_result(
tool_use_id: LanguageModelToolUseId::from(id.into()),
tool_name: name.into(),
is_error: false,
content: LanguageModelToolResultContent::Text(result.into()),
content: result.into(),
output: None,
})
}
@@ -1032,46 +959,12 @@ fn tool_result(
#[derive(Clone)]
struct EvalInput {
conversation: Vec<LanguageModelRequestMessage>,
edit_file_input: EditFileToolInput,
input_path: PathBuf,
input_content: Option<String>,
edit_description: String,
assertion: EvalAssertion,
}
impl EvalInput {
fn from_conversation(
conversation: Vec<LanguageModelRequestMessage>,
input_content: Option<String>,
assertion: EvalAssertion,
) -> Self {
let msg = conversation.last().expect("Conversation must not be empty");
if msg.role != Role::Assistant {
panic!("Conversation must end with an assistant message");
}
let tool_use = msg
.content
.iter()
.flat_map(|content| match content {
MessageContent::ToolUse(tool_use) if tool_use.name == "edit_file".into() => {
Some(tool_use)
}
_ => None,
})
.next()
.expect("Conversation must end with an edit_file tool use")
.clone();
let edit_file_input: EditFileToolInput =
serde_json::from_value(tool_use.input.clone()).unwrap();
EvalInput {
conversation,
edit_file_input,
input_content,
assertion,
}
}
}
#[derive(Clone)]
struct EvalSample {
text: String,
@@ -1179,7 +1072,10 @@ impl EvalAssertion {
}
}
anyhow::bail!("No score found in response. Raw output: {output}");
Err(anyhow!(
"No score found in response. Raw output: {}",
output
))
})
}
@@ -1316,7 +1212,7 @@ fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usiz
passed_count as f64 / evaluated_count as f64
};
print!(
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
"\r\x1b[KEvaluated {}/{} ({:.2}%)",
evaluated_count,
iterations,
passed_ratio * 100.0
@@ -1355,21 +1251,13 @@ impl EditAgentTest {
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let agent_model = SelectedModel::from_str(
&std::env::var("ZED_AGENT_MODEL")
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
)
.unwrap();
let judge_model = SelectedModel::from_str(
&std::env::var("ZED_JUDGE_MODEL")
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
)
.unwrap();
let (agent_model, judge_model) = cx
.update(|cx| {
cx.spawn(async move |cx| {
let agent_model = Self::load_model(&agent_model, cx).await;
let judge_model = Self::load_model(&judge_model, cx).await;
let agent_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
let judge_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
(agent_model.unwrap(), judge_model.unwrap())
})
})
@@ -1384,17 +1272,15 @@ impl EditAgentTest {
}
async fn load_model(
selected_model: &SelectedModel,
provider: &str,
id: &str,
cx: &mut AsyncApp,
) -> Result<Arc<dyn LanguageModel>> {
let (provider, model) = cx.update(|cx| {
let models = LanguageModelRegistry::read_global(cx);
let model = models
.available_models(cx)
.find(|model| {
model.provider_id() == selected_model.provider
&& model.id() == selected_model.model
})
.find(|model| model.provider_id().0 == provider && model.id().0 == id)
.unwrap();
let provider = models.provider(&model.provider_id()).unwrap();
(provider, model)
@@ -1407,7 +1293,7 @@ impl EditAgentTest {
let path = self
.project
.read_with(cx, |project, cx| {
project.find_project_path(eval.edit_file_input.path, cx)
project.find_project_path(eval.input_path, cx)
})
.unwrap();
let buffer = self
@@ -1415,68 +1301,31 @@ impl EditAgentTest {
.update(cx, |project, cx| project.open_buffer(path, cx))
.await
.unwrap();
let tools = cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
})
.collect::<Vec<_>>()
});
let tool_names = tools
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
let worktrees = vec![WorktreeContext {
root_name: "root".to_string(),
rules_file: None,
}];
let prompt_builder = PromptBuilder::new(None)?;
let project_context = ProjectContext::new(worktrees, Vec::default());
let system_prompt = prompt_builder.generate_assistant_system_prompt(
&project_context,
&ModelContext {
available_tools: tool_names,
},
)?;
let has_system_prompt = eval
.conversation
.first()
.map_or(false, |msg| msg.role == Role::System);
let messages = if has_system_prompt {
eval.conversation
} else {
[LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
}]
.into_iter()
.chain(eval.conversation)
.collect::<Vec<_>>()
};
let conversation = LanguageModelRequest {
messages,
tools,
messages: eval.conversation,
tools: cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
})
.collect()
}),
..Default::default()
};
let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) {
if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
}
let edit_output = if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
let (edit_output, _) = self.agent.edit(
buffer.clone(),
eval.edit_file_input.display_description,
eval.edit_description,
&conversation,
&mut cx.to_async(),
);
@@ -1484,7 +1333,7 @@ impl EditAgentTest {
} else {
let (edit_output, _) = self.agent.overwrite(
buffer.clone(),
eval.edit_file_input.display_description,
eval.edit_description,
&conversation,
&mut cx.to_async(),
);

View File

@@ -98,21 +98,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("starting git blame process")?;
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
let stdin = child
.stdin
@@ -92,7 +92,10 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -100,7 +103,7 @@ async fn run_git_blame(
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
anyhow::bail!("git blame process failed: {stderr}");
return Err(anyhow!("git blame process failed: {}", stderr));
}
Ok(String::from_utf8(output.stdout)?)
@@ -141,21 +144,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -1249,7 +1249,7 @@ pub struct ActiveDiagnosticGroup {
}
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum ActiveDiagnostic {
None,
All,
@@ -5272,7 +5272,7 @@ impl Editor {
task.await?;
}
anyhow::Ok(())
Ok::<_, anyhow::Error>(())
})
.detach_and_log_err(cx);
}
@@ -10369,8 +10369,8 @@ impl Editor {
.map(|line| {
line.strip_prefix(&line_prefix)
.or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start()))
.with_context(|| {
format!("line did not start with prefix {line_prefix:?}: {line:?}")
.ok_or_else(|| {
anyhow!("line did not start with prefix {line_prefix:?}: {line:?}")
})
})
.collect::<Result<Vec<_>, _>>()
@@ -16944,7 +16944,7 @@ impl Editor {
Err(err) => {
let message = format!("Failed to copy permalink: {err}");
anyhow::Result::<()>::Err(err).log_err();
Err::<(), anyhow::Error>(err).log_err();
if let Some(workspace) = workspace {
workspace
@@ -16999,7 +16999,7 @@ impl Editor {
Err(err) => {
let message = format!("Failed to open permalink: {err}");
anyhow::Result::<()>::Err(err).log_err();
Err::<(), anyhow::Error>(err).log_err();
if let Some(workspace) = workspace {
workspace

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("starting git blame process")?;
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
let stdin = child
.stdin
@@ -92,7 +92,10 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
handle_command_output(output)
}
@@ -104,7 +107,7 @@ fn handle_command_output(output: std::process::Output) -> Result<String> {
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
anyhow::bail!("git blame process failed: {stderr}");
return Err(anyhow!("git blame process failed: {}", stderr));
}
Ok(String::from_utf8(output.stdout)?)
@@ -145,21 +148,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("starting git blame process")?;
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
let stdin = child
.stdin
@@ -92,7 +92,10 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -100,7 +103,7 @@ async fn run_git_blame(
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
anyhow::bail!("git blame process failed: {stderr}");
return Err(anyhow!("git blame process failed: {}", stderr));
}
Ok(String::from_utf8(output.stdout)?)
@@ -141,21 +144,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -20,7 +20,7 @@ use std::{
#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
use anyhow::Error;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context, Result, anyhow};
use etcetera::BaseStrategy as _;
use fs4::fs_std::FileExt;
use indoc::indoc;
@@ -875,13 +875,16 @@ impl Loader {
FileExt::unlock(lock_file)?;
fs::remove_file(lock_path)?;
anyhow::ensure!(
output.status.success(),
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok(())
if output.status.success() {
Ok(())
} else {
Err(anyhow!(
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))
}
}
#[cfg(unix)]
@@ -938,13 +941,17 @@ impl Loader {
.map(|f| format!(" `{f}`"))
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!(format!(indoc! {"
Missing required functions in the external scanner, parsing won't work without these!
{missing}
return Err(anyhow!(format!(
indoc! {"
Missing required functions in the external scanner, parsing won't work without these!
You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners
"}));
{}
You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners
"},
missing,
)));
}
}
}
@@ -1001,9 +1008,9 @@ impl Loader {
{
EmccSource::Podman
} else {
anyhow::bail!(
return Err(anyhow!(
"You must have either emcc, docker, or podman on your PATH to run this command"
);
));
};
let mut command = match source {
@@ -1096,11 +1103,12 @@ impl Loader {
.spawn()
.with_context(|| "Failed to run emcc command")?
.wait()?;
anyhow::ensure!(status.success(), "emcc command failed");
let source_path = src_path.join(output_name);
fs::rename(&source_path, &output_path).with_context(|| {
format!("failed to rename wasm output file from {source_path:?} to {output_path:?}")
})?;
if !status.success() {
return Err(anyhow!("emcc command failed"));
}
fs::rename(src_path.join(output_name), output_path)
.context("failed to rename wasm output file")?;
Ok(())
}
@@ -1177,8 +1185,11 @@ impl Loader {
.map(|path| {
let path = parser_path.join(path);
// prevent p being above/outside of parser_path
anyhow::ensure!(path.starts_with(parser_path), "External file path {path:?} is outside of parser directory {parser_path:?}");
Ok(path)
if path.starts_with(parser_path) {
Ok(path)
} else {
Err(anyhow!("External file path {path:?} is outside of parser directory {parser_path:?}"))
}
})
.collect::<Result<Vec<_>>>()
}).transpose()?,
@@ -1313,8 +1324,11 @@ impl Loader {
let name = GRAMMAR_NAME_REGEX
.captures(&first_three_lines)
.and_then(|c| c.get(1))
.with_context(|| {
format!("Failed to parse the language name from grammar.json at {grammar_path:?}")
.ok_or_else(|| {
anyhow!(
"Failed to parse the language name from grammar.json at {}",
grammar_path.display()
)
})?;
Ok(name.as_str().to_string())
@@ -1333,7 +1347,7 @@ impl Loader {
{
Ok(config.0)
} else {
anyhow::bail!("Unknown scope '{scope}'")
Err(anyhow!("Unknown scope '{scope}'"))
}
} else if let Some((lang, _)) = self
.language_configuration_for_file_name(path)
@@ -1357,7 +1371,7 @@ impl Loader {
} else if let Some(lang) = self.language_configuration_for_first_line_regex(path)? {
Ok(lang.0)
} else {
anyhow::bail!("No language found");
Err(anyhow!("No language found"))
}
}

View File

@@ -3,10 +3,9 @@ use crate::{
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
schema::json_schema_for,
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
ToolUseStatus,
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
@@ -22,7 +21,7 @@ use language::{
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use project::{Project, ProjectPath};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -38,7 +37,7 @@ use workspace::Workspace;
pub struct EditFileTool;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
@@ -76,22 +75,12 @@ pub struct EditFileToolInput {
/// </example>
pub path: PathBuf,
/// The mode of operation on the file. Possible values:
/// - 'edit': Make granular edits to an existing file.
/// - 'create': Create a new file if it doesn't exist.
/// - 'overwrite': Replace the entire contents of an existing file.
/// If true, this tool will recreate the file from scratch.
/// If false, this tool will produce granular edits to an existing file.
///
/// When a file already exists or you just created it, prefer editing
/// When a file already exists or you just created it, always prefer editing
/// it as opposed to recreating it from scratch.
pub mode: EditFileMode,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum EditFileMode {
Edit,
Create,
Overwrite,
pub create_or_overwrite: bool,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -171,9 +160,12 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project_path = match resolve_path(&input, project.clone(), cx) {
Ok(path) => path,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)))
.into();
};
let card = window.and_then(|window| {
@@ -196,6 +188,16 @@ impl Tool for EditFileTool {
})?
.await?;
let exists = buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(false, |file| file.disk_state().exists())
})?;
if !input.create_or_overwrite && !exists {
return Err(anyhow!("{} not found", input.path.display()));
}
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
@@ -204,15 +206,15 @@ impl Tool for EditFileTool {
})
.await;
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
edit_agent.edit(
let (output, mut events) = if input.create_or_overwrite {
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
} else {
edit_agent.overwrite(
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
&request,
@@ -279,21 +281,18 @@ impl Tool for EditFileTool {
let input_path = input.path.display();
if diff.is_empty() {
anyhow::ensure!(
!hallucinated_old_text,
formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}
);
Ok("No edits were made.".to_string().into())
if hallucinated_old_text {
Err(anyhow!(formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}))
} else {
Ok("No edits were made.".to_string().into())
}
} else {
Ok(ToolResultOutput {
content: ToolResultContent::Text(format!(
"Edited {}:\n\n```diff\n{}\n```",
input_path, diff
)),
content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff),
output: serde_json::to_value(output).ok(),
})
}
@@ -332,71 +331,6 @@ impl Tool for EditFileTool {
}
}
/// Validate that the file path is valid, meaning:
///
/// - For `edit` and `overwrite`, the path must point to an existing file.
/// - For `create`, the file must not already exist, but it's parent dir must exist.
fn resolve_path(
input: &EditFileToolInput,
project: Entity<Project>,
cx: &mut App,
) -> Result<ProjectPath> {
let project = project.read(cx);
match input.mode {
EditFileMode::Edit | EditFileMode::Overwrite => {
let path = project
.find_project_path(&input.path, cx)
.context("Can't edit file: path not found")?;
let entry = project
.entry_for_path(&path, cx)
.context("Can't edit file: path not found")?;
anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
Ok(path)
}
EditFileMode::Create => {
if let Some(path) = project.find_project_path(&input.path, cx) {
anyhow::ensure!(
project.entry_for_path(&path, cx).is_none(),
"Can't create file: file already exists"
);
}
let parent_path = input
.path
.parent()
.context("Can't create file: incorrect path")?;
let parent_project_path = project.find_project_path(&parent_path, cx);
let parent_entry = parent_project_path
.as_ref()
.and_then(|path| project.entry_for_path(&path, cx))
.context("Can't create file: parent directory doesn't exist")?;
anyhow::ensure!(
parent_entry.is_dir(),
"Can't create file: parent is not a directory"
);
let file_name = input
.path
.file_name()
.context("Can't create file: invalid filename")?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: Arc::from(parent.path.join(file_name)),
..parent
});
new_file_path.context("Can't create file")
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
@@ -448,7 +382,7 @@ impl EditFileToolCard {
diff_task: None,
preview_expanded: true,
error_expanded: None,
full_height_expanded: true,
full_height_expanded: false,
total_lines: None,
}
}
@@ -917,7 +851,6 @@ async fn build_buffer_diff(
#[cfg(test)]
mod tests {
use super::*;
use client::TelemetrySettings;
use fs::FakeFs;
use gpui::TestAppContext;
use language_model::fake_provider::FakeLanguageModel;
@@ -939,7 +872,7 @@ mod tests {
let input = serde_json::to_value(EditFileToolInput {
display_description: "Some edit".into(),
path: "root/nonexistent_file.txt".into(),
mode: EditFileMode::Edit,
create_or_overwrite: false,
})
.unwrap();
Arc::new(EditFileTool)
@@ -957,102 +890,10 @@ mod tests {
.await;
assert_eq!(
result.unwrap_err().to_string(),
"Can't edit file: path not found"
"root/nonexistent_file.txt not found"
);
}
#[gpui::test]
async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Create;
let result = test_resolve_path(mode, "root/new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "dir/new.txt", cx);
assert_resolved_path_eq(result.await, "dir/new.txt");
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: file already exists"
);
let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: parent directory doesn't exist"
);
}
#[gpui::test]
async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Edit;
let path_with_root = "root/dir/subdir/existing.txt";
let path_without_root = "dir/subdir/existing.txt";
let result = test_resolve_path(mode, path_with_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, path_without_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path not found"
);
let result = test_resolve_path(mode, "root/dir", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path is a directory"
);
}
async fn test_resolve_path(
mode: &EditFileMode,
path: &str,
cx: &mut TestAppContext,
) -> anyhow::Result<ProjectPath> {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"dir": {
"subdir": {
"existing.txt": "hello"
}
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let input = EditFileToolInput {
display_description: "Some edit".into(),
path: path.into(),
mode: mode.clone(),
};
let result = cx.update(|cx| resolve_path(&input, project, cx));
result
}
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
let actual = path
.expect("Should return valid path")
.path
.to_str()
.unwrap()
.replace("\\", "/"); // Naive Windows paths normalization
assert_eq!(actual, expected);
}
#[test]
fn still_streaming_ui_text_with_path() {
let input = json!({
@@ -1125,7 +966,6 @@ mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
Project::init_settings(cx);
});
}

View File

@@ -1,8 +1,6 @@
use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use editor::Editor;
use futures::channel::oneshot::{self, Receiver};
use gpui::{
@@ -40,12 +38,6 @@ pub struct FindPathToolInput {
pub offset: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct FindPathToolOutput {
glob: String,
paths: Vec<PathBuf>,
}
const RESULTS_PER_PAGE: usize = 50;
pub struct FindPathTool;
@@ -119,18 +111,10 @@ impl Tool for FindPathTool {
)
.unwrap();
}
let output = FindPathToolOutput {
glob,
paths: matches.clone(),
};
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
}
Ok(ToolResultOutput {
content: ToolResultContent::Text(message),
output: Some(serde_json::to_value(output)?),
})
Ok(message.into())
}
});
@@ -139,18 +123,6 @@ impl Tool for FindPathTool {
card: Some(card.into()),
}
}
fn deserialize_card(
self: Arc<Self>,
output: serde_json::Value,
_project: Entity<Project>,
_window: &mut Window,
cx: &mut App,
) -> Option<assistant_tool::AnyToolCard> {
let output = serde_json::from_value::<FindPathToolOutput>(output).ok()?;
let card = cx.new(|_| FindPathToolCard::from_output(output));
Some(card.into())
}
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
@@ -208,15 +180,6 @@ impl FindPathToolCard {
_receiver_task: Some(_receiver_task),
}
}
fn from_output(output: FindPathToolOutput) -> Self {
Self {
glob: output.glob,
paths: output.paths,
expanded: false,
_receiver_task: None,
}
}
}
impl ToolCard for FindPathToolCard {

View File

@@ -109,7 +109,7 @@ impl Tool for GrepTool {
let input = match serde_json::from_value::<GrepToolInput>(input) {
Ok(input) => input,
Err(error) => {
return Task::ready(Err(anyhow!("Failed to parse input: {error}"))).into();
return Task::ready(Err(anyhow!("Failed to parse input: {}", error))).into();
}
};
@@ -122,7 +122,7 @@ impl Tool for GrepTool {
) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid include glob pattern: {error}"))).into();
return Task::ready(Err(anyhow!("invalid include glob pattern: {}", error))).into();
}
};
@@ -752,9 +752,9 @@ mod tests {
match task.output.await {
Ok(result) => {
if cfg!(windows) {
result.content.as_str().unwrap().replace("root\\", "root/")
result.content.replace("root\\", "root/")
} else {
result.content.as_str().unwrap().to_string()
result.content
}
}
Err(e) => panic!("Failed to run grep tool: {}", e),

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
@@ -117,10 +117,17 @@ impl Tool for MovePathTool {
});
cx.background_spawn(async move {
let _ = rename_task.await.with_context(|| {
format!("Moving {} to {}", input.source_path, input.destination_path)
})?;
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
match rename_task.await {
Ok(_) => {
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
}
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",
input.source_path,
input.destination_path,
err
)),
}
})
.into()
}

View File

@@ -1,17 +1,13 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::outline;
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{ToolResultContent, outline};
use gpui::{AnyWindowHandle, App, Entity, Task};
use project::{ImageItem, image_store};
use assistant_tool::ToolResultOutput;
use indoc::formatdoc;
use itertools::Itertools;
use language::{Anchor, Point};
use language_model::{
LanguageModel, LanguageModelImage, LanguageModelRequest, LanguageModelToolSchemaFormat,
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{AgentLocation, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -90,7 +86,7 @@ impl Tool for ReadFileTool {
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
model: Arc<dyn LanguageModel>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -104,42 +100,6 @@ impl Tool for ReadFileTool {
};
let file_path = input.path.clone();
if image_store::is_image_file(&project, &project_path, cx) {
if !model.supports_images() {
return Task::ready(Err(anyhow!(
"Attempted to read an image, but Zed doesn't currently sending images to {}.",
model.name().0
)))
.into();
}
let task = cx.spawn(async move |cx| -> Result<ToolResultOutput> {
let image_entity: Entity<ImageItem> = cx
.update(|cx| {
project.update(cx, |project, cx| {
project.open_image(project_path.clone(), cx)
})
})?
.await?;
let image =
image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?;
let language_model_image = cx
.update(|cx| LanguageModelImage::from_image(image, cx))?
.await
.context("processing image")?;
Ok(ToolResultOutput {
content: ToolResultContent::Image(language_model_image),
output: None,
})
});
return task.into();
}
cx.spawn(async move |cx| {
let buffer = cx
.update(|cx| {
@@ -152,7 +112,7 @@ impl Tool for ReadFileTool {
.as_ref()
.map_or(true, |file| !file.disk_state().exists())
})? {
anyhow::bail!("{file_path} not found");
return Err(anyhow!("{} not found", file_path));
}
project.update(cx, |project, cx| {
@@ -322,10 +282,7 @@ mod test {
.output
})
.await;
assert_eq!(
result.unwrap().content.as_str(),
Some("This is a small file content")
);
assert_eq!(result.unwrap().content, "This is a small file content");
}
#[gpui::test]
@@ -365,7 +322,6 @@ mod test {
})
.await;
let content = result.unwrap();
let content = content.as_str().unwrap();
assert_eq!(
content.lines().skip(4).take(6).collect::<Vec<_>>(),
vec![
@@ -409,8 +365,6 @@ mod test {
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(
content
.as_str()
.unwrap()
.lines()
.skip(4)
.take(expected_content.len())
@@ -454,10 +408,7 @@ mod test {
.output
})
.await;
assert_eq!(
result.unwrap().content.as_str(),
Some("Line 2\nLine 3\nLine 4")
);
assert_eq!(result.unwrap().content, "Line 2\nLine 3\nLine 4");
}
#[gpui::test]
@@ -497,7 +448,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content.as_str(), Some("Line 1\nLine 2"));
assert_eq!(result.unwrap().content, "Line 1\nLine 2");
// end_line of 0 should result in at least 1 line
let result = cx
@@ -520,7 +471,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content.as_str(), Some("Line 1"));
assert_eq!(result.unwrap().content, "Line 1");
// when start_line > end_line, should still return at least 1 line
let result = cx
@@ -543,7 +494,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content.as_str(), Some("Line 3"));
assert_eq!(result.unwrap().content, "Line 3");
}
fn init_test(cx: &mut TestAppContext) {

View File

@@ -1,13 +1,12 @@
You are an expert engineer and your task is to write a new file from scratch.
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.
Tool calls have been disabled. You MUST start your response directly with the file's new content.
<file_path>
<file_to_edit>
{{path}}
</file_path>
</file_to_edit>
<edit_description>
{{edit_description}}
</edit_description>
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
@@ -125,24 +125,14 @@ impl Tool for TerminalTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let working_dir = match working_dir(&input, &project, cx) {
let input_path = Path::new(&input.cd);
let working_dir = match working_dir(&input, &project, input_path, cx) {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(err)).into(),
};
let program = self.determine_shell.clone();
let command = if cfg!(windows) {
format!("$null | & {{{}}}", input.command.replace("\"", "'"))
} else if let Some(cwd) = working_dir
.as_ref()
.and_then(|cwd| cwd.as_os_str().to_str())
{
// Make sure once we're *inside* the shell, we cd into `cwd`
format!("(cd {cwd}; {}) </dev/null", input.command)
} else {
format!("({}) </dev/null", input.command)
};
let args = vec!["-c".into(), command];
let command = format!("({}) </dev/null", input.command);
let args = vec!["-c".into(), command.clone()];
let cwd = working_dir.clone();
let env = match &working_dir {
Some(dir) => project.update(cx, |project, cx| {
@@ -325,13 +315,19 @@ fn process_content(
} else {
content
};
let content = content.trim();
let is_empty = content.is_empty();
let content = format!("```\n{content}\n```");
let is_empty = content.trim().is_empty();
let content = format!(
"```\n{}{}```",
content,
if content.ends_with('\n') { "" } else { "\n" }
);
let content = if should_truncate {
format!(
"Command output too long. The first {} bytes:\n\n{content}",
"Command output too long. The first {} bytes:\n\n{}",
content.len(),
content,
)
} else {
content
@@ -371,43 +367,42 @@ fn process_content(
fn working_dir(
input: &TerminalToolInput,
project: &Entity<Project>,
input_path: &Path,
cx: &mut App,
) -> Result<Option<PathBuf>> {
let project = project.read(cx);
let cd = &input.cd;
if cd == "." || cd == "" {
// Accept "." or "" as meaning "the one worktree" if we only have one worktree.
if input.cd == "." {
// Accept "." as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
match worktrees.next() {
Some(worktree) => {
anyhow::ensure!(
worktrees.next().is_none(),
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
);
if worktrees.next().is_some() {
bail!(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
);
}
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
}
None => Ok(None),
}
} else {
let input_path = Path::new(cd);
if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Ok(Some(input_path.into()));
}
} else {
if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
}
} else if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if !project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
bail!("The absolute path must be within one of the project's worktrees");
}
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
Ok(Some(input_path.into()))
} else {
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
bail!("`cd` directory {:?} not found in the project", input.cd);
};
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
}
}
@@ -728,8 +723,8 @@ mod tests {
)
});
let output = result.output.await.log_err().unwrap().content;
assert_eq!(output.as_str().unwrap(), "Command executed successfully.");
let output = result.output.await.log_err().map(|output| output.content);
assert_eq!(output, Some("Command executed successfully.".into()));
}
#[gpui::test]
@@ -762,13 +757,12 @@ mod tests {
cx,
);
cx.spawn(async move |_| {
let output = headless_result.output.await.map(|output| output.content);
assert_eq!(
output
.ok()
.and_then(|content| content.as_str().map(ToString::to_string)),
expected
);
let output = headless_result
.output
.await
.log_err()
.map(|output| output.content);
assert_eq!(output, expected);
})
};
@@ -776,7 +770,7 @@ mod tests {
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
cd: "project".into(),
},
Some(format!(
"```\n{}\n```",
@@ -791,9 +785,12 @@ mod tests {
check(
TerminalToolInput {
command: "pwd".into(),
cd: "other-project".into(),
cd: ".".into(),
},
None, // other-project is a dir, but *not* a worktree (yet)
Some(format!(
"```\n{}\n```",
tree.path().join("project").display()
)),
cx,
)
})

View File

@@ -3,9 +3,7 @@ use std::{sync::Arc, time::Duration};
use crate::schema::json_schema_for;
use crate::ui::ToolCallCardHeader;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{Future, FutureExt, TryFutureExt};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
@@ -75,13 +73,9 @@ impl Tool for WebSearchTool {
let search_task = search_task.clone();
async move {
let response = search_task.await.map_err(|err| anyhow!(err))?;
Ok(ToolResultOutput {
content: ToolResultContent::Text(
serde_json::to_string(&response)
.context("Failed to serialize search results")?,
),
output: Some(serde_json::to_value(response)?),
})
serde_json::to_string(&response)
.context("Failed to serialize search results")
.map(Into::into)
}
});
@@ -90,18 +84,6 @@ impl Tool for WebSearchTool {
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
}
}
fn deserialize_card(
self: Arc<Self>,
output: serde_json::Value,
_project: Entity<Project>,
_window: &mut Window,
cx: &mut App,
) -> Option<assistant_tool::AnyToolCard> {
let output = serde_json::from_value::<WebSearchResponse>(output).ok()?;
let card = cx.new(|cx| WebSearchToolCard::new(Task::ready(Ok(output)), cx));
Some(card.into())
}
}
#[derive(RegisterComponent)]

View File

@@ -1,6 +1,6 @@
use std::{io::Cursor, sync::Arc};
use anyhow::{Context as _, Result};
use anyhow::Result;
use collections::HashMap;
use gpui::{App, AssetSource, Global};
use rodio::{
@@ -44,8 +44,8 @@ impl SoundRegistry {
let bytes = self
.assets
.load(&path)?
.map(anyhow::Ok)
.with_context(|| format!("No asset available for path {path}"))??
.map(Ok)
.unwrap_or_else(|| Err(anyhow::anyhow!("No such asset available")))?
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use client::{Client, TelemetrySettings};
use db::RELEASE_CHANNEL;
use db::kvp::KEY_VALUE_STORE;
@@ -358,7 +358,7 @@ impl AutoUpdater {
cx.default_global::<GlobalAutoUpdate>()
.0
.clone()
.context("auto-update not initialized")
.ok_or_else(|| anyhow!("auto-update not initialized"))
})??;
let release = Self::get_release(
@@ -402,7 +402,7 @@ impl AutoUpdater {
cx.default_global::<GlobalAutoUpdate>()
.0
.clone()
.context("auto-update not initialized")
.ok_or_else(|| anyhow!("auto-update not initialized"))
})??;
let release = Self::get_release(
@@ -456,11 +456,12 @@ impl AutoUpdater {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
anyhow::ensure!(
response.status().is_success(),
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
);
if !response.status().is_success() {
return Err(anyhow!(
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
));
}
serde_json::from_slice(body.as_slice()).with_context(|| {
format!(
@@ -520,10 +521,10 @@ impl AutoUpdater {
let installer_dir = InstallerDir::new().await?;
let filename = match OS {
"macos" => anyhow::Ok("Zed.dmg"),
"macos" => Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
"windows" => Ok("ZedUpdateInstaller.exe"),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
_ => Err(anyhow!("not supported: {:?}", OS)),
}?;
#[cfg(not(target_os = "windows"))]
@@ -544,7 +545,7 @@ impl AutoUpdater {
"macos" => install_release_macos(&installer_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&installer_dir, downloaded_asset, &cx).await,
"windows" => install_release_windows(downloaded_asset).await,
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
_ => Err(anyhow!("not supported: {:?}", OS)),
}?;
this.update(&mut cx, |this, cx| {
@@ -600,11 +601,12 @@ async fn download_remote_server_binary(
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
let mut response = client.get(&release.url, request_body, true).await?;
anyhow::ensure!(
response.status().is_success(),
"failed to download remote server release: {:?}",
response.status()
);
if !response.status().is_success() {
return Err(anyhow!(
"failed to download remote server release: {:?}",
response.status()
));
}
smol::io::copy(response.body_mut(), &mut temp_file).await?;
smol::fs::rename(&temp, &target_path).await?;
@@ -751,7 +753,7 @@ async fn install_release_macos(
let running_app_path = cx.update(|cx| cx.app_path())??;
let running_app_filename = running_app_path
.file_name()
.with_context(|| format!("invalid running app path {running_app_path:?}"))?;
.ok_or_else(|| anyhow!("invalid running app path"))?;
let mount_path = temp_dir.path().join("Zed");
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();

View File

@@ -22,7 +22,7 @@ mod windows_impl {
use super::dialog::create_dialog_window;
use super::updater::perform_update;
use anyhow::{Context as _, Result};
use anyhow::{Context, Result};
use windows::{
Win32::{
Foundation::{HWND, LPARAM, WPARAM},

View File

@@ -4,7 +4,7 @@ use std::{
time::{Duration, Instant},
};
use anyhow::{Context as _, Result};
use anyhow::{Context, Result};
use windows::Win32::{
Foundation::{HWND, LPARAM, WPARAM},
System::Threading::CREATE_NEW_PROCESS_GROUP,
@@ -124,7 +124,9 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()>
for job in JOBS.iter() {
let start = Instant::now();
loop {
anyhow::ensure!(start.elapsed().as_secs() <= 2, "Timed out");
if start.elapsed().as_secs() > 2 {
return Err(anyhow::anyhow!("Timed out"));
}
match (*job)(app_dir) {
Ok(_) => {
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };

View File

@@ -3,7 +3,7 @@ mod models;
use std::collections::HashMap;
use std::pin::Pin;
use anyhow::{Context as _, Error, Result, anyhow};
use anyhow::{Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
pub use aws_sdk_bedrockruntime::types::{
@@ -97,7 +97,7 @@ pub async fn stream_completion(
}
})
.await
.context("spawning a task")?
.map_err(|err| anyhow!("failed to spawn task: {err:?}"))?
}
pub fn aws_document_to_value(document: &Document) -> Value {

View File

@@ -1,3 +1,4 @@
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use strum::EnumIter;
@@ -37,7 +38,6 @@ pub enum Model {
AmazonNovaLite,
AmazonNovaMicro,
AmazonNovaPro,
AmazonNovaPremier,
// AI21 models
AI21J2GrandeInstruct,
AI21J2JumboInstruct,
@@ -72,10 +72,6 @@ pub enum Model {
MistralMixtral8x7BInstructV0,
MistralMistralLarge2402V1,
MistralMistralSmall2402V1,
MistralPixtralLarge2502V1,
// Writer models
PalmyraWriterX5,
PalmyraWriterX4,
#[serde(rename = "custom")]
Custom {
name: String,
@@ -106,7 +102,7 @@ impl Model {
} else if id.starts_with("claude-3-7-sonnet-thinking") {
Ok(Self::Claude3_7SonnetThinking)
} else {
anyhow::bail!("invalid model id {id}");
Err(anyhow!("invalid model id"))
}
}
@@ -124,7 +120,6 @@ impl Model {
Model::AmazonNovaLite => "amazon.nova-lite-v1:0",
Model::AmazonNovaMicro => "amazon.nova-micro-v1:0",
Model::AmazonNovaPro => "amazon.nova-pro-v1:0",
Model::AmazonNovaPremier => "amazon.nova-premier-v1:0",
Model::DeepSeekR1 => "us.deepseek.r1-v1:0",
Model::AI21J2GrandeInstruct => "ai21.j2-grande-instruct",
Model::AI21J2JumboInstruct => "ai21.j2-jumbo-instruct",
@@ -154,9 +149,6 @@ impl Model {
Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
Model::MistralMistralSmall2402V1 => "mistral.mistral-small-2402-v1:0",
Model::MistralPixtralLarge2502V1 => "mistral.pixtral-large-2502-v1:0",
Model::PalmyraWriterX4 => "writer.palmyra-x4-v1:0",
Model::PalmyraWriterX5 => "writer.palmyra-x5-v1:0",
Self::Custom { name, .. } => name,
}
}
@@ -174,7 +166,6 @@ impl Model {
Self::AmazonNovaLite => "Amazon Nova Lite",
Self::AmazonNovaMicro => "Amazon Nova Micro",
Self::AmazonNovaPro => "Amazon Nova Pro",
Self::AmazonNovaPremier => "Amazon Nova Premier",
Self::DeepSeekR1 => "DeepSeek R1",
Self::AI21J2GrandeInstruct => "AI21 Jurassic2 Grande Instruct",
Self::AI21J2JumboInstruct => "AI21 Jurassic2 Jumbo Instruct",
@@ -204,9 +195,6 @@ impl Model {
Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
Self::MistralMistralSmall2402V1 => "Mistral Small 2402 V1",
Self::MistralPixtralLarge2502V1 => "Pixtral Large 25.02 V1",
Self::PalmyraWriterX5 => "Writer Palmyra X5",
Self::PalmyraWriterX4 => "Writer Palmyra X4",
Self::Custom {
display_name, name, ..
} => display_name.as_deref().unwrap_or(name),
@@ -220,11 +208,8 @@ impl Model {
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet => 200_000,
Self::AmazonNovaPremier => 1_000_000,
Self::PalmyraWriterX5 => 1_000_000,
Self::PalmyraWriterX4 => 128_000,
Self::Custom { max_tokens, .. } => *max_tokens,
_ => 128_000,
_ => 200_000,
}
}
@@ -232,7 +217,7 @@ impl Model {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
Self::Claude3_5SonnetV2 => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -267,10 +252,7 @@ impl Model {
| Self::Claude3_5Haiku => true,
// Amazon Nova models (all support tool use)
Self::AmazonNovaPremier
| Self::AmazonNovaPro
| Self::AmazonNovaLite
| Self::AmazonNovaMicro => true,
Self::AmazonNovaPro | Self::AmazonNovaLite | Self::AmazonNovaMicro => true,
// AI21 Jamba 1.5 models support tool use
Self::AI21Jamba15LargeV1 | Self::AI21Jamba15MiniV1 => true,
@@ -293,7 +275,7 @@ impl Model {
}
}
pub fn cross_region_inference_id(&self, region: &str) -> anyhow::Result<String> {
pub fn cross_region_inference_id(&self, region: &str) -> Result<String, anyhow::Error> {
let region_group = if region.starts_with("us-gov-") {
"us-gov"
} else if region.starts_with("us-") {
@@ -306,7 +288,8 @@ impl Model {
// Canada and South America regions - default to US profiles
"us"
} else {
anyhow::bail!("Unsupported Region {region}");
// Unknown region
return Err(anyhow!("Unsupported Region"));
};
let model_id = self.id();
@@ -322,11 +305,8 @@ impl Model {
// Models available only in US
(Model::Claude3Opus, "us")
| (Model::Claude3_5Haiku, "us")
| (Model::Claude3_7Sonnet, "us")
| (Model::Claude3_7SonnetThinking, "us")
| (Model::AmazonNovaPremier, "us")
| (Model::MistralPixtralLarge2502V1, "us") => {
| (Model::Claude3_7SonnetThinking, "us") => {
Ok(format!("{}.{}", region_group, model_id))
}
@@ -360,12 +340,6 @@ impl Model {
Ok(format!("{}.{}", region_group, model_id))
}
// Writer models only available in the US
(Model::PalmyraWriterX4, "us") | (Model::PalmyraWriterX5, "us") => {
// They have some goofiness
Ok(format!("{}.{}", region_group, model_id))
}
// Any other combination is not supported
_ => Ok(self.id().into()),
}

View File

@@ -2,7 +2,7 @@ pub mod participant;
pub mod room;
use crate::call_settings::CallSettings;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use audio::Audio;
use client::{ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, proto};
use collections::HashSet;
@@ -187,7 +187,7 @@ impl ActiveCall {
let invite = if let Some(room) = room {
cx.spawn(async move |_, cx| {
let room = room.await.map_err(|err| anyhow!("{err:?}"))?;
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
@@ -236,7 +236,7 @@ impl ActiveCall {
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_spawn(async move {
room.await.map_err(|err| anyhow!("{err:?}"))?;
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
@@ -326,7 +326,7 @@ impl ActiveCall {
.0
.borrow_mut()
.take()
.context("no incoming call")?;
.ok_or_else(|| anyhow!("no incoming call"))?;
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
@@ -399,9 +399,12 @@ impl ActiveCall {
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Result<()> {
let (room, _) = self.room.as_ref().context("no active call")?;
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakEntity<Project>> {

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result};
use anyhow::{Result, anyhow};
use client::{ParticipantIndex, User, proto};
use collections::HashMap;
use gpui::WeakEntity;
@@ -18,17 +18,17 @@ pub enum ParticipantLocation {
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location
.and_then(|l| l.variant)
.context("participant location was not provided")?
{
proto::participant_location::Variant::SharedProject(project) => {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
proto::participant_location::Variant::External(_) => Ok(Self::External),
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
call_settings::CallSettings,
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use audio::{Audio, Sound};
use client::{
ChannelId, Client, ParticipantIndex, TypedEnvelope, User, UserStore,
@@ -165,7 +165,7 @@ impl Room {
) -> Task<Result<Entity<Self>>> {
cx.spawn(async move |cx| {
let response = client.request(proto::CreateRoom {}).await?;
let room_proto = response.room.context("invalid room")?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new(|cx| {
let mut room = Self::new(
room_proto.id,
@@ -270,7 +270,7 @@ impl Room {
user_store: Entity<UserStore>,
mut cx: AsyncApp,
) -> Result<Entity<Self>> {
let room_proto = response.room.context("invalid room")?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new(|cx| {
Self::new(
room_proto.id,
@@ -360,7 +360,7 @@ impl Room {
log::info!("detected client disconnection");
this.upgrade()
.context("room was dropped")?
.ok_or_else(|| anyhow!("room was dropped"))?
.update(cx, |this, cx| {
this.status = RoomStatus::Rejoining;
cx.notify();
@@ -428,7 +428,9 @@ impl Room {
log::info!("reconnection failed, leaving room");
this.update(cx, |this, cx| this.leave(cx))?.await?;
}
anyhow::bail!("can't reconnect to room: client failed to re-establish connection");
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
}
fn rejoin(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
@@ -492,7 +494,7 @@ impl Room {
let response = response.await?;
let message_id = response.message_id;
let response = response.payload;
let room_proto = response.room.context("invalid room")?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.update(cx, |this, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, cx)?;
@@ -643,7 +645,10 @@ impl Room {
envelope: TypedEnvelope<proto::RoomUpdated>,
mut cx: AsyncApp,
) -> Result<()> {
let room = envelope.payload.room.context("invalid room")?;
let room = envelope
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
}
@@ -932,15 +937,12 @@ impl Room {
} => {
let user_id = participant.identity().0.parse()?;
let track_id = track.sid();
let participant =
self.remote_participants
.get_mut(&user_id)
.with_context(|| {
format!(
"{:?} subscribed to track by unknown participant {user_id}",
self.client.user_id()
)
})?;
let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| {
anyhow!(
"{:?} subscribed to track by unknown participant {user_id}",
self.client.user_id()
)
})?;
if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) {
if publication.is_audio() {
publication.set_enabled(false, cx);
@@ -970,15 +972,12 @@ impl Room {
track, participant, ..
} => {
let user_id = participant.identity().0.parse()?;
let participant =
self.remote_participants
.get_mut(&user_id)
.with_context(|| {
format!(
"{:?}, unsubscribed from track by unknown participant {user_id}",
self.client.user_id()
)
})?;
let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| {
anyhow!(
"{:?}, unsubscribed from track by unknown participant {user_id}",
self.client.user_id()
)
})?;
match track {
livekit_client::RemoteTrack::Audio(track) => {
participant.audio_tracks.remove(&track.sid());
@@ -1325,7 +1324,7 @@ impl Room {
let live_kit = this
.live_kit
.as_mut()
.context("live-kit was not initialized")?;
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
let canceled = if let LocalTrack::Pending {
publish_id: cur_publish_id,
@@ -1390,7 +1389,7 @@ impl Room {
cx.spawn(async move |this, cx| {
let sources = sources.await??;
let source = sources.first().context("no display found")?;
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
let publication = participant.publish_screenshare_track(&**source, cx).await;
@@ -1398,7 +1397,7 @@ impl Room {
let live_kit = this
.live_kit
.as_mut()
.context("live-kit was not initialized")?;
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
let canceled = if let LocalTrack::Pending {
publish_id: cur_publish_id,
@@ -1486,14 +1485,16 @@ impl Room {
}
pub fn unshare_screen(&mut self, cx: &mut Context<Self>) -> Result<()> {
anyhow::ensure!(!self.status.is_offline(), "room is offline");
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
let live_kit = self
.live_kit
.as_mut()
.context("live-kit was not initialized")?;
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
match mem::take(&mut live_kit.screen_track) {
LocalTrack::None => anyhow::bail!("screen was not shared"),
LocalTrack::None => Err(anyhow!("screen was not shared")),
LocalTrack::Pending { .. } => {
cx.notify();
Ok(())

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