diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml
index e132eca1e52bc617f35fc2ec6e4e34fe3c796b11..1bf6c80e4073dafa90e736f995053c570f0ba2da 100644
--- a/.github/ISSUE_TEMPLATE/10_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml
@@ -14,7 +14,7 @@ body:
### Description
diff --git a/Cargo.lock b/Cargo.lock
index 5ceb015927208d8dda15d4fe10b8f92968a45a26..9735ada538c930289cd5b47e8cf2332881a1bb14 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -191,9 +191,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
-version = "0.0.31"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
+checksum = "6b91e5ec3ce05e8effb2a7a3b7b1a587daa6699b9f98bbde6a35e44b8c6c773a"
dependencies = [
"anyhow",
"async-broadcast",
@@ -292,23 +292,21 @@ dependencies = [
"anyhow",
"client",
"collections",
- "context_server",
"env_logger 0.11.8",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"indoc",
- "itertools 0.14.0",
"language",
"language_model",
"language_models",
"libc",
"log",
"nix 0.29.0",
+ "node_runtime",
"paths",
"project",
- "rand 0.8.5",
"reqwest_client",
"schemars",
"semver",
@@ -316,12 +314,10 @@ dependencies = [
"serde_json",
"settings",
"smol",
- "strum 0.27.1",
"tempfile",
"thiserror 2.0.12",
"ui",
"util",
- "uuid",
"watch",
"which 6.0.3",
"workspace-hack",
@@ -9213,6 +9209,7 @@ dependencies = [
"language",
"lsp",
"project",
+ "proto",
"release_channel",
"serde_json",
"settings",
@@ -17185,8 +17182,7 @@ dependencies = [
[[package]]
name = "tree-sitter-cpp"
version = "0.23.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
+source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609"
dependencies = [
"cc",
"tree-sitter-language",
@@ -20396,7 +20392,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.202.0"
+version = "0.203.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -20590,7 +20586,7 @@ dependencies = [
[[package]]
name = "zed_html"
-version = "0.2.1"
+version = "0.2.2"
dependencies = [
"zed_extension_api 0.1.0",
]
diff --git a/Cargo.toml b/Cargo.toml
index 6ec243a9b9de4d2ab322e0466e804fa542a1ed35..974796a5e5ff4a3093fc8b492628e1c6d33a616a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -426,7 +426,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
-agent-client-protocol = "0.0.31"
+agent-client-protocol = "0.1"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -624,7 +624,7 @@ tower-http = "0.4.4"
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
-tree-sitter-cpp = "0.23"
+tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
diff --git a/assets/icons/list_filter.svg b/assets/icons/list_filter.svg
new file mode 100644
index 0000000000000000000000000000000000000000..82f41f5f6832a8cb35e2703e0f8ce36d148454dd
--- /dev/null
+++ b/assets/icons/list_filter.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/terminal_ghost.svg b/assets/icons/terminal_ghost.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7d0d0e068e8a6f01837e860e8223690a95541769
--- /dev/null
+++ b/assets/icons/terminal_ghost.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8ebff8e1bc87b17e536c7f97dfa2118130233258
--- /dev/null
+++ b/assets/images/acp_grid.svg
@@ -0,0 +1,1257 @@
+
diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..efaa46707be0a893917c3fc072a14b9c7b6b0c9b
--- /dev/null
+++ b/assets/images/acp_logo.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6bc359cf82dde8060a66c051c8727f0e0624b938
--- /dev/null
+++ b/assets/images/acp_logo_serif.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index e84f4834af58ffcac596a73264260d7d1f922d89..2610f9b7051cbce74ce6df13d49699c74e870395 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -40,7 +40,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
- "ctrl-shift-i": "edit_prediction::ToggleMenu",
+ "ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
@@ -120,7 +120,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
- "ctrl-shift-e": "editor::ToggleEditPrediction",
+ "ctrl-alt-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
@@ -130,8 +130,8 @@
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
- "ctrl-enter": "editor::NewlineAbove",
- "ctrl-shift-enter": "editor::NewlineBelow",
+ "ctrl-enter": "editor::NewlineBelow",
+ "ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
new file mode 100644
index 0000000000000000000000000000000000000000..dbd377409f4423dd12bac06b651efd079772dbb5
--- /dev/null
+++ b/assets/keymaps/default-windows.json
@@ -0,0 +1,1260 @@
+[
+ // Standard Windows bindings
+ {
+ "use_key_equivalents": true,
+ "bindings": {
+ "home": "menu::SelectFirst",
+ "shift-pageup": "menu::SelectFirst",
+ "pageup": "menu::SelectFirst",
+ "end": "menu::SelectLast",
+ "shift-pagedown": "menu::SelectLast",
+ "pagedown": "menu::SelectLast",
+ "ctrl-n": "menu::SelectNext",
+ "tab": "menu::SelectNext",
+ "down": "menu::SelectNext",
+ "ctrl-p": "menu::SelectPrevious",
+ "shift-tab": "menu::SelectPrevious",
+ "up": "menu::SelectPrevious",
+ "enter": "menu::Confirm",
+ "ctrl-enter": "menu::SecondaryConfirm",
+ "ctrl-escape": "menu::Cancel",
+ "ctrl-c": "menu::Cancel",
+ "escape": "menu::Cancel",
+ "shift-alt-enter": "menu::Restart",
+ "alt-enter": ["picker::ConfirmInput", { "secondary": false }],
+ "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
+ "ctrl-shift-w": "workspace::CloseWindow",
+ "shift-escape": "workspace::ToggleZoom",
+ "open": "workspace::Open",
+ "ctrl-o": "workspace::Open",
+ "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
+ "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
+ "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
+ "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
+ "ctrl-,": "zed::OpenSettings",
+ "ctrl-q": "zed::Quit",
+ "f4": "debugger::Start",
+ "shift-f5": "debugger::Stop",
+ "ctrl-shift-f5": "debugger::RerunSession",
+ "f6": "debugger::Pause",
+ "f7": "debugger::StepOver",
+ "ctrl-f11": "debugger::StepInto",
+ "shift-f11": "debugger::StepOut",
+ "f11": "zed::ToggleFullScreen",
+ "ctrl-shift-i": "edit_prediction::ToggleMenu",
+ "shift-alt-l": "lsp_tool::ToggleMenu"
+ }
+ },
+ {
+ "context": "Picker || menu",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "menu::SelectPrevious",
+ "down": "menu::SelectNext"
+ }
+ },
+ {
+ "context": "Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "editor::Cancel",
+ "shift-backspace": "editor::Backspace",
+ "backspace": "editor::Backspace",
+ "delete": "editor::Delete",
+ "tab": "editor::Tab",
+ "shift-tab": "editor::Backtab",
+ "ctrl-k": "editor::CutToEndOfLine",
+ "ctrl-k ctrl-q": "editor::Rewrap",
+ "ctrl-k q": "editor::Rewrap",
+ "ctrl-backspace": "editor::DeleteToPreviousWordStart",
+ "ctrl-delete": "editor::DeleteToNextWordEnd",
+ "cut": "editor::Cut",
+ "shift-delete": "editor::Cut",
+ "ctrl-x": "editor::Cut",
+ "copy": "editor::Copy",
+ "ctrl-insert": "editor::Copy",
+ "ctrl-c": "editor::Copy",
+ "paste": "editor::Paste",
+ "shift-insert": "editor::Paste",
+ "ctrl-v": "editor::Paste",
+ "undo": "editor::Undo",
+ "ctrl-z": "editor::Undo",
+ "redo": "editor::Redo",
+ "ctrl-y": "editor::Redo",
+ "ctrl-shift-z": "editor::Redo",
+ "up": "editor::MoveUp",
+ "ctrl-up": "editor::LineUp",
+ "ctrl-down": "editor::LineDown",
+ "pageup": "editor::MovePageUp",
+ "alt-pageup": "editor::PageUp",
+ "shift-pageup": "editor::SelectPageUp",
+ "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
+ "down": "editor::MoveDown",
+ "pagedown": "editor::MovePageDown",
+ "alt-pagedown": "editor::PageDown",
+ "shift-pagedown": "editor::SelectPageDown",
+ "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
+ "left": "editor::MoveLeft",
+ "right": "editor::MoveRight",
+ "ctrl-left": "editor::MoveToPreviousWordStart",
+ "ctrl-right": "editor::MoveToNextWordEnd",
+ "ctrl-home": "editor::MoveToBeginning",
+ "ctrl-end": "editor::MoveToEnd",
+ "shift-up": "editor::SelectUp",
+ "shift-down": "editor::SelectDown",
+ "shift-left": "editor::SelectLeft",
+ "shift-right": "editor::SelectRight",
+ "ctrl-shift-left": "editor::SelectToPreviousWordStart",
+ "ctrl-shift-right": "editor::SelectToNextWordEnd",
+ "ctrl-shift-home": "editor::SelectToBeginning",
+ "ctrl-shift-end": "editor::SelectToEnd",
+ "ctrl-a": "editor::SelectAll",
+ "ctrl-l": "editor::SelectLine",
+ "shift-alt-f": "editor::Format",
+ "shift-alt-o": "editor::OrganizeImports",
+ "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
+ "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
+ "ctrl-alt-space": "editor::ShowCharacterPalette",
+ "ctrl-;": "editor::ToggleLineNumbers",
+ "ctrl-'": "editor::ToggleSelectedDiffHunks",
+ "ctrl-\"": "editor::ExpandAllDiffHunks",
+ "ctrl-i": "editor::ShowSignatureHelp",
+ "alt-g b": "git::Blame",
+ "alt-g m": "git::OpenModifiedFiles",
+ "menu": "editor::OpenContextMenu",
+ "shift-f10": "editor::OpenContextMenu",
+ "ctrl-shift-e": "editor::ToggleEditPrediction",
+ "f9": "editor::ToggleBreakpoint",
+ "shift-f9": "editor::EditLogBreakpoint"
+ }
+ },
+ {
+ "context": "Editor && mode == full",
+ "use_key_equivalents": true,
+ "bindings": {
+ "shift-enter": "editor::Newline",
+ "enter": "editor::Newline",
+ "ctrl-enter": "editor::NewlineBelow",
+ "ctrl-shift-enter": "editor::NewlineAbove",
+ "ctrl-k ctrl-z": "editor::ToggleSoftWrap",
+ "ctrl-k z": "editor::ToggleSoftWrap",
+ "find": "buffer_search::Deploy",
+ "ctrl-f": "buffer_search::Deploy",
+ "ctrl-h": "buffer_search::DeployReplace",
+ "ctrl-shift-.": "assistant::QuoteSelection",
+ "ctrl-shift-,": "assistant::InsertIntoEditor",
+ "shift-alt-e": "editor::SelectEnclosingSymbol",
+ "ctrl-shift-backspace": "editor::GoToPreviousChange",
+ "ctrl-shift-alt-backspace": "editor::GoToNextChange",
+ "alt-enter": "editor::OpenSelectionsInMultibuffer"
+ }
+ },
+ {
+ "context": "Editor && mode == full && edit_prediction",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-]": "editor::NextEditPrediction",
+ "alt-[": "editor::PreviousEditPrediction"
+ }
+ },
+ {
+ "context": "Editor && !edit_prediction",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-\\": "editor::ShowEditPrediction"
+ }
+ },
+ {
+ "context": "Editor && mode == auto_height",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "editor::Newline",
+ "shift-enter": "editor::Newline",
+ "ctrl-shift-enter": "editor::NewlineBelow"
+ }
+ },
+ {
+ "context": "Markdown",
+ "use_key_equivalents": true,
+ "bindings": {
+ "copy": "markdown::Copy",
+ "ctrl-c": "markdown::Copy"
+ }
+ },
+ {
+ "context": "Editor && jupyter && !ContextEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-enter": "repl::Run",
+ "ctrl-alt-enter": "repl::RunInPlace"
+ }
+ },
+ {
+ "context": "Editor && !agent_diff",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k ctrl-r": "git::Restore",
+ "alt-y": "git::StageAndNext",
+ "shift-alt-y": "git::UnstageAndNext"
+ }
+ },
+ {
+ "context": "Editor && editor_agent_diff",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-y": "agent::Keep",
+ "ctrl-n": "agent::Reject",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll",
+ "ctrl-shift-r": "agent::OpenAgentDiff"
+ }
+ },
+ {
+ "context": "AgentDiff",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-y": "agent::Keep",
+ "ctrl-n": "agent::Reject",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
+ }
+ },
+ {
+ "context": "ContextEditor > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "assistant::Assist",
+ "ctrl-s": "workspace::Save",
+ "save": "workspace::Save",
+ "ctrl-shift-,": "assistant::InsertIntoEditor",
+ "shift-enter": "assistant::Split",
+ "ctrl-r": "assistant::CycleMessageRole",
+ "enter": "assistant::ConfirmCommand",
+ "alt-enter": "editor::Newline",
+ "ctrl-k c": "assistant::CopyCode",
+ "ctrl-g": "search::SelectNextMatch",
+ "ctrl-shift-g": "search::SelectPreviousMatch",
+ "ctrl-k l": "agent::OpenRulesLibrary"
+ }
+ },
+ {
+ "context": "AgentPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-n": "agent::NewThread",
+ "shift-alt-n": "agent::NewTextThread",
+ "ctrl-shift-h": "agent::OpenHistory",
+ "shift-alt-c": "agent::OpenSettings",
+ "shift-alt-p": "agent::OpenRulesLibrary",
+ "ctrl-i": "agent::ToggleProfileSelector",
+ "shift-alt-/": "agent::ToggleModelSelector",
+ "ctrl-shift-a": "agent::ToggleContextPicker",
+ "ctrl-shift-j": "agent::ToggleNavigationMenu",
+ "ctrl-shift-i": "agent::ToggleOptionsMenu",
+ // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
+ "shift-alt-escape": "agent::ExpandMessageEditor",
+ "ctrl-shift-.": "assistant::QuoteSelection",
+ "shift-alt-e": "agent::RemoveAllContext",
+ "ctrl-shift-e": "project_panel::ToggleFocus",
+ "ctrl-shift-enter": "agent::ContinueThread",
+ "super-ctrl-b": "agent::ToggleBurnMode",
+ "alt-enter": "agent::ContinueWithBurnMode"
+ }
+ },
+ {
+ "context": "AgentPanel > NavigationMenu",
+ "use_key_equivalents": true,
+ "bindings": {
+ "shift-backspace": "agent::DeleteRecentlyOpenThread"
+ }
+ },
+ {
+ "context": "AgentPanel > Markdown",
+ "use_key_equivalents": true,
+ "bindings": {
+ "copy": "markdown::CopyAsMarkdown",
+ "ctrl-c": "markdown::CopyAsMarkdown"
+ }
+ },
+ {
+ "context": "AgentPanel && prompt_editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-n": "agent::NewTextThread",
+ "ctrl-alt-t": "agent::NewThread"
+ }
+ },
+ {
+ "context": "AgentPanel && external_agent_thread",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-n": "agent::NewExternalAgentThread",
+ "ctrl-alt-t": "agent::NewThread"
+ }
+ },
+ {
+ "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "agent::Chat",
+ "ctrl-enter": "agent::ChatWithFollow",
+ "ctrl-i": "agent::ToggleProfileSelector",
+ "ctrl-shift-r": "agent::OpenAgentDiff",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
+ }
+ },
+ {
+ "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "agent::Chat",
+ "enter": "editor::Newline",
+ "ctrl-i": "agent::ToggleProfileSelector",
+ "ctrl-shift-r": "agent::OpenAgentDiff",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
+ }
+ },
+ {
+ "context": "EditMessageEditor > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "menu::Confirm",
+ "alt-enter": "editor::Newline"
+ }
+ },
+ {
+ "context": "AgentFeedbackMessageEditor > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "menu::Confirm",
+ "alt-enter": "editor::Newline"
+ }
+ },
+ {
+ "context": "ContextStrip",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "agent::FocusUp",
+ "right": "agent::FocusRight",
+ "left": "agent::FocusLeft",
+ "down": "agent::FocusDown",
+ "backspace": "agent::RemoveFocusedContext",
+ "enter": "agent::AcceptSuggestedContext"
+ }
+ },
+ {
+ "context": "AcpThread > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "agent::Chat",
+ "ctrl-shift-r": "agent::OpenAgentDiff",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
+ }
+ },
+ {
+ "context": "ThreadHistory",
+ "use_key_equivalents": true,
+ "bindings": {
+ "backspace": "agent::RemoveSelectedThread"
+ }
+ },
+ {
+ "context": "PromptLibrary",
+ "use_key_equivalents": true,
+ "bindings": {
+ "new": "rules_library::NewRule",
+ "ctrl-n": "rules_library::NewRule",
+ "ctrl-shift-s": "rules_library::ToggleDefaultRule"
+ }
+ },
+ {
+ "context": "BufferSearchBar",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "buffer_search::Dismiss",
+ "tab": "buffer_search::FocusEditor",
+ "enter": "search::SelectNextMatch",
+ "shift-enter": "search::SelectPreviousMatch",
+ "alt-enter": "search::SelectAllMatches",
+ "find": "search::FocusSearch",
+ "ctrl-f": "search::FocusSearch",
+ "ctrl-h": "search::ToggleReplace",
+ "ctrl-l": "search::ToggleSelection"
+ }
+ },
+ {
+ "context": "BufferSearchBar && in_replace > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "search::ReplaceNext",
+ "ctrl-enter": "search::ReplaceAll"
+ }
+ },
+ {
+ "context": "BufferSearchBar && !in_replace > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "search::PreviousHistoryQuery",
+ "down": "search::NextHistoryQuery"
+ }
+ },
+ {
+ "context": "ProjectSearchBar",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "project_search::ToggleFocus",
+ "shift-find": "search::FocusSearch",
+ "ctrl-shift-f": "search::FocusSearch",
+ "ctrl-shift-h": "search::ToggleReplace",
+ "alt-r": "search::ToggleRegex" // vscode
+ }
+ },
+ {
+ "context": "ProjectSearchBar > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "search::PreviousHistoryQuery",
+ "down": "search::NextHistoryQuery"
+ }
+ },
+ {
+ "context": "ProjectSearchBar && in_replace > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "search::ReplaceNext",
+ "ctrl-alt-enter": "search::ReplaceAll"
+ }
+ },
+ {
+ "context": "ProjectSearchView",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "project_search::ToggleFocus",
+ "ctrl-shift-h": "search::ToggleReplace",
+ "alt-r": "search::ToggleRegex" // vscode
+ }
+ },
+ {
+ "context": "Pane",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-1": ["pane::ActivateItem", 0],
+ "alt-2": ["pane::ActivateItem", 1],
+ "alt-3": ["pane::ActivateItem", 2],
+ "alt-4": ["pane::ActivateItem", 3],
+ "alt-5": ["pane::ActivateItem", 4],
+ "alt-6": ["pane::ActivateItem", 5],
+ "alt-7": ["pane::ActivateItem", 6],
+ "alt-8": ["pane::ActivateItem", 7],
+ "alt-9": ["pane::ActivateItem", 8],
+ "alt-0": "pane::ActivateLastItem",
+ "ctrl-pageup": "pane::ActivatePreviousItem",
+ "ctrl-pagedown": "pane::ActivateNextItem",
+ "ctrl-shift-pageup": "pane::SwapItemLeft",
+ "ctrl-shift-pagedown": "pane::SwapItemRight",
+ "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }],
+ "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }],
+ "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }],
+ "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes",
+ "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
+ "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
+ "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
+ "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
+ "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
+ "back": "pane::GoBack",
+ "alt--": "pane::GoBack",
+ "alt-=": "pane::GoForward",
+ "forward": "pane::GoForward",
+ "f3": "search::SelectNextMatch",
+ "shift-f3": "search::SelectPreviousMatch",
+ "shift-find": "project_search::ToggleFocus",
+ "ctrl-shift-f": "project_search::ToggleFocus",
+ "shift-alt-h": "search::ToggleReplace",
+ "alt-l": "search::ToggleSelection",
+ "alt-enter": "search::SelectAllMatches",
+ "alt-c": "search::ToggleCaseSensitive",
+ "alt-w": "search::ToggleWholeWord",
+ "alt-find": "project_search::ToggleFilters",
+ "alt-f": "project_search::ToggleFilters",
+ "alt-r": "search::ToggleRegex",
+ // "ctrl-shift-alt-x": "search::ToggleRegex",
+ "ctrl-k shift-enter": "pane::TogglePinTab"
+ }
+ },
+ // Bindings from VS Code
+ {
+ "context": "Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-[": "editor::Outdent",
+ "ctrl-]": "editor::Indent",
+ "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
+ "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
+ "ctrl-shift-k": "editor::DeleteLine",
+ "alt-up": "editor::MoveLineUp",
+ "alt-down": "editor::MoveLineDown",
+ "shift-alt-up": "editor::DuplicateLineUp",
+ "shift-alt-down": "editor::DuplicateLineDown",
+ "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection
+ "shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
+ "ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
+ "ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
+ "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
+ "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch
+ "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
+ "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
+ "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
+ "ctrl-k ctrl-i": "editor::Hover",
+ "ctrl-k ctrl-b": "editor::BlameHover",
+ "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
+ "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
+ "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
+ "f2": "editor::Rename",
+ "f12": "editor::GoToDefinition",
+ "alt-f12": "editor::GoToDefinitionSplit",
+ "ctrl-shift-f10": "editor::GoToDefinitionSplit",
+ "ctrl-f12": "editor::GoToImplementation",
+ "shift-f12": "editor::GoToTypeDefinition",
+ "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
+ "shift-alt-f12": "editor::FindAllReferences",
+ "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
+ "ctrl-shift-\\": "editor::MoveToEnclosingBracket",
+ "ctrl-shift-[": "editor::Fold",
+ "ctrl-shift-]": "editor::UnfoldLines",
+ "ctrl-k ctrl-l": "editor::ToggleFold",
+ "ctrl-k ctrl-[": "editor::FoldRecursive",
+ "ctrl-k ctrl-]": "editor::UnfoldRecursive",
+ "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
+ "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
+ "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
+ "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
+ "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
+ "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
+ "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
+ "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
+ "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
+ "ctrl-k ctrl-0": "editor::FoldAll",
+ "ctrl-k ctrl-j": "editor::UnfoldAll",
+ "ctrl-space": "editor::ShowCompletions",
+ "ctrl-shift-space": "editor::ShowWordCompletions",
+ "ctrl-.": "editor::ToggleCodeActions",
+ "ctrl-k r": "editor::RevealInFileManager",
+ "ctrl-k p": "editor::CopyPath",
+ "ctrl-\\": "pane::SplitRight",
+ "ctrl-shift-alt-c": "editor::DisplayCursorNames",
+ "alt-.": "editor::GoToHunk",
+ "alt-,": "editor::GoToPreviousHunk"
+ }
+ },
+ {
+ "context": "Editor && extension == md",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k v": "markdown::OpenPreviewToTheSide",
+ "ctrl-shift-v": "markdown::OpenPreview"
+ }
+ },
+ {
+ "context": "Editor && extension == svg",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k v": "svg::OpenPreviewToTheSide",
+ "ctrl-shift-v": "svg::OpenPreview"
+ }
+ },
+ {
+ "context": "Editor && mode == full",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-o": "outline::Toggle",
+ "ctrl-g": "go_to_line::Toggle"
+ }
+ },
+ {
+ "context": "Workspace",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-open": ["projects::OpenRecent", { "create_new_window": false }],
+ // Change the default action on `menu::Confirm` by setting the parameter
+ // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
+ "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
+ "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
+ // Change to open path modal for existing remote connection by setting the parameter
+ // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
+ "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
+ "shift-alt-b": "branches::OpenRecent",
+ "shift-alt-enter": "toast::RunAction",
+ "ctrl-shift-`": "workspace::NewTerminal",
+ "save": "workspace::Save",
+ "ctrl-s": "workspace::Save",
+ "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
+ "shift-save": "workspace::SaveAs",
+ "ctrl-shift-s": "workspace::SaveAs",
+ "new": "workspace::NewFile",
+ "ctrl-n": "workspace::NewFile",
+ "shift-new": "workspace::NewWindow",
+ "ctrl-shift-n": "workspace::NewWindow",
+ "ctrl-`": "terminal_panel::ToggleFocus",
+ "f10": ["app_menu::OpenApplicationMenu", "Zed"],
+ "alt-1": ["workspace::ActivatePane", 0],
+ "alt-2": ["workspace::ActivatePane", 1],
+ "alt-3": ["workspace::ActivatePane", 2],
+ "alt-4": ["workspace::ActivatePane", 3],
+ "alt-5": ["workspace::ActivatePane", 4],
+ "alt-6": ["workspace::ActivatePane", 5],
+ "alt-7": ["workspace::ActivatePane", 6],
+ "alt-8": ["workspace::ActivatePane", 7],
+ "alt-9": ["workspace::ActivatePane", 8],
+ "ctrl-alt-b": "workspace::ToggleRightDock",
+ "ctrl-b": "workspace::ToggleLeftDock",
+ "ctrl-j": "workspace::ToggleBottomDock",
+ "ctrl-shift-y": "workspace::CloseAllDocks",
+ "alt-r": "workspace::ResetActiveDockSize",
+ // For 0px parameter, uses UI font size value.
+ "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
+ "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
+ "shift-alt-0": "workspace::ResetOpenDocksSize",
+ "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
+ "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
+ "shift-find": "pane::DeploySearch",
+ "ctrl-shift-f": "pane::DeploySearch",
+ "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
+ "ctrl-shift-t": "pane::ReopenClosedItem",
+ "ctrl-k ctrl-s": "zed::OpenKeymapEditor",
+ "ctrl-k ctrl-t": "theme_selector::Toggle",
+ "ctrl-alt-super-p": "settings_profile_selector::Toggle",
+ "ctrl-t": "project_symbols::Toggle",
+ "ctrl-p": "file_finder::Toggle",
+ "ctrl-tab": "tab_switcher::Toggle",
+ "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
+ "ctrl-e": "file_finder::Toggle",
+ "f1": "command_palette::Toggle",
+ "ctrl-shift-p": "command_palette::Toggle",
+ "ctrl-shift-m": "diagnostics::Deploy",
+ "ctrl-shift-e": "project_panel::ToggleFocus",
+ "ctrl-shift-b": "outline_panel::ToggleFocus",
+ "ctrl-shift-g": "git_panel::ToggleFocus",
+ "ctrl-shift-d": "debug_panel::ToggleFocus",
+ "ctrl-shift-/": "agent::ToggleFocus",
+ "alt-save": "workspace::SaveAll",
+ "ctrl-k s": "workspace::SaveAll",
+ "ctrl-k m": "language_selector::Toggle",
+ "escape": "workspace::Unfollow",
+ "ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
+ "ctrl-k ctrl-right": "workspace::ActivatePaneRight",
+ "ctrl-k ctrl-up": "workspace::ActivatePaneUp",
+ "ctrl-k ctrl-down": "workspace::ActivatePaneDown",
+ "ctrl-k shift-left": "workspace::SwapPaneLeft",
+ "ctrl-k shift-right": "workspace::SwapPaneRight",
+ "ctrl-k shift-up": "workspace::SwapPaneUp",
+ "ctrl-k shift-down": "workspace::SwapPaneDown",
+ "ctrl-shift-x": "zed::Extensions",
+ "ctrl-shift-r": "task::Rerun",
+ "alt-t": "task::Rerun",
+ "shift-alt-t": "task::Spawn",
+ "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }],
+ // also possible to spawn tasks by name:
+ // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
+ // or by tag:
+ // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
+ "f5": "debugger::Rerun",
+ "ctrl-f4": "workspace::CloseActiveDock",
+ "ctrl-w": "workspace::CloseActiveDock"
+ }
+ },
+ {
+ "context": "Workspace && debugger_running",
+ "use_key_equivalents": true,
+ "bindings": {
+ "f5": "zed::NoAction"
+ }
+ },
+ {
+ "context": "Workspace && debugger_stopped",
+ "use_key_equivalents": true,
+ "bindings": {
+ "f5": "debugger::Continue"
+ }
+ },
+ {
+ "context": "ApplicationMenu",
+ "use_key_equivalents": true,
+ "bindings": {
+ "f10": "menu::Cancel",
+ "left": "app_menu::ActivateMenuLeft",
+ "right": "app_menu::ActivateMenuRight"
+ }
+ },
+ // Bindings from Sublime Text
+ {
+ "context": "Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-u": "editor::UndoSelection",
+ "ctrl-shift-u": "editor::RedoSelection",
+ "ctrl-shift-j": "editor::JoinLines",
+ "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
+ "shift-alt-h": "editor::DeleteToPreviousSubwordStart",
+ "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
+ "shift-alt-d": "editor::DeleteToNextSubwordEnd",
+ "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
+ "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
+ "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart",
+ "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd"
+ }
+ },
+ // Bindings from Atom
+ {
+ "context": "Pane",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k up": "pane::SplitUp",
+ "ctrl-k down": "pane::SplitDown",
+ "ctrl-k left": "pane::SplitLeft",
+ "ctrl-k right": "pane::SplitRight"
+ }
+ },
+ // Bindings that should be unified with bindings for more general actions
+ {
+ "context": "Editor && renaming",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "editor::ConfirmRename"
+ }
+ },
+ {
+ "context": "Editor && showing_completions",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "editor::ConfirmCompletion",
+ "shift-enter": "editor::ConfirmCompletionReplace",
+ "tab": "editor::ComposeCompletion"
+ }
+ },
+ // Bindings for accepting edit predictions
+ //
+ // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
+ // because alt-tab may not be available, as it is often used for window switching.
+ {
+ "context": "Editor && edit_prediction",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-tab": "editor::AcceptEditPrediction",
+ "alt-l": "editor::AcceptEditPrediction",
+ "tab": "editor::AcceptEditPrediction",
+ "alt-right": "editor::AcceptPartialEditPrediction"
+ }
+ },
+ {
+ "context": "Editor && edit_prediction_conflict",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-tab": "editor::AcceptEditPrediction",
+ "alt-l": "editor::AcceptEditPrediction",
+ "alt-right": "editor::AcceptPartialEditPrediction"
+ }
+ },
+ {
+ "context": "Editor && showing_code_actions",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "editor::ConfirmCodeAction"
+ }
+ },
+ {
+ "context": "Editor && (showing_code_actions || showing_completions)",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-p": "editor::ContextMenuPrevious",
+ "up": "editor::ContextMenuPrevious",
+ "ctrl-n": "editor::ContextMenuNext",
+ "down": "editor::ContextMenuNext",
+ "pageup": "editor::ContextMenuFirst",
+ "pagedown": "editor::ContextMenuLast"
+ }
+ },
+ {
+ "context": "Editor && showing_signature_help && !showing_completions",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "editor::SignatureHelpPrevious",
+ "down": "editor::SignatureHelpNext"
+ }
+ },
+ // Custom bindings
+ {
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-alt-f": "workspace::FollowNextCollaborator",
+ // Only available in debug builds: opens an element inspector for development.
+ "shift-alt-i": "dev::ToggleInspector"
+ }
+ },
+ {
+ "context": "!Terminal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-c": "collab_panel::ToggleFocus"
+ }
+ },
+ {
+ "context": "!ContextEditor > Editor && mode == full",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-enter": "editor::OpenExcerpts",
+ "shift-enter": "editor::ExpandExcerpts",
+ "ctrl-alt-enter": "editor::OpenExcerptsSplit",
+ "ctrl-shift-e": "pane::RevealInProjectPanel",
+ "ctrl-f8": "editor::GoToHunk",
+ "ctrl-shift-f8": "editor::GoToPreviousHunk",
+ "ctrl-enter": "assistant::InlineAssist",
+ "ctrl-shift-;": "editor::ToggleInlayHints"
+ }
+ },
+ {
+ "context": "PromptEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-[": "agent::CyclePreviousInlineAssist",
+ "ctrl-]": "agent::CycleNextInlineAssist",
+ "shift-alt-e": "agent::RemoveAllContext"
+ }
+ },
+ {
+ "context": "Prompt",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "menu::SelectPrevious",
+ "right": "menu::SelectNext",
+ "h": "menu::SelectPrevious",
+ "l": "menu::SelectNext"
+ }
+ },
+ {
+ "context": "ProjectSearchBar && !in_replace",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "project_search::SearchInNew"
+ }
+ },
+ {
+ "context": "OutlinePanel && not_editing",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "outline_panel::CollapseSelectedEntry",
+ "right": "outline_panel::ExpandSelectedEntry",
+ "alt-copy": "outline_panel::CopyPath",
+ "shift-alt-c": "outline_panel::CopyPath",
+ "shift-alt-copy": "workspace::CopyRelativePath",
+ "ctrl-shift-alt-c": "workspace::CopyRelativePath",
+ "ctrl-alt-r": "outline_panel::RevealInFileManager",
+ "space": "outline_panel::OpenSelectedEntry",
+ "shift-down": "menu::SelectNext",
+ "shift-up": "menu::SelectPrevious",
+ "alt-enter": "editor::OpenExcerpts",
+ "ctrl-alt-enter": "editor::OpenExcerptsSplit"
+ }
+ },
+ {
+ "context": "ProjectPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "project_panel::CollapseSelectedEntry",
+ "right": "project_panel::ExpandSelectedEntry",
+ "new": "project_panel::NewFile",
+ "ctrl-n": "project_panel::NewFile",
+ "alt-new": "project_panel::NewDirectory",
+ "alt-n": "project_panel::NewDirectory",
+ "cut": "project_panel::Cut",
+ "ctrl-x": "project_panel::Cut",
+ "copy": "project_panel::Copy",
+ "ctrl-insert": "project_panel::Copy",
+ "ctrl-c": "project_panel::Copy",
+ "paste": "project_panel::Paste",
+ "shift-insert": "project_panel::Paste",
+ "ctrl-v": "project_panel::Paste",
+ "alt-copy": "project_panel::CopyPath",
+ "shift-alt-c": "project_panel::CopyPath",
+ "shift-alt-copy": "workspace::CopyRelativePath",
+ "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
+ "enter": "project_panel::Rename",
+ "f2": "project_panel::Rename",
+ "backspace": ["project_panel::Trash", { "skip_prompt": false }],
+ "delete": ["project_panel::Trash", { "skip_prompt": false }],
+ "shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
+ "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
+ "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
+ "ctrl-alt-r": "project_panel::RevealInFileManager",
+ "ctrl-shift-enter": "project_panel::OpenWithSystem",
+ "alt-d": "project_panel::CompareMarkedFiles",
+ "shift-find": "project_panel::NewSearchInDirectory",
+ "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
+ "shift-down": "menu::SelectNext",
+ "shift-up": "menu::SelectPrevious",
+ "escape": "menu::Cancel"
+ }
+ },
+ {
+ "context": "ProjectPanel && not_editing",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "project_panel::Open"
+ }
+ },
+ {
+ "context": "GitPanel && ChangesList",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "menu::SelectPrevious",
+ "down": "menu::SelectNext",
+ "enter": "menu::Confirm",
+ "alt-y": "git::StageFile",
+ "shift-alt-y": "git::UnstageFile",
+ "space": "git::ToggleStaged",
+ "shift-space": "git::StageRange",
+ "tab": "git_panel::FocusEditor",
+ "shift-tab": "git_panel::FocusEditor",
+ "escape": "git_panel::ToggleFocus",
+ "alt-enter": "menu::SecondaryConfirm",
+ "delete": ["git::RestoreFile", { "skip_prompt": false }],
+ "backspace": ["git::RestoreFile", { "skip_prompt": false }],
+ "shift-delete": ["git::RestoreFile", { "skip_prompt": false }],
+ "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
+ "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
+ }
+ },
+ {
+ "context": "GitPanel && CommitEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "git::Cancel"
+ }
+ },
+ {
+ "context": "GitCommit > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "editor::Newline",
+ "ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
+ "alt-l": "git::GenerateCommitMessage"
+ }
+ },
+ {
+ "context": "GitPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-g ctrl-g": "git::Fetch",
+ "ctrl-g up": "git::Push",
+ "ctrl-g down": "git::Pull",
+ "ctrl-g shift-up": "git::ForcePush",
+ "ctrl-g d": "git::Diff",
+ "ctrl-g backspace": "git::RestoreTrackedFiles",
+ "ctrl-g shift-backspace": "git::TrashUntrackedFiles",
+ "ctrl-space": "git::StageAll",
+ "ctrl-shift-space": "git::UnstageAll",
+ "ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend"
+ }
+ },
+ {
+ "context": "GitDiff > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
+ "ctrl-space": "git::StageAll",
+ "ctrl-shift-space": "git::UnstageAll"
+ }
+ },
+ {
+ "context": "AskPass > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "menu::Confirm"
+ }
+ },
+ {
+ "context": "CommitEditor > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "git_panel::FocusChanges",
+ "tab": "git_panel::FocusChanges",
+ "shift-tab": "git_panel::FocusChanges",
+ "enter": "editor::Newline",
+ "ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
+ "alt-up": "git_panel::FocusChanges",
+ "alt-l": "git::GenerateCommitMessage"
+ }
+ },
+ {
+ "context": "DebugPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-t": "debugger::ToggleThreadPicker",
+ "ctrl-i": "debugger::ToggleSessionPicker",
+ "shift-alt-escape": "debugger::ToggleExpandItem"
+ }
+ },
+ {
+ "context": "VariableList",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "variable_list::CollapseSelectedEntry",
+ "right": "variable_list::ExpandSelectedEntry",
+ "enter": "variable_list::EditVariable",
+ "ctrl-c": "variable_list::CopyVariableValue",
+ "ctrl-alt-c": "variable_list::CopyVariableName",
+ "delete": "variable_list::RemoveWatch",
+ "backspace": "variable_list::RemoveWatch",
+ "alt-enter": "variable_list::AddWatch"
+ }
+ },
+ {
+ "context": "BreakpointList",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "debugger::ToggleEnableBreakpoint",
+ "backspace": "debugger::UnsetBreakpoint",
+ "left": "debugger::PreviousBreakpointProperty",
+ "right": "debugger::NextBreakpointProperty"
+ }
+ },
+ {
+ "context": "CollabPanel && not_editing",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-backspace": "collab_panel::Remove",
+ "space": "menu::Confirm"
+ }
+ },
+ {
+ "context": "CollabPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-up": "collab_panel::MoveChannelUp",
+ "alt-down": "collab_panel::MoveChannelDown"
+ }
+ },
+ {
+ "context": "(CollabPanel && editing) > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "collab_panel::InsertSpace"
+ }
+ },
+ {
+ "context": "ChannelModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "tab": "channel_modal::ToggleMode"
+ }
+ },
+ {
+ "context": "Picker > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "up": "menu::SelectPrevious",
+ "down": "menu::SelectNext",
+ "tab": "picker::ConfirmCompletion",
+ "alt-enter": ["picker::ConfirmInput", { "secondary": false }]
+ }
+ },
+ {
+ "context": "ChannelModal > Picker > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "tab": "channel_modal::ToggleMode"
+ }
+ },
+ {
+ "context": "FileFinder || (FileFinder > Picker > Editor)",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-p": "file_finder::Toggle",
+ "ctrl-shift-a": "file_finder::ToggleSplitMenu",
+ "ctrl-shift-i": "file_finder::ToggleFilterMenu"
+ }
+ },
+ {
+ "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-p": "file_finder::SelectPrevious",
+ "ctrl-j": "pane::SplitDown",
+ "ctrl-k": "pane::SplitUp",
+ "ctrl-h": "pane::SplitLeft",
+ "ctrl-l": "pane::SplitRight"
+ }
+ },
+ {
+ "context": "TabSwitcher",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-tab": "menu::SelectPrevious",
+ "ctrl-up": "menu::SelectPrevious",
+ "ctrl-down": "menu::SelectNext",
+ "ctrl-backspace": "tab_switcher::CloseSelectedItem"
+ }
+ },
+ {
+ "context": "Terminal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-alt-space": "terminal::ShowCharacterPalette",
+ "copy": "terminal::Copy",
+ "ctrl-insert": "terminal::Copy",
+ "ctrl-shift-c": "terminal::Copy",
+ "paste": "terminal::Paste",
+ "shift-insert": "terminal::Paste",
+ "ctrl-shift-v": "terminal::Paste",
+ "ctrl-enter": "assistant::InlineAssist",
+ "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"],
+ "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
+ "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
+ "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
+ "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
+ "ctrl-shift-a": "editor::SelectAll",
+ "find": "buffer_search::Deploy",
+ "ctrl-shift-f": "buffer_search::Deploy",
+ "ctrl-shift-l": "terminal::Clear",
+ "ctrl-shift-w": "pane::CloseActiveItem",
+ "up": ["terminal::SendKeystroke", "up"],
+ "pageup": ["terminal::SendKeystroke", "pageup"],
+ "down": ["terminal::SendKeystroke", "down"],
+ "pagedown": ["terminal::SendKeystroke", "pagedown"],
+ "escape": ["terminal::SendKeystroke", "escape"],
+ "enter": ["terminal::SendKeystroke", "enter"],
+ "shift-pageup": "terminal::ScrollPageUp",
+ "shift-pagedown": "terminal::ScrollPageDown",
+ "shift-up": "terminal::ScrollLineUp",
+ "shift-down": "terminal::ScrollLineDown",
+ "shift-home": "terminal::ScrollToTop",
+ "shift-end": "terminal::ScrollToBottom",
+ "ctrl-shift-space": "terminal::ToggleViMode",
+ "ctrl-shift-r": "terminal::RerunTask",
+ "ctrl-alt-r": "terminal::RerunTask",
+ "alt-t": "terminal::RerunTask"
+ }
+ },
+ {
+ "context": "ZedPredictModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel"
+ }
+ },
+ {
+ "context": "ConfigureContextServerModal > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "editor::Newline",
+ "ctrl-enter": "menu::Confirm"
+ }
+ },
+ {
+ "context": "OnboardingAiConfigurationModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel"
+ }
+ },
+ {
+ "context": "Diagnostics",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
+ }
+ },
+ {
+ "context": "DebugConsole > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "menu::Confirm",
+ "alt-enter": "console::WatchExpression"
+ }
+ },
+ {
+ "context": "RunModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-tab": "pane::ActivateNextItem",
+ "ctrl-shift-tab": "pane::ActivatePreviousItem"
+ }
+ },
+ {
+ "context": "MarkdownPreview",
+ "use_key_equivalents": true,
+ "bindings": {
+ "pageup": "markdown::MovePageUp",
+ "pagedown": "markdown::MovePageDown"
+ }
+ },
+ {
+ "context": "KeymapEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-f": "search::FocusSearch",
+ "alt-find": "keymap_editor::ToggleKeystrokeSearch",
+ "alt-f": "keymap_editor::ToggleKeystrokeSearch",
+ "alt-c": "keymap_editor::ToggleConflictFilter",
+ "enter": "keymap_editor::EditBinding",
+ "alt-enter": "keymap_editor::CreateBinding",
+ "ctrl-c": "keymap_editor::CopyAction",
+ "ctrl-shift-c": "keymap_editor::CopyContext",
+ "ctrl-t": "keymap_editor::ShowMatchingKeybinds"
+ }
+ },
+ {
+ "context": "KeystrokeInput",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "keystroke_input::StartRecording",
+ "escape escape escape": "keystroke_input::StopRecording",
+ "delete": "keystroke_input::ClearKeystrokes"
+ }
+ },
+ {
+ "context": "KeybindEditorModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "menu::Confirm",
+ "escape": "menu::Cancel"
+ }
+ },
+ {
+ "context": "KeybindEditorModal > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "menu::SelectPrevious",
+ "down": "menu::SelectNext"
+ }
+ },
+ {
+ "context": "Onboarding",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-1": "onboarding::ActivateBasicsPage",
+ "ctrl-2": "onboarding::ActivateEditingPage",
+ "ctrl-3": "onboarding::ActivateAISetupPage",
+ "ctrl-escape": "onboarding::Finish",
+ "alt-tab": "onboarding::SignIn",
+ "shift-alt-a": "onboarding::OpenAccount"
+ }
+ }
+]
diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json
index 0ff3796f03d85affdae88d009e88e73516ba385a..62910e297bb18f52917477806ceea1b79dcb5d86 100755
--- a/assets/keymaps/linux/emacs.json
+++ b/assets/keymaps/linux/emacs.json
@@ -38,6 +38,7 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
+ "alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json
index 0ff3796f03d85affdae88d009e88e73516ba385a..62910e297bb18f52917477806ceea1b79dcb5d86 100755
--- a/assets/keymaps/macos/emacs.json
+++ b/assets/keymaps/macos/emacs.json
@@ -38,6 +38,7 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
+ "alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index 62e50b3c8c21a39f6185e7e1c293c949ce6c6af8..0a88baee027a3ae4d72409f5f142ceda3f4d9717 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -428,12 +428,14 @@
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
+ "g .": "vim::HelixGotoLastModification", // go to last modification
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom",
- "x": "editor::SelectLine",
+ "shift-r": "editor::Paste",
+ "x": "vim::HelixSelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
diff --git a/assets/settings/default.json b/assets/settings/default.json
index f0b9e11e57f074ac3c50b4e830343f7a5290f965..57a5d13eab281d6bffec2f299fbb1e2d5a3a01c5 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -363,6 +363,8 @@
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
+ // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
+ "use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
@@ -653,6 +655,8 @@
// "never"
"show": "always"
},
+ // Whether to enable drag-and-drop operations in the project panel.
+ "drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
},
@@ -1581,7 +1585,7 @@
"ensure_final_newline_on_save": false
},
"Elixir": {
- "language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
+ "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
},
"Elm": {
"tab_size": 4
@@ -1606,7 +1610,7 @@
}
},
"HEEX": {
- "language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
+ "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
},
"HTML": {
"prettier": {
diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json
index a79c550671f85d7b107db5e85883caa28fe41411..5cead67b6d5bb89e878e3bfb8d250dcbbd2ce447 100644
--- a/assets/settings/initial_tasks.json
+++ b/assets/settings/initial_tasks.json
@@ -43,8 +43,8 @@
// "args": ["--login"]
// }
// }
- "shell": "system",
+ "shell": "system"
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
- "tags": []
+ // "tags": []
}
]
diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs
index 4ded647a746f18e030529e4c14a00c1ffd2335e3..04ff032ad40c600c80fed7cff9f48139b2307931 100644
--- a/crates/acp_thread/src/acp_thread.rs
+++ b/crates/acp_thread/src/acp_thread.rs
@@ -789,16 +789,12 @@ pub enum ThreadStatus {
#[derive(Debug, Clone)]
pub enum LoadError {
- NotInstalled {
- error_message: SharedString,
- install_message: SharedString,
- install_command: String,
- },
Unsupported {
- error_message: SharedString,
- upgrade_message: SharedString,
- upgrade_command: String,
+ command: SharedString,
+ current_version: SharedString,
+ minimum_version: SharedString,
},
+ FailedToInstall(SharedString),
Exited {
status: ExitStatus,
},
@@ -808,12 +804,19 @@ pub enum LoadError {
impl Display for LoadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
- LoadError::NotInstalled { error_message, .. }
- | LoadError::Unsupported { error_message, .. } => {
- write!(f, "{error_message}")
+ LoadError::Unsupported {
+ command: path,
+ current_version,
+ minimum_version,
+ } => {
+ write!(
+ f,
+ "version {current_version} from {path} is not supported (need at least {minimum_version})"
+ )
}
+ LoadError::FailedToInstall(msg) => write!(f, "Failed to install: {msg}"),
LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
- LoadError::Other(msg) => write!(f, "{}", msg),
+ LoadError::Other(msg) => write!(f, "{msg}"),
}
}
}
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index 899e360ab01226cdb6b47b37713f99e45c0ef6b7..7b70fde56ab1e7acb6705aeace82f142dc28a9f3 100644
--- a/crates/agent/src/thread.rs
+++ b/crates/agent/src/thread.rs
@@ -664,7 +664,7 @@ impl Thread {
}
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option {
- if self.configured_model.is_none() || self.messages.is_empty() {
+ if self.configured_model.is_none() {
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
}
self.configured_model.clone()
@@ -2097,7 +2097,7 @@ impl Thread {
}
pub fn summarize(&mut self, cx: &mut Context) {
- let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
+ let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
println!("No thread summary model");
return;
};
@@ -2416,7 +2416,7 @@ impl Thread {
}
let Some(ConfiguredModel { model, provider }) =
- LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
+ LanguageModelRegistry::read_global(cx).thread_summary_model()
else {
return;
};
@@ -5410,10 +5410,13 @@ fn main() {{
}),
cx,
);
- registry.set_thread_summary_model(Some(ConfiguredModel {
- provider,
- model: model.clone(),
- }));
+ registry.set_thread_summary_model(
+ Some(ConfiguredModel {
+ provider,
+ model: model.clone(),
+ }),
+ cx,
+ );
})
});
diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs
index ecfaea4b4967a019da83edb3416cef66464c229e..ea80df8fb52cffab80c8c64307b75de7f0954a56 100644
--- a/crates/agent2/src/agent.rs
+++ b/crates/agent2/src/agent.rs
@@ -61,16 +61,19 @@ pub struct LanguageModels {
model_list: acp_thread::AgentModelList,
refresh_models_rx: watch::Receiver<()>,
refresh_models_tx: watch::Sender<()>,
+ _authenticate_all_providers_task: Task<()>,
}
impl LanguageModels {
- fn new(cx: &App) -> Self {
+ fn new(cx: &mut App) -> Self {
let (refresh_models_tx, refresh_models_rx) = watch::channel(());
+
let mut this = Self {
models: HashMap::default(),
model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),
refresh_models_rx,
refresh_models_tx,
+ _authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx),
};
this.refresh_list(cx);
this
@@ -90,7 +93,7 @@ impl LanguageModels {
let mut recommended = Vec::new();
for provider in &providers {
for model in provider.recommended_models(cx) {
- recommended_models.insert(model.id());
+ recommended_models.insert((model.provider_id(), model.id()));
recommended.push(Self::map_language_model_to_info(&model, provider));
}
}
@@ -107,7 +110,7 @@ impl LanguageModels {
for model in provider.provided_models(cx) {
let model_info = Self::map_language_model_to_info(&model, &provider);
let model_id = model_info.id.clone();
- if !recommended_models.contains(&model.id()) {
+ if !recommended_models.contains(&(model.provider_id(), model.id())) {
provider_models.push(model_info);
}
models.insert(model_id, model);
@@ -150,6 +153,52 @@ impl LanguageModels {
fn model_id(model: &Arc) -> acp_thread::AgentModelId {
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
}
+
+ fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
+ let authenticate_all_providers = LanguageModelRegistry::global(cx)
+ .read(cx)
+ .providers()
+ .iter()
+ .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
+ .collect::>();
+
+ cx.background_spawn(async move {
+ for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
+ if let Err(err) = authenticate_task.await {
+ if matches!(err, language_model::AuthenticateError::CredentialsNotFound) {
+ // Since we're authenticating these providers in the
+ // background for the purposes of populating the
+ // language selector, we don't care about providers
+ // where the credentials are not found.
+ } else {
+ // Some providers have noisy failure states that we
+ // don't want to spam the logs with every time the
+ // language model selector is initialized.
+ //
+ // Ideally these should have more clear failure modes
+ // that we know are safe to ignore here, like what we do
+ // with `CredentialsNotFound` above.
+ match provider_id.0.as_ref() {
+ "lmstudio" | "ollama" => {
+ // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
+ //
+ // These fail noisily, so we don't log them.
+ }
+ "copilot_chat" => {
+ // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
+ }
+ _ => {
+ log::error!(
+ "Failed to authenticate provider: {}: {err}",
+ provider_name.0
+ );
+ }
+ }
+ }
+ }
+ }
+ })
+ }
}
pub struct NativeAgent {
@@ -228,7 +277,7 @@ impl NativeAgent {
) -> Entity {
let connection = Rc::new(NativeAgentConnection(cx.entity()));
let registry = LanguageModelRegistry::read_global(cx);
- let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
+ let summarization_model = registry.thread_summary_model().map(|c| c.model);
thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx);
@@ -524,7 +573,7 @@ impl NativeAgent {
let registry = LanguageModelRegistry::read_global(cx);
let default_model = registry.default_model().map(|m| m.model);
- let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
+ let summarization_model = registry.thread_summary_model().map(|m| m.model);
for session in self.sessions.values_mut() {
session.thread.update(cx, |thread, cx| {
diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs
index 9ff98ccd18dec4d9a17a1a7161cd3622dacf0d3f..030d2cce746970bd9c8a0c7f0f5e1516eb68fcaf 100644
--- a/crates/agent2/src/native_agent_server.rs
+++ b/crates/agent2/src/native_agent_server.rs
@@ -1,10 +1,9 @@
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
-use agent_servers::AgentServer;
+use agent_servers::{AgentServer, AgentServerDelegate};
use anyhow::Result;
use fs::Fs;
use gpui::{App, Entity, SharedString, Task};
-use project::Project;
use prompt_store::PromptStore;
use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
@@ -30,14 +29,6 @@ impl AgentServer for NativeAgentServer {
"Zed Agent".into()
}
- fn empty_state_headline(&self) -> SharedString {
- self.name()
- }
-
- fn empty_state_message(&self) -> SharedString {
- "".into()
- }
-
fn logo(&self) -> ui::IconName {
ui::IconName::ZedAgent
}
@@ -45,14 +36,14 @@ impl AgentServer for NativeAgentServer {
fn connect(
&self,
_root_dir: &Path,
- project: &Entity,
+ delegate: AgentServerDelegate,
cx: &mut App,
) -> Task>> {
log::debug!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
);
- let project = project.clone();
+ let project = delegate.project().clone();
let fs = self.fs.clone();
let history = self.history.clone();
let prompt_store = PromptStore::global(cx);
diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs
index 093b8ba971ae5509478122f93180c124bd16eab5..fbeee46a484a71742dd4ce52b537bebb5da91924 100644
--- a/crates/agent2/src/tests/mod.rs
+++ b/crates/agent2/src/tests/mod.rs
@@ -72,6 +72,7 @@ async fn test_echo(cx: &mut TestAppContext) {
}
#[gpui::test]
+#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_thinking(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -471,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
tool_name: ToolRequiringPermission::name().into(),
is_error: true,
content: "Permission to run tool denied by user".into(),
- output: None
+ output: Some("Permission to run tool denied by user".into())
})
]
);
@@ -1821,11 +1822,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
- Project::init_settings(cx);
- agent_settings::init(cx);
language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx);
+ Project::init_settings(cx);
LanguageModelRegistry::test(cx);
+ agent_settings::init(cx);
});
cx.executor().forbid_parking();
diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs
index 1b1c014b7930091271b541e9d7cd12281274ec14..97ea1caf1d766be0314a16cc0f518ad701564569 100644
--- a/crates/agent2/src/thread.rs
+++ b/crates/agent2/src/thread.rs
@@ -732,7 +732,17 @@ impl Thread {
stream.update_tool_call_fields(
&tool_use.id,
acp::ToolCallUpdateFields {
- status: Some(acp::ToolCallStatus::Completed),
+ status: Some(
+ tool_result
+ .as_ref()
+ .map_or(acp::ToolCallStatus::Failed, |result| {
+ if result.is_error {
+ acp::ToolCallStatus::Failed
+ } else {
+ acp::ToolCallStatus::Completed
+ }
+ }),
+ ),
raw_output: output,
..Default::default()
},
@@ -1557,7 +1567,7 @@ impl Thread {
tool_name: tool_use.name,
is_error: true,
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
- output: None,
+ output: Some(error.to_string().into()),
},
}
}))
@@ -2459,6 +2469,30 @@ impl ToolCallEventStreamReceiver {
}
}
+ pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
+ let event = self.0.next().await;
+ if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
+ update,
+ )))) = event
+ {
+ update.fields
+ } else {
+ panic!("Expected update fields but got: {:?}", event);
+ }
+ }
+
+ pub async fn expect_diff(&mut self) -> Entity {
+ let event = self.0.next().await;
+ if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
+ update,
+ )))) = event
+ {
+ update.diff
+ } else {
+ panic!("Expected diff but got: {:?}", event);
+ }
+ }
+
pub async fn expect_terminal(&mut self) -> Entity {
let event = self.0.next().await;
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(
diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs
index 5a68d0c70a04aea7367b8264c34d139d3602cc44..f86bfd25f74556118c827050837ec5beda37d471 100644
--- a/crates/agent2/src/tools/edit_file_tool.rs
+++ b/crates/agent2/src/tools/edit_file_tool.rs
@@ -273,6 +273,13 @@ impl AgentTool for EditFileTool {
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone());
+ let _finalize_diff = util::defer({
+ let diff = diff.downgrade();
+ let mut cx = cx.clone();
+ move || {
+ diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
+ }
+ });
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
@@ -389,8 +396,6 @@ impl AgentTool for EditFileTool {
})
.await;
- diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
-
let input_path = input.path.display();
if unified_diff.is_empty() {
anyhow::ensure!(
@@ -1545,6 +1550,100 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_diff_finalization(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = project::FakeFs::new(cx.executor());
+ fs.insert_tree("/", json!({"main.rs": ""})).await;
+
+ let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
+ let languages = project.read_with(cx, |project, _cx| project.languages().clone());
+ let context_server_registry =
+ cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+ let model = Arc::new(FakeLanguageModel::default());
+ let thread = cx.new(|cx| {
+ Thread::new(
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
+ context_server_registry.clone(),
+ Templates::new(),
+ Some(model.clone()),
+ cx,
+ )
+ });
+
+ // Ensure the diff is finalized after the edit completes.
+ {
+ let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
+ let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+ let edit = cx.update(|cx| {
+ tool.run(
+ EditFileToolInput {
+ display_description: "Edit file".into(),
+ path: path!("/main.rs").into(),
+ mode: EditFileMode::Edit,
+ },
+ stream_tx,
+ cx,
+ )
+ });
+ stream_rx.expect_update_fields().await;
+ let diff = stream_rx.expect_diff().await;
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
+ cx.run_until_parked();
+ model.end_last_completion_stream();
+ edit.await.unwrap();
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
+ }
+
+ // Ensure the diff is finalized if an error occurs while editing.
+ {
+ model.forbid_requests();
+ let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
+ let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+ let edit = cx.update(|cx| {
+ tool.run(
+ EditFileToolInput {
+ display_description: "Edit file".into(),
+ path: path!("/main.rs").into(),
+ mode: EditFileMode::Edit,
+ },
+ stream_tx,
+ cx,
+ )
+ });
+ stream_rx.expect_update_fields().await;
+ let diff = stream_rx.expect_diff().await;
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
+ edit.await.unwrap_err();
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
+ model.allow_requests();
+ }
+
+ // Ensure the diff is finalized if the tool call gets dropped.
+ {
+ let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
+ let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+ let edit = cx.update(|cx| {
+ tool.run(
+ EditFileToolInput {
+ display_description: "Edit file".into(),
+ path: path!("/main.rs").into(),
+ mode: EditFileMode::Edit,
+ },
+ stream_tx,
+ cx,
+ )
+ });
+ stream_rx.expect_update_fields().await;
+ let diff = stream_rx.expect_diff().await;
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
+ drop(edit);
+ cx.run_until_parked();
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
+ }
+ }
+
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs
index f41b909d0b286b80bb3c9e8e8c18d0d03f3e05c7..2270a7c32f076bee774c7c8177c4276985adc0b6 100644
--- a/crates/agent2/src/tools/terminal_tool.rs
+++ b/crates/agent2/src/tools/terminal_tool.rs
@@ -2,7 +2,7 @@ use agent_client_protocol as acp;
use anyhow::Result;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, AppContext, Entity, SharedString, Task};
-use project::{Project, terminals::TerminalKind};
+use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
@@ -144,14 +144,14 @@ impl AgentTool for TerminalTool {
let terminal = self
.project
.update(cx, |project, cx| {
- project.create_terminal(
- TerminalKind::Task(task::SpawnInTerminal {
+ project.create_terminal_task(
+ task::SpawnInTerminal {
command: Some(program),
args,
cwd: working_dir.clone(),
env,
..Default::default()
- }),
+ },
cx,
)
})?
diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml
index 9f90f3a78aed825c372cc8bffc67d194b7ec2027..222feb9aaa31a6ace1e13ba8943f416942e8918c 100644
--- a/crates/agent_servers/Cargo.toml
+++ b/crates/agent_servers/Cargo.toml
@@ -6,7 +6,7 @@ publish.workspace = true
license = "GPL-3.0-or-later"
[features]
-test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
+test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
e2e = []
[lints]
@@ -25,21 +25,19 @@ agent_settings.workspace = true
anyhow.workspace = true
client = { workspace = true, optional = true }
collections.workspace = true
-context_server.workspace = true
env_logger = { workspace = true, optional = true }
-fs = { workspace = true, optional = true }
+fs.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
indoc.workspace = true
-itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
+node_runtime.workspace = true
paths.workspace = true
project.workspace = true
-rand.workspace = true
reqwest_client = { workspace = true, optional = true }
schemars.workspace = true
semver.workspace = true
@@ -47,12 +45,10 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
-strum.workspace = true
tempfile.workspace = true
thiserror.workspace = true
ui.workspace = true
util.workspace = true
-uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs
index 9080fc1ab07d91e13708ccf0348d5df01d49e3c0..d929d1fc501fb2093f47f8bdeb4d3695b7b87ebf 100644
--- a/crates/agent_servers/src/acp.rs
+++ b/crates/agent_servers/src/acp.rs
@@ -3,6 +3,7 @@ use acp_thread::AgentConnection;
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
+use agent_settings::AgentSettings;
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
@@ -10,6 +11,7 @@ use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
+use settings::Settings as _;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use thiserror::Error;
@@ -30,6 +32,8 @@ pub struct AcpConnection {
auth_methods: Vec,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task>,
+ _wait_task: Task>,
+ _stderr_task: Task>,
}
pub struct AcpSession {
@@ -56,7 +60,7 @@ impl AcpConnection {
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result {
- let mut child = util::command::new_smol_command(&command.path)
+ let mut child = util::command::new_smol_command(command.path)
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
@@ -86,7 +90,7 @@ impl AcpConnection {
let io_task = cx.background_spawn(io_task);
- cx.background_spawn(async move {
+ let stderr_task = cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
@@ -95,10 +99,10 @@ impl AcpConnection {
log::warn!("agent stderr: {}", &line);
line.clear();
}
- })
- .detach();
+ Ok(())
+ });
- cx.spawn({
+ let wait_task = cx.spawn({
let sessions = sessions.clone();
async move |cx| {
let status = child.status().await?;
@@ -114,8 +118,7 @@ impl AcpConnection {
anyhow::Ok(())
}
- })
- .detach();
+ });
let connection = Rc::new(connection);
@@ -148,8 +151,14 @@ impl AcpConnection {
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_io_task: io_task,
+ _wait_task: wait_task,
+ _stderr_task: stderr_task,
})
}
+
+ pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
+ &self.prompt_capabilities
+ }
}
impl AgentConnection for AcpConnection {
@@ -162,12 +171,34 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
+ let context_server_store = project.read(cx).context_server_store().read(cx);
+ let mcp_servers = context_server_store
+ .configured_server_ids()
+ .iter()
+ .filter_map(|id| {
+ let configuration = context_server_store.configuration_for_server(id)?;
+ let command = configuration.command();
+ Some(acp::McpServer {
+ name: id.0.to_string(),
+ command: command.path.clone(),
+ args: command.args.clone(),
+ env: if let Some(env) = command.env.as_ref() {
+ env.iter()
+ .map(|(name, value)| acp::EnvVariable {
+ name: name.clone(),
+ value: value.clone(),
+ })
+ .collect()
+ } else {
+ vec![]
+ },
+ })
+ })
+ .collect();
+
cx.spawn(async move |cx| {
let response = conn
- .new_session(acp::NewSessionRequest {
- mcp_servers: vec![],
- cwd,
- })
+ .new_session(acp::NewSessionRequest { mcp_servers, cwd })
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
@@ -313,6 +344,28 @@ impl acp::Client for ClientDelegate {
arguments: acp::RequestPermissionRequest,
) -> Result {
let cx = &mut self.cx.clone();
+
+ // If always_allow_tool_actions is enabled, then auto-choose the first "Allow" button
+ if AgentSettings::try_read_global(cx, |settings| settings.always_allow_tool_actions)
+ .unwrap_or(false)
+ {
+ // Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions,
+ // some tools would (incorrectly) continue to auto-accept.
+ if let Some(allow_once_option) = arguments.options.iter().find_map(|option| {
+ if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) {
+ Some(option.id.clone())
+ } else {
+ None
+ }
+ }) {
+ return Ok(acp::RequestPermissionResponse {
+ outcome: acp::RequestPermissionOutcome::Selected {
+ option_id: allow_once_option,
+ },
+ });
+ }
+ }
+
let rx = self
.sessions
.borrow()
diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs
index 7c7e124ca71b684cdda7a24e02c82d1b6117a0cc..e1b4057b71b0b4aee84548df74935d9b0598f598 100644
--- a/crates/agent_servers/src/agent_servers.rs
+++ b/crates/agent_servers/src/agent_servers.rs
@@ -13,12 +13,19 @@ pub use gemini::*;
pub use settings::*;
use acp_thread::AgentConnection;
+use acp_thread::LoadError;
use anyhow::Result;
+use anyhow::anyhow;
+use anyhow::bail;
use collections::HashMap;
+use gpui::AppContext as _;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
+use node_runtime::VersionStrategy;
use project::Project;
use schemars::JsonSchema;
+use semver::Version;
use serde::{Deserialize, Serialize};
+use std::str::FromStr as _;
use std::{
any::Any,
path::{Path, PathBuf},
@@ -31,17 +38,108 @@ pub fn init(cx: &mut App) {
settings::init(cx);
}
+pub struct AgentServerDelegate {
+ project: Entity,
+ status_tx: watch::Sender,
+}
+
+impl AgentServerDelegate {
+ pub fn new(project: Entity, status_tx: watch::Sender) -> Self {
+ Self { project, status_tx }
+ }
+
+ pub fn project(&self) -> &Entity {
+ &self.project
+ }
+
+ fn get_or_npm_install_builtin_agent(
+ self,
+ binary_name: SharedString,
+ package_name: SharedString,
+ entrypoint_path: PathBuf,
+ ignore_system_version: bool,
+ minimum_version: Option,
+ cx: &mut App,
+ ) -> Task> {
+ let project = self.project;
+ let fs = project.read(cx).fs().clone();
+ let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
+ return Task::ready(Err(anyhow!("Missing node runtime")));
+ };
+ let mut status_tx = self.status_tx;
+
+ cx.spawn(async move |cx| {
+ if !ignore_system_version {
+ if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
+ return Ok(AgentServerCommand { path: bin, args: Vec::new(), env: Default::default() })
+ }
+ }
+
+ cx.background_spawn(async move {
+ let node_path = node_runtime.binary_path().await?;
+ let dir = paths::data_dir().join("external_agents").join(binary_name.as_str());
+ fs.create_dir(&dir).await?;
+ let local_executable_path = dir.join(entrypoint_path);
+ let command = AgentServerCommand {
+ path: node_path,
+ args: vec![local_executable_path.to_string_lossy().to_string()],
+ env: Default::default(),
+ };
+
+ let installed_version = node_runtime
+ .npm_package_installed_version(&dir, &package_name)
+ .await?
+ .filter(|version| {
+ Version::from_str(&version)
+ .is_ok_and(|version| Some(version) >= minimum_version)
+ });
+
+ status_tx.send("Checking for latest version…".into())?;
+ let latest_version = match node_runtime.npm_package_latest_version(&package_name).await
+ {
+ Ok(latest_version) => latest_version,
+ Err(e) => {
+ if let Some(installed_version) = installed_version {
+ log::error!("{e}");
+ log::warn!("failed to fetch latest version of {package_name}, falling back to cached version {installed_version}");
+ return Ok(command);
+ } else {
+ bail!(e);
+ }
+ }
+ };
+
+ let should_install = node_runtime
+ .should_install_npm_package(
+ &package_name,
+ &local_executable_path,
+ &dir,
+ VersionStrategy::Latest(&latest_version),
+ )
+ .await;
+
+ if should_install {
+ status_tx.send("Installing latest version…".into())?;
+ node_runtime
+ .npm_install_packages(&dir, &[(&package_name, &latest_version)])
+ .await?;
+ }
+
+ Ok(command)
+ }).await.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
+ })
+ }
+}
+
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
- fn empty_state_headline(&self) -> SharedString;
- fn empty_state_message(&self) -> SharedString;
fn telemetry_id(&self) -> &'static str;
fn connect(
&self,
root_dir: &Path,
- project: &Entity,
+ delegate: AgentServerDelegate,
cx: &mut App,
) -> Task>>;
@@ -79,15 +177,6 @@ impl std::fmt::Debug for AgentServerCommand {
}
}
-pub enum AgentServerVersion {
- Supported,
- Unsupported {
- error_message: SharedString,
- upgrade_message: SharedString,
- upgrade_command: String,
- },
-}
-
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct AgentServerCommand {
#[serde(rename = "command")]
@@ -102,23 +191,16 @@ impl AgentServerCommand {
path_bin_name: &'static str,
extra_args: &[&'static str],
fallback_path: Option<&Path>,
- settings: Option,
+ settings: Option,
project: &Entity,
cx: &mut AsyncApp,
) -> Option {
- if let Some(agent_settings) = settings {
- Some(Self {
- path: agent_settings.command.path,
- args: agent_settings
- .command
- .args
- .into_iter()
- .chain(extra_args.iter().map(|arg| arg.to_string()))
- .collect(),
- env: agent_settings.command.env,
- })
+ if let Some(settings) = settings
+ && let Some(command) = settings.custom_command()
+ {
+ Some(command)
} else {
- match find_bin_in_path(path_bin_name, project, cx).await {
+ match find_bin_in_path(path_bin_name.into(), project, cx).await {
Some(path) => Some(Self {
path,
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
@@ -141,7 +223,7 @@ impl AgentServerCommand {
}
async fn find_bin_in_path(
- bin_name: &'static str,
+ bin_name: SharedString,
project: &Entity,
cx: &mut AsyncApp,
) -> Option {
@@ -171,11 +253,11 @@ async fn find_bin_in_path(
cx.background_executor()
.spawn(async move {
let which_result = if cfg!(windows) {
- which::which(bin_name)
+ which::which(bin_name.as_str())
} else {
let env = env_task.await.unwrap_or_default();
let shell_path = env.get("PATH").cloned();
- which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
+ which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
};
if let Err(which::Error::CannotFindBinaryPath) = which_result {
diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs
index 250e564526d5360be7a84f5cfd9511e5f73a2c1f..db8853695ec798a8b146666292cd29f2c1fc145c 100644
--- a/crates/agent_servers/src/claude.rs
+++ b/crates/agent_servers/src/claude.rs
@@ -1,47 +1,23 @@
-mod edit_tool;
-mod mcp_server;
-mod permission_tool;
-mod read_tool;
-pub mod tools;
-mod write_tool;
-
-use action_log::ActionLog;
-use collections::HashMap;
-use context_server::listener::McpServerTool;
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
-use project::Project;
use settings::SettingsStore;
-use smol::process::Child;
use std::any::Any;
-use std::cell::RefCell;
-use std::fmt::Display;
-use std::path::{Path, PathBuf};
+use std::path::Path;
use std::rc::Rc;
-use util::command::new_smol_command;
-use uuid::Uuid;
-use agent_client_protocol as acp;
-use anyhow::{Context as _, Result, anyhow};
-use futures::channel::oneshot;
-use futures::{AsyncBufReadExt, AsyncWriteExt};
-use futures::{
- AsyncRead, AsyncWrite, FutureExt, StreamExt,
- channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
- io::BufReader,
- select_biased,
-};
-use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
-use serde::{Deserialize, Serialize};
-use util::{ResultExt, debug_panic};
+use anyhow::Result;
+use gpui::{App, AppContext as _, SharedString, Task};
-use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
-use crate::claude::tools::ClaudeTool;
-use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
-use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri};
+use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
+use acp_thread::AgentConnection;
#[derive(Clone)]
pub struct ClaudeCode;
+impl ClaudeCode {
+ const BINARY_NAME: &'static str = "claude-code-acp";
+ const PACKAGE_NAME: &'static str = "@zed-industries/claude-code-acp";
+}
+
impl AgentServer for ClaudeCode {
fn telemetry_id(&self) -> &'static str {
"claude-code"
@@ -51,1327 +27,55 @@ impl AgentServer for ClaudeCode {
"Claude Code".into()
}
- fn empty_state_headline(&self) -> SharedString {
- self.name()
- }
-
- fn empty_state_message(&self) -> SharedString {
- "How can I help you today?".into()
- }
-
fn logo(&self) -> ui::IconName {
ui::IconName::AiClaude
}
fn connect(
&self,
- _root_dir: &Path,
- _project: &Entity,
- _cx: &mut App,
+ root_dir: &Path,
+ delegate: AgentServerDelegate,
+ cx: &mut App,
) -> Task>> {
- let connection = ClaudeAgentConnection {
- sessions: Default::default(),
- };
-
- Task::ready(Ok(Rc::new(connection) as _))
- }
-
- fn into_any(self: Rc) -> Rc {
- self
- }
-}
-
-struct ClaudeAgentConnection {
- sessions: Rc>>,
-}
+ let root_dir = root_dir.to_path_buf();
+ let server_name = self.name();
+ let settings = cx.read_global(|settings: &SettingsStore, _| {
+ settings.get::(None).claude.clone()
+ });
-impl AgentConnection for ClaudeAgentConnection {
- fn new_thread(
- self: Rc,
- project: Entity,
- cwd: &Path,
- cx: &mut App,
- ) -> Task>> {
- let cwd = cwd.to_owned();
cx.spawn(async move |cx| {
- let settings = cx.read_global(|settings: &SettingsStore, _| {
- settings.get::(None).claude.clone()
- })?;
-
- let Some(command) = AgentServerCommand::resolve(
- "claude",
- &[],
- Some(&util::paths::home_dir().join(".claude/local/claude")),
- settings,
- &project,
- cx,
- )
- .await
- else {
- return Err(LoadError::NotInstalled {
- error_message: "Failed to find Claude Code binary".into(),
- install_message: "Install Claude Code".into(),
- install_command: "npm install -g @anthropic-ai/claude-code@latest".into(),
- }.into());
- };
-
- let api_key =
- cx.update(AnthropicLanguageModelProvider::api_key)?
- .await
- .map_err(|err| {
- if err.is::() {
- anyhow!(AuthRequired::new().with_language_model_provider(
- language_model::ANTHROPIC_PROVIDER_ID
- ))
- } else {
- anyhow!(err)
- }
- })?;
-
- let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
- let fs = project.read_with(cx, |project, _cx| project.fs().clone())?;
- let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), fs, cx).await?;
-
- let mut mcp_servers = HashMap::default();
- mcp_servers.insert(
- mcp_server::SERVER_NAME.to_string(),
- permission_mcp_server.server_config()?,
- );
- let mcp_config = McpConfig { mcp_servers };
-
- let mcp_config_file = tempfile::NamedTempFile::new()?;
- let (mcp_config_file, mcp_config_path) = mcp_config_file.into_parts();
-
- let mut mcp_config_file = smol::fs::File::from(mcp_config_file);
- mcp_config_file
- .write_all(serde_json::to_string(&mcp_config)?.as_bytes())
- .await?;
- mcp_config_file.flush().await?;
-
- let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
- let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
-
- let session_id = acp::SessionId(Uuid::new_v4().to_string().into());
-
- log::trace!("Starting session with id: {}", session_id);
-
- let mut child = spawn_claude(
- &command,
- ClaudeSessionMode::Start,
- session_id.clone(),
- api_key,
- &mcp_config_path,
- &cwd,
- )?;
-
- let stdout = child.stdout.take().context("Failed to take stdout")?;
- let stdin = child.stdin.take().context("Failed to take stdin")?;
- let stderr = child.stderr.take().context("Failed to take stderr")?;
-
- let pid = child.id();
- log::trace!("Spawned (pid: {})", pid);
-
- cx.background_spawn(async move {
- let mut stderr = BufReader::new(stderr);
- let mut line = String::new();
- while let Ok(n) = stderr.read_line(&mut line).await
- && n > 0
- {
- log::warn!("agent stderr: {}", &line);
- line.clear();
- }
- })
- .detach();
-
- cx.background_spawn(async move {
- let mut outgoing_rx = Some(outgoing_rx);
-
- ClaudeAgentSession::handle_io(
- outgoing_rx.take().unwrap(),
- incoming_message_tx.clone(),
- stdin,
- stdout,
- )
- .await?;
-
- log::trace!("Stopped (pid: {})", pid);
-
- drop(mcp_config_path);
- anyhow::Ok(())
- })
- .detach();
-
- let turn_state = Rc::new(RefCell::new(TurnState::None));
-
- let handler_task = cx.spawn({
- let turn_state = turn_state.clone();
- let mut thread_rx = thread_rx.clone();
- async move |cx| {
- while let Some(message) = incoming_message_rx.next().await {
- ClaudeAgentSession::handle_message(
- thread_rx.clone(),
- message,
- turn_state.clone(),
- cx,
- )
- .await
- }
-
- if let Some(status) = child.status().await.log_err()
- && let Some(thread) = thread_rx.recv().await.ok()
- {
- let version = claude_version(command.path.clone(), cx).await.log_err();
- let help = claude_help(command.path.clone(), cx).await.log_err();
- thread
- .update(cx, |thread, cx| {
- let error = if let Some(version) = version
- && let Some(help) = help
- && (!help.contains("--input-format")
- || !help.contains("--session-id"))
- {
- LoadError::Unsupported {
- error_message: format!(
- "Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.",
- command.path.to_string_lossy(),
- version,
- )
- .into(),
- upgrade_message: "Upgrade Claude Code to latest".into(),
- upgrade_command: format!(
- "{} update",
- command.path.to_string_lossy()
- ),
- }
- } else {
- LoadError::Exited { status }
- };
- thread.emit_load_error(error, cx);
- })
- .ok();
- }
- }
- });
-
- let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
- let thread = cx.new(|cx| {
- AcpThread::new(
- "Claude Code",
- self.clone(),
- project,
- action_log,
- session_id.clone(),
- watch::Receiver::constant(acp::PromptCapabilities {
- image: true,
- audio: false,
- embedded_context: true,
- }),
- cx,
- )
- })?;
-
- thread_tx.send(thread.downgrade())?;
-
- let session = ClaudeAgentSession {
- outgoing_tx,
- turn_state,
- _handler_task: handler_task,
- _mcp_server: Some(permission_mcp_server),
+ let mut command = if let Some(settings) = settings {
+ settings.command
+ } else {
+ cx.update(|cx| {
+ delegate.get_or_npm_install_builtin_agent(
+ Self::BINARY_NAME.into(),
+ Self::PACKAGE_NAME.into(),
+ format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
+ true,
+ None,
+ cx,
+ )
+ })?
+ .await?
};
- self.sessions.borrow_mut().insert(session_id, session);
+ if let Some(api_key) = cx
+ .update(AnthropicLanguageModelProvider::api_key)?
+ .await
+ .ok()
+ {
+ command
+ .env
+ .get_or_insert_default()
+ .insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
+ }
- Ok(thread)
+ crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
})
}
- fn auth_methods(&self) -> &[acp::AuthMethod] {
- &[]
- }
-
- fn authenticate(&self, _: acp::AuthMethodId, _cx: &mut App) -> Task> {
- Task::ready(Err(anyhow!("Authentication not supported")))
- }
-
- fn prompt(
- &self,
- _id: Option,
- params: acp::PromptRequest,
- cx: &mut App,
- ) -> Task> {
- let sessions = self.sessions.borrow();
- let Some(session) = sessions.get(¶ms.session_id) else {
- return Task::ready(Err(anyhow!(
- "Attempted to send message to nonexistent session {}",
- params.session_id
- )));
- };
-
- let (end_tx, end_rx) = oneshot::channel();
- session.turn_state.replace(TurnState::InProgress { end_tx });
-
- let content = acp_content_to_claude(params.prompt);
-
- if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
- message: Message {
- role: Role::User,
- content: Content::Chunks(content),
- id: None,
- model: None,
- stop_reason: None,
- stop_sequence: None,
- usage: None,
- },
- session_id: Some(params.session_id.to_string()),
- }) {
- return Task::ready(Err(anyhow!(err)));
- }
-
- cx.foreground_executor().spawn(async move { end_rx.await? })
- }
-
- fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
- let sessions = self.sessions.borrow();
- let Some(session) = sessions.get(session_id) else {
- log::warn!("Attempted to cancel nonexistent session {}", session_id);
- return;
- };
-
- let request_id = new_request_id();
-
- let turn_state = session.turn_state.take();
- let TurnState::InProgress { end_tx } = turn_state else {
- // Already canceled or idle, put it back
- session.turn_state.replace(turn_state);
- return;
- };
-
- session.turn_state.replace(TurnState::CancelRequested {
- end_tx,
- request_id: request_id.clone(),
- });
-
- session
- .outgoing_tx
- .unbounded_send(SdkMessage::ControlRequest {
- request_id,
- request: ControlRequest::Interrupt,
- })
- .log_err();
- }
-
fn into_any(self: Rc) -> Rc {
self
}
}
-
-#[derive(Clone, Copy)]
-enum ClaudeSessionMode {
- Start,
- #[expect(dead_code)]
- Resume,
-}
-
-fn spawn_claude(
- command: &AgentServerCommand,
- mode: ClaudeSessionMode,
- session_id: acp::SessionId,
- api_key: language_models::provider::anthropic::ApiKey,
- mcp_config_path: &Path,
- root_dir: &Path,
-) -> Result {
- let child = util::command::new_smol_command(&command.path)
- .args([
- "--input-format",
- "stream-json",
- "--output-format",
- "stream-json",
- "--print",
- "--verbose",
- "--mcp-config",
- mcp_config_path.to_string_lossy().as_ref(),
- "--permission-prompt-tool",
- &format!(
- "mcp__{}__{}",
- mcp_server::SERVER_NAME,
- permission_tool::PermissionTool::NAME,
- ),
- "--allowedTools",
- &format!(
- "mcp__{}__{}",
- mcp_server::SERVER_NAME,
- read_tool::ReadTool::NAME
- ),
- "--disallowedTools",
- "Read,Write,Edit,MultiEdit",
- ])
- .args(match mode {
- ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()],
- ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()],
- })
- .args(command.args.iter().map(|arg| arg.as_str()))
- .envs(command.env.iter().flatten())
- .env("ANTHROPIC_API_KEY", api_key.key)
- .current_dir(root_dir)
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::piped())
- .kill_on_drop(true)
- .spawn()?;
-
- Ok(child)
-}
-
-fn claude_version(path: PathBuf, cx: &mut AsyncApp) -> Task> {
- cx.background_spawn(async move {
- let output = new_smol_command(path).arg("--version").output().await?;
- let output = String::from_utf8(output.stdout)?;
- let version = output
- .trim()
- .strip_suffix(" (Claude Code)")
- .context("parsing Claude version")?;
- let version = semver::Version::parse(version)?;
- anyhow::Ok(version)
- })
-}
-
-fn claude_help(path: PathBuf, cx: &mut AsyncApp) -> Task> {
- cx.background_spawn(async move {
- let output = new_smol_command(path).arg("--help").output().await?;
- let output = String::from_utf8(output.stdout)?;
- anyhow::Ok(output)
- })
-}
-
-struct ClaudeAgentSession {
- outgoing_tx: UnboundedSender,
- turn_state: Rc>,
- _mcp_server: Option,
- _handler_task: Task<()>,
-}
-
-#[derive(Debug, Default)]
-enum TurnState {
- #[default]
- None,
- InProgress {
- end_tx: oneshot::Sender>,
- },
- CancelRequested {
- end_tx: oneshot::Sender>,
- request_id: String,
- },
- CancelConfirmed {
- end_tx: oneshot::Sender>,
- },
-}
-
-impl TurnState {
- fn is_canceled(&self) -> bool {
- matches!(self, TurnState::CancelConfirmed { .. })
- }
-
- fn end_tx(self) -> Option>> {
- match self {
- TurnState::None => None,
- TurnState::InProgress { end_tx, .. } => Some(end_tx),
- TurnState::CancelRequested { end_tx, .. } => Some(end_tx),
- TurnState::CancelConfirmed { end_tx } => Some(end_tx),
- }
- }
-
- fn confirm_cancellation(self, id: &str) -> Self {
- match self {
- TurnState::CancelRequested { request_id, end_tx } if request_id == id => {
- TurnState::CancelConfirmed { end_tx }
- }
- _ => self,
- }
- }
-}
-
-impl ClaudeAgentSession {
- async fn handle_message(
- mut thread_rx: watch::Receiver>,
- message: SdkMessage,
- turn_state: Rc>,
- cx: &mut AsyncApp,
- ) {
- match message {
- // we should only be sending these out, they don't need to be in the thread
- SdkMessage::ControlRequest { .. } => {}
- SdkMessage::User {
- message,
- session_id: _,
- } => {
- let Some(thread) = thread_rx
- .recv()
- .await
- .log_err()
- .and_then(|entity| entity.upgrade())
- else {
- log::error!("Received an SDK message but thread is gone");
- return;
- };
-
- for chunk in message.content.chunks() {
- match chunk {
- ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
- if !turn_state.borrow().is_canceled() {
- thread
- .update(cx, |thread, cx| {
- thread.push_user_content_block(None, text.into(), cx)
- })
- .log_err();
- }
- }
- ContentChunk::ToolResult {
- content,
- tool_use_id,
- } => {
- let content = content.to_string();
- thread
- .update(cx, |thread, cx| {
- let id = acp::ToolCallId(tool_use_id.into());
- let set_new_content = !content.is_empty()
- && thread.tool_call(&id).is_none_or(|(_, tool_call)| {
- // preserve rich diff if we have one
- tool_call.diffs().next().is_none()
- });
-
- thread.update_tool_call(
- acp::ToolCallUpdate {
- id,
- fields: acp::ToolCallUpdateFields {
- status: if turn_state.borrow().is_canceled() {
- // Do not set to completed if turn was canceled
- None
- } else {
- Some(acp::ToolCallStatus::Completed)
- },
- content: set_new_content
- .then(|| vec![content.into()]),
- ..Default::default()
- },
- },
- cx,
- )
- })
- .log_err();
- }
- ContentChunk::Thinking { .. }
- | ContentChunk::RedactedThinking
- | ContentChunk::ToolUse { .. } => {
- debug_panic!(
- "Should not get {:?} with role: assistant. should we handle this?",
- chunk
- );
- }
- ContentChunk::Image { source } => {
- if !turn_state.borrow().is_canceled() {
- thread
- .update(cx, |thread, cx| {
- thread.push_user_content_block(None, source.into(), cx)
- })
- .log_err();
- }
- }
-
- ContentChunk::Document | ContentChunk::WebSearchToolResult => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(
- format!("Unsupported content: {:?}", chunk).into(),
- false,
- cx,
- )
- })
- .log_err();
- }
- }
- }
- }
- SdkMessage::Assistant {
- message,
- session_id: _,
- } => {
- let Some(thread) = thread_rx
- .recv()
- .await
- .log_err()
- .and_then(|entity| entity.upgrade())
- else {
- log::error!("Received an SDK message but thread is gone");
- return;
- };
-
- for chunk in message.content.chunks() {
- match chunk {
- ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(text.into(), false, cx)
- })
- .log_err();
- }
- ContentChunk::Thinking { thinking } => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(thinking.into(), true, cx)
- })
- .log_err();
- }
- ContentChunk::RedactedThinking => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(
- "[REDACTED]".into(),
- true,
- cx,
- )
- })
- .log_err();
- }
- ContentChunk::ToolUse { id, name, input } => {
- let claude_tool = ClaudeTool::infer(&name, input);
-
- thread
- .update(cx, |thread, cx| {
- if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
- thread.update_plan(
- acp::Plan {
- entries: params
- .todos
- .into_iter()
- .map(Into::into)
- .collect(),
- },
- cx,
- )
- } else {
- thread.upsert_tool_call(
- claude_tool.as_acp(acp::ToolCallId(id.into())),
- cx,
- )?;
- }
- anyhow::Ok(())
- })
- .log_err();
- }
- ContentChunk::ToolResult { .. } | ContentChunk::WebSearchToolResult => {
- debug_panic!(
- "Should not get tool results with role: assistant. should we handle this?"
- );
- }
- ContentChunk::Image { source } => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(source.into(), false, cx)
- })
- .log_err();
- }
- ContentChunk::Document => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(
- format!("Unsupported content: {:?}", chunk).into(),
- false,
- cx,
- )
- })
- .log_err();
- }
- }
- }
- }
- SdkMessage::Result {
- is_error,
- subtype,
- result,
- ..
- } => {
- let turn_state = turn_state.take();
- let was_canceled = turn_state.is_canceled();
- let Some(end_turn_tx) = turn_state.end_tx() else {
- debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn");
- return;
- };
-
- if is_error || (!was_canceled && subtype == ResultErrorType::ErrorDuringExecution) {
- end_turn_tx
- .send(Err(anyhow!(
- "Error: {}",
- result.unwrap_or_else(|| subtype.to_string())
- )))
- .ok();
- } else {
- let stop_reason = match subtype {
- ResultErrorType::Success => acp::StopReason::EndTurn,
- ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests,
- ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled,
- };
- end_turn_tx
- .send(Ok(acp::PromptResponse { stop_reason }))
- .ok();
- }
- }
- SdkMessage::ControlResponse { response } => {
- if matches!(response.subtype, ResultErrorType::Success) {
- let new_state = turn_state.take().confirm_cancellation(&response.request_id);
- turn_state.replace(new_state);
- } else {
- log::error!("Control response error: {:?}", response);
- }
- }
- SdkMessage::System { .. } => {}
- }
- }
-
- async fn handle_io(
- mut outgoing_rx: UnboundedReceiver,
- incoming_tx: UnboundedSender,
- mut outgoing_bytes: impl Unpin + AsyncWrite,
- incoming_bytes: impl Unpin + AsyncRead,
- ) -> Result> {
- let mut output_reader = BufReader::new(incoming_bytes);
- let mut outgoing_line = Vec::new();
- let mut incoming_line = String::new();
- loop {
- select_biased! {
- message = outgoing_rx.next() => {
- if let Some(message) = message {
- outgoing_line.clear();
- serde_json::to_writer(&mut outgoing_line, &message)?;
- log::trace!("send: {}", String::from_utf8_lossy(&outgoing_line));
- outgoing_line.push(b'\n');
- outgoing_bytes.write_all(&outgoing_line).await.ok();
- } else {
- break;
- }
- }
- bytes_read = output_reader.read_line(&mut incoming_line).fuse() => {
- if bytes_read? == 0 {
- break
- }
- log::trace!("recv: {}", &incoming_line);
- match serde_json::from_str::(&incoming_line) {
- Ok(message) => {
- incoming_tx.unbounded_send(message).log_err();
- }
- Err(error) => {
- log::error!("failed to parse incoming message: {error}. Raw: {incoming_line}");
- }
- }
- incoming_line.clear();
- }
- }
- }
-
- Ok(outgoing_rx)
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct Message {
- role: Role,
- content: Content,
- #[serde(skip_serializing_if = "Option::is_none")]
- id: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- model: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- stop_reason: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- stop_sequence: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- usage: Option,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(untagged)]
-enum Content {
- UntaggedText(String),
- Chunks(Vec),
-}
-
-impl Content {
- pub fn chunks(self) -> impl Iterator- {
- match self {
- Self::Chunks(chunks) => chunks.into_iter(),
- Self::UntaggedText(text) => vec![ContentChunk::Text { text }].into_iter(),
- }
- }
-}
-
-impl Display for Content {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Content::UntaggedText(txt) => write!(f, "{}", txt),
- Content::Chunks(chunks) => {
- for chunk in chunks {
- write!(f, "{}", chunk)?;
- }
- Ok(())
- }
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "type", rename_all = "snake_case")]
-enum ContentChunk {
- Text {
- text: String,
- },
- ToolUse {
- id: String,
- name: String,
- input: serde_json::Value,
- },
- ToolResult {
- content: Content,
- tool_use_id: String,
- },
- Thinking {
- thinking: String,
- },
- RedactedThinking,
- Image {
- source: ImageSource,
- },
- // TODO
- Document,
- WebSearchToolResult,
- #[serde(untagged)]
- UntaggedText(String),
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "type", rename_all = "snake_case")]
-enum ImageSource {
- Base64 { data: String, media_type: String },
- Url { url: String },
-}
-
-impl Into for ImageSource {
- fn into(self) -> acp::ContentBlock {
- match self {
- ImageSource::Base64 { data, media_type } => {
- acp::ContentBlock::Image(acp::ImageContent {
- annotations: None,
- data,
- mime_type: media_type,
- uri: None,
- })
- }
- ImageSource::Url { url } => acp::ContentBlock::Image(acp::ImageContent {
- annotations: None,
- data: "".to_string(),
- mime_type: "".to_string(),
- uri: Some(url),
- }),
- }
- }
-}
-
-impl Display for ContentChunk {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- ContentChunk::Text { text } => write!(f, "{}", text),
- ContentChunk::Thinking { thinking } => write!(f, "Thinking: {}", thinking),
- ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"),
- ContentChunk::UntaggedText(text) => write!(f, "{}", text),
- ContentChunk::ToolResult { content, .. } => write!(f, "{}", content),
- ContentChunk::Image { .. }
- | ContentChunk::Document
- | ContentChunk::ToolUse { .. }
- | ContentChunk::WebSearchToolResult => {
- write!(f, "\n{:?}\n", &self)
- }
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct Usage {
- input_tokens: u32,
- cache_creation_input_tokens: u32,
- cache_read_input_tokens: u32,
- output_tokens: u32,
- service_tier: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-enum Role {
- System,
- Assistant,
- User,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct MessageParam {
- role: Role,
- content: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "type", rename_all = "snake_case")]
-enum SdkMessage {
- // An assistant message
- Assistant {
- message: Message, // from Anthropic SDK
- #[serde(skip_serializing_if = "Option::is_none")]
- session_id: Option,
- },
- // A user message
- User {
- message: Message, // from Anthropic SDK
- #[serde(skip_serializing_if = "Option::is_none")]
- session_id: Option,
- },
- // Emitted as the last message in a conversation
- Result {
- subtype: ResultErrorType,
- duration_ms: f64,
- duration_api_ms: f64,
- is_error: bool,
- num_turns: i32,
- #[serde(skip_serializing_if = "Option::is_none")]
- result: Option,
- session_id: String,
- total_cost_usd: f64,
- },
- // Emitted as the first message at the start of a conversation
- System {
- cwd: String,
- session_id: String,
- tools: Vec,
- model: String,
- mcp_servers: Vec,
- #[serde(rename = "apiKeySource")]
- api_key_source: String,
- #[serde(rename = "permissionMode")]
- permission_mode: PermissionMode,
- },
- /// Messages used to control the conversation, outside of chat messages to the model
- ControlRequest {
- request_id: String,
- request: ControlRequest,
- },
- /// Response to a control request
- ControlResponse { response: ControlResponse },
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "subtype", rename_all = "snake_case")]
-enum ControlRequest {
- /// Cancel the current conversation
- Interrupt,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct ControlResponse {
- request_id: String,
- subtype: ResultErrorType,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
-#[serde(rename_all = "snake_case")]
-enum ResultErrorType {
- Success,
- ErrorMaxTurns,
- ErrorDuringExecution,
-}
-
-impl Display for ResultErrorType {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- ResultErrorType::Success => write!(f, "success"),
- ResultErrorType::ErrorMaxTurns => write!(f, "error_max_turns"),
- ResultErrorType::ErrorDuringExecution => write!(f, "error_during_execution"),
- }
- }
-}
-
-fn acp_content_to_claude(prompt: Vec) -> Vec {
- let mut content = Vec::with_capacity(prompt.len());
- let mut context = Vec::with_capacity(prompt.len());
-
- for chunk in prompt {
- match chunk {
- acp::ContentBlock::Text(text_content) => {
- content.push(ContentChunk::Text {
- text: text_content.text,
- });
- }
- acp::ContentBlock::ResourceLink(resource_link) => {
- match MentionUri::parse(&resource_link.uri) {
- Ok(uri) => {
- content.push(ContentChunk::Text {
- text: format!("{}", uri.as_link()),
- });
- }
- Err(_) => {
- content.push(ContentChunk::Text {
- text: resource_link.uri,
- });
- }
- }
- }
- acp::ContentBlock::Resource(resource) => match resource.resource {
- acp::EmbeddedResourceResource::TextResourceContents(resource) => {
- match MentionUri::parse(&resource.uri) {
- Ok(uri) => {
- content.push(ContentChunk::Text {
- text: format!("{}", uri.as_link()),
- });
- }
- Err(_) => {
- content.push(ContentChunk::Text {
- text: resource.uri.clone(),
- });
- }
- }
-
- context.push(ContentChunk::Text {
- text: format!(
- "\n\n{}\n",
- resource.uri, resource.text
- ),
- });
- }
- acp::EmbeddedResourceResource::BlobResourceContents(_) => {
- // Unsupported by SDK
- }
- },
- acp::ContentBlock::Image(acp::ImageContent {
- data, mime_type, ..
- }) => content.push(ContentChunk::Image {
- source: ImageSource::Base64 {
- data,
- media_type: mime_type,
- },
- }),
- acp::ContentBlock::Audio(_) => {
- // Unsupported by SDK
- }
- }
- }
-
- content.extend(context);
- content
-}
-
-fn new_request_id() -> String {
- use rand::Rng;
- // In the Claude Code TS SDK they just generate a random 12 character string,
- // `Math.random().toString(36).substring(2, 15)`
- rand::thread_rng()
- .sample_iter(&rand::distributions::Alphanumeric)
- .take(12)
- .map(char::from)
- .collect()
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct McpServer {
- name: String,
- status: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum PermissionMode {
- Default,
- AcceptEdits,
- BypassPermissions,
- Plan,
-}
-
-#[cfg(test)]
-pub(crate) mod tests {
- use super::*;
- use crate::e2e_tests;
- use gpui::TestAppContext;
- use serde_json::json;
-
- crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow");
-
- pub fn local_command() -> AgentServerCommand {
- AgentServerCommand {
- path: "claude".into(),
- args: vec![],
- env: None,
- }
- }
-
- #[gpui::test]
- #[cfg_attr(not(feature = "e2e"), ignore)]
- async fn test_todo_plan(cx: &mut TestAppContext) {
- let fs = e2e_tests::init_test(cx).await;
- let project = Project::test(fs, [], cx).await;
- let thread =
- e2e_tests::new_test_thread(ClaudeCode, project.clone(), "/private/tmp", cx).await;
-
- thread
- .update(cx, |thread, cx| {
- thread.send_raw(
- "Create a todo plan for initializing a new React app. I'll follow it myself, do not execute on it.",
- cx,
- )
- })
- .await
- .unwrap();
-
- let mut entries_len = 0;
-
- thread.read_with(cx, |thread, _| {
- entries_len = thread.plan().entries.len();
- assert!(!thread.plan().entries.is_empty(), "Empty plan");
- });
-
- thread
- .update(cx, |thread, cx| {
- thread.send_raw(
- "Mark the first entry status as in progress without acting on it.",
- cx,
- )
- })
- .await
- .unwrap();
-
- thread.read_with(cx, |thread, _| {
- assert!(matches!(
- thread.plan().entries[0].status,
- acp::PlanEntryStatus::InProgress
- ));
- assert_eq!(thread.plan().entries.len(), entries_len);
- });
-
- thread
- .update(cx, |thread, cx| {
- thread.send_raw(
- "Now mark the first entry as completed without acting on it.",
- cx,
- )
- })
- .await
- .unwrap();
-
- thread.read_with(cx, |thread, _| {
- assert!(matches!(
- thread.plan().entries[0].status,
- acp::PlanEntryStatus::Completed
- ));
- assert_eq!(thread.plan().entries.len(), entries_len);
- });
- }
-
- #[test]
- fn test_deserialize_content_untagged_text() {
- let json = json!("Hello, world!");
- let content: Content = serde_json::from_value(json).unwrap();
- match content {
- Content::UntaggedText(text) => assert_eq!(text, "Hello, world!"),
- _ => panic!("Expected UntaggedText variant"),
- }
- }
-
- #[test]
- fn test_deserialize_content_chunks() {
- let json = json!([
- {
- "type": "text",
- "text": "Hello"
- },
- {
- "type": "tool_use",
- "id": "tool_123",
- "name": "calculator",
- "input": {"operation": "add", "a": 1, "b": 2}
- }
- ]);
- let content: Content = serde_json::from_value(json).unwrap();
- match content {
- Content::Chunks(chunks) => {
- assert_eq!(chunks.len(), 2);
- match &chunks[0] {
- ContentChunk::Text { text } => assert_eq!(text, "Hello"),
- _ => panic!("Expected Text chunk"),
- }
- match &chunks[1] {
- ContentChunk::ToolUse { id, name, input } => {
- assert_eq!(id, "tool_123");
- assert_eq!(name, "calculator");
- assert_eq!(input["operation"], "add");
- assert_eq!(input["a"], 1);
- assert_eq!(input["b"], 2);
- }
- _ => panic!("Expected ToolUse chunk"),
- }
- }
- _ => panic!("Expected Chunks variant"),
- }
- }
-
- #[test]
- fn test_deserialize_tool_result_untagged_text() {
- let json = json!({
- "type": "tool_result",
- "content": "Result content",
- "tool_use_id": "tool_456"
- });
- let chunk: ContentChunk = serde_json::from_value(json).unwrap();
- match chunk {
- ContentChunk::ToolResult {
- content,
- tool_use_id,
- } => {
- match content {
- Content::UntaggedText(text) => assert_eq!(text, "Result content"),
- _ => panic!("Expected UntaggedText content"),
- }
- assert_eq!(tool_use_id, "tool_456");
- }
- _ => panic!("Expected ToolResult variant"),
- }
- }
-
- #[test]
- fn test_deserialize_tool_result_chunks() {
- let json = json!({
- "type": "tool_result",
- "content": [
- {
- "type": "text",
- "text": "Processing complete"
- },
- {
- "type": "text",
- "text": "Result: 42"
- }
- ],
- "tool_use_id": "tool_789"
- });
- let chunk: ContentChunk = serde_json::from_value(json).unwrap();
- match chunk {
- ContentChunk::ToolResult {
- content,
- tool_use_id,
- } => {
- match content {
- Content::Chunks(chunks) => {
- assert_eq!(chunks.len(), 2);
- match &chunks[0] {
- ContentChunk::Text { text } => assert_eq!(text, "Processing complete"),
- _ => panic!("Expected Text chunk"),
- }
- match &chunks[1] {
- ContentChunk::Text { text } => assert_eq!(text, "Result: 42"),
- _ => panic!("Expected Text chunk"),
- }
- }
- _ => panic!("Expected Chunks content"),
- }
- assert_eq!(tool_use_id, "tool_789");
- }
- _ => panic!("Expected ToolResult variant"),
- }
- }
-
- #[test]
- fn test_acp_content_to_claude() {
- let acp_content = vec![
- acp::ContentBlock::Text(acp::TextContent {
- text: "Hello world".to_string(),
- annotations: None,
- }),
- acp::ContentBlock::Image(acp::ImageContent {
- data: "base64data".to_string(),
- mime_type: "image/png".to_string(),
- annotations: None,
- uri: None,
- }),
- acp::ContentBlock::ResourceLink(acp::ResourceLink {
- uri: "file:///path/to/example.rs".to_string(),
- name: "example.rs".to_string(),
- annotations: None,
- description: None,
- mime_type: None,
- size: None,
- title: None,
- }),
- acp::ContentBlock::Resource(acp::EmbeddedResource {
- annotations: None,
- resource: acp::EmbeddedResourceResource::TextResourceContents(
- acp::TextResourceContents {
- mime_type: None,
- text: "fn main() { println!(\"Hello!\"); }".to_string(),
- uri: "file:///path/to/code.rs".to_string(),
- },
- ),
- }),
- acp::ContentBlock::ResourceLink(acp::ResourceLink {
- uri: "invalid_uri_format".to_string(),
- name: "invalid.txt".to_string(),
- annotations: None,
- description: None,
- mime_type: None,
- size: None,
- title: None,
- }),
- ];
-
- let claude_content = acp_content_to_claude(acp_content);
-
- assert_eq!(claude_content.len(), 6);
-
- match &claude_content[0] {
- ContentChunk::Text { text } => assert_eq!(text, "Hello world"),
- _ => panic!("Expected Text chunk"),
- }
-
- match &claude_content[1] {
- ContentChunk::Image { source } => match source {
- ImageSource::Base64 { data, media_type } => {
- assert_eq!(data, "base64data");
- assert_eq!(media_type, "image/png");
- }
- _ => panic!("Expected Base64 image source"),
- },
- _ => panic!("Expected Image chunk"),
- }
-
- match &claude_content[2] {
- ContentChunk::Text { text } => {
- assert!(text.contains("example.rs"));
- assert!(text.contains("file:///path/to/example.rs"));
- }
- _ => panic!("Expected Text chunk for ResourceLink"),
- }
-
- match &claude_content[3] {
- ContentChunk::Text { text } => {
- assert!(text.contains("code.rs"));
- assert!(text.contains("file:///path/to/code.rs"));
- }
- _ => panic!("Expected Text chunk for Resource"),
- }
-
- match &claude_content[4] {
- ContentChunk::Text { text } => {
- assert_eq!(text, "invalid_uri_format");
- }
- _ => panic!("Expected Text chunk for invalid URI"),
- }
-
- match &claude_content[5] {
- ContentChunk::Text { text } => {
- assert!(text.contains(""));
- assert!(text.contains("fn main() { println!(\"Hello!\"); }"));
- assert!(text.contains(""));
- }
- _ => panic!("Expected Text chunk for context"),
- }
- }
-}
diff --git a/crates/agent_servers/src/claude/edit_tool.rs b/crates/agent_servers/src/claude/edit_tool.rs
deleted file mode 100644
index a8d93c3f3d5579173709b3ace6194059745885a7..0000000000000000000000000000000000000000
--- a/crates/agent_servers/src/claude/edit_tool.rs
+++ /dev/null
@@ -1,178 +0,0 @@
-use acp_thread::AcpThread;
-use anyhow::Result;
-use context_server::{
- listener::{McpServerTool, ToolResponse},
- types::{ToolAnnotations, ToolResponseContent},
-};
-use gpui::{AsyncApp, WeakEntity};
-use language::unified_diff;
-use util::markdown::MarkdownCodeBlock;
-
-use crate::tools::EditToolParams;
-
-#[derive(Clone)]
-pub struct EditTool {
- thread_rx: watch::Receiver>,
-}
-
-impl EditTool {
- pub fn new(thread_rx: watch::Receiver>) -> Self {
- Self { thread_rx }
- }
-}
-
-impl McpServerTool for EditTool {
- type Input = EditToolParams;
- type Output = ();
-
- const NAME: &'static str = "Edit";
-
- fn annotations(&self) -> ToolAnnotations {
- ToolAnnotations {
- title: Some("Edit file".to_string()),
- read_only_hint: Some(false),
- destructive_hint: Some(false),
- open_world_hint: Some(false),
- idempotent_hint: Some(false),
- }
- }
-
- async fn run(
- &self,
- input: Self::Input,
- cx: &mut AsyncApp,
- ) -> Result> {
- let mut thread_rx = self.thread_rx.clone();
- let Some(thread) = thread_rx.recv().await?.upgrade() else {
- anyhow::bail!("Thread closed");
- };
-
- let content = thread
- .update(cx, |thread, cx| {
- thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
- })?
- .await?;
-
- let (new_content, diff) = cx
- .background_executor()
- .spawn(async move {
- let new_content = content.replace(&input.old_text, &input.new_text);
- if new_content == content {
- return Err(anyhow::anyhow!("Failed to find `old_text`",));
- }
- let diff = unified_diff(&content, &new_content);
-
- Ok((new_content, diff))
- })
- .await?;
-
- thread
- .update(cx, |thread, cx| {
- thread.write_text_file(input.abs_path, new_content, cx)
- })?
- .await?;
-
- Ok(ToolResponse {
- content: vec![ToolResponseContent::Text {
- text: MarkdownCodeBlock {
- tag: "diff",
- text: diff.as_str().trim_end_matches('\n'),
- }
- .to_string(),
- }],
- structured_content: (),
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::rc::Rc;
-
- use acp_thread::{AgentConnection, StubAgentConnection};
- use gpui::{Entity, TestAppContext};
- use indoc::indoc;
- use project::{FakeFs, Project};
- use serde_json::json;
- use settings::SettingsStore;
- use util::path;
-
- use super::*;
-
- #[gpui::test]
- async fn old_text_not_found(cx: &mut TestAppContext) {
- let (_thread, tool) = init_test(cx).await;
-
- let result = tool
- .run(
- EditToolParams {
- abs_path: path!("/root/file.txt").into(),
- old_text: "hi".into(),
- new_text: "bye".into(),
- },
- &mut cx.to_async(),
- )
- .await;
-
- assert_eq!(result.unwrap_err().to_string(), "Failed to find `old_text`");
- }
-
- #[gpui::test]
- async fn found_and_replaced(cx: &mut TestAppContext) {
- let (_thread, tool) = init_test(cx).await;
-
- let result = tool
- .run(
- EditToolParams {
- abs_path: path!("/root/file.txt").into(),
- old_text: "hello".into(),
- new_text: "hi".into(),
- },
- &mut cx.to_async(),
- )
- .await;
-
- assert_eq!(
- result.unwrap().content[0].text().unwrap(),
- indoc! {
- r"
- ```diff
- @@ -1,1 +1,1 @@
- -hello
- +hi
- ```
- "
- }
- );
- }
-
- async fn init_test(cx: &mut TestAppContext) -> (Entity, EditTool) {
- cx.update(|cx| {
- let settings_store = SettingsStore::test(cx);
- cx.set_global(settings_store);
- language::init(cx);
- Project::init_settings(cx);
- });
-
- let connection = Rc::new(StubAgentConnection::new());
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(
- path!("/root"),
- json!({
- "file.txt": "hello"
- }),
- )
- .await;
- let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
- let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
-
- let thread = cx
- .update(|cx| connection.new_thread(project, path!("/test").as_ref(), cx))
- .await
- .unwrap();
-
- thread_tx.send(thread.downgrade()).unwrap();
-
- (thread, EditTool::new(thread_rx))
- }
-}
diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs
deleted file mode 100644
index 6442c784b59a655def92bcae108c27c503daa630..0000000000000000000000000000000000000000
--- a/crates/agent_servers/src/claude/mcp_server.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-use std::path::PathBuf;
-use std::sync::Arc;
-
-use crate::claude::edit_tool::EditTool;
-use crate::claude::permission_tool::PermissionTool;
-use crate::claude::read_tool::ReadTool;
-use crate::claude::write_tool::WriteTool;
-use acp_thread::AcpThread;
-#[cfg(not(test))]
-use anyhow::Context as _;
-use anyhow::Result;
-use collections::HashMap;
-use context_server::types::{
- Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
- ToolsCapabilities, requests,
-};
-use gpui::{App, AsyncApp, Task, WeakEntity};
-use project::Fs;
-use serde::Serialize;
-
-pub struct ClaudeZedMcpServer {
- server: context_server::listener::McpServer,
-}
-
-pub const SERVER_NAME: &str = "zed";
-
-impl ClaudeZedMcpServer {
- pub async fn new(
- thread_rx: watch::Receiver>,
- fs: Arc,
- cx: &AsyncApp,
- ) -> Result {
- let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
- mcp_server.handle_request::(Self::handle_initialize);
-
- mcp_server.add_tool(PermissionTool::new(fs.clone(), thread_rx.clone()));
- mcp_server.add_tool(ReadTool::new(thread_rx.clone()));
- mcp_server.add_tool(EditTool::new(thread_rx.clone()));
- mcp_server.add_tool(WriteTool::new(thread_rx.clone()));
-
- Ok(Self { server: mcp_server })
- }
-
- pub fn server_config(&self) -> Result {
- #[cfg(not(test))]
- let zed_path = std::env::current_exe()
- .context("finding current executable path for use in mcp_server")?;
-
- #[cfg(test)]
- let zed_path = crate::e2e_tests::get_zed_path();
-
- Ok(McpServerConfig {
- command: zed_path,
- args: vec![
- "--nc".into(),
- self.server.socket_path().display().to_string(),
- ],
- env: None,
- })
- }
-
- fn handle_initialize(_: InitializeParams, cx: &App) -> Task> {
- cx.foreground_executor().spawn(async move {
- Ok(InitializeResponse {
- protocol_version: ProtocolVersion("2025-06-18".into()),
- capabilities: ServerCapabilities {
- experimental: None,
- logging: None,
- completions: None,
- prompts: None,
- resources: None,
- tools: Some(ToolsCapabilities {
- list_changed: Some(false),
- }),
- },
- server_info: Implementation {
- name: SERVER_NAME.into(),
- version: "0.1.0".into(),
- },
- meta: None,
- })
- })
- }
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct McpConfig {
- pub mcp_servers: HashMap,
-}
-
-#[derive(Serialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct McpServerConfig {
- pub command: PathBuf,
- pub args: Vec,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub env: Option>,
-}
diff --git a/crates/agent_servers/src/claude/permission_tool.rs b/crates/agent_servers/src/claude/permission_tool.rs
deleted file mode 100644
index 96a24105e87bd99b46ec16e39dc32df57557f882..0000000000000000000000000000000000000000
--- a/crates/agent_servers/src/claude/permission_tool.rs
+++ /dev/null
@@ -1,158 +0,0 @@
-use std::sync::Arc;
-
-use acp_thread::AcpThread;
-use agent_client_protocol as acp;
-use agent_settings::AgentSettings;
-use anyhow::{Context as _, Result};
-use context_server::{
- listener::{McpServerTool, ToolResponse},
- types::ToolResponseContent,
-};
-use gpui::{AsyncApp, WeakEntity};
-use project::Fs;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings as _, update_settings_file};
-use util::debug_panic;
-
-use crate::tools::ClaudeTool;
-
-#[derive(Clone)]
-pub struct PermissionTool {
- fs: Arc,
- thread_rx: watch::Receiver>,
-}
-
-/// Request permission for tool calls
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct PermissionToolParams {
- tool_name: String,
- input: serde_json::Value,
- tool_use_id: Option,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct PermissionToolResponse {
- behavior: PermissionToolBehavior,
- updated_input: serde_json::Value,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "snake_case")]
-enum PermissionToolBehavior {
- Allow,
- Deny,
-}
-
-impl PermissionTool {
- pub fn new(fs: Arc, thread_rx: watch::Receiver>) -> Self {
- Self { fs, thread_rx }
- }
-}
-
-impl McpServerTool for PermissionTool {
- type Input = PermissionToolParams;
- type Output = ();
-
- const NAME: &'static str = "Confirmation";
-
- async fn run(
- &self,
- input: Self::Input,
- cx: &mut AsyncApp,
- ) -> Result> {
- if agent_settings::AgentSettings::try_read_global(cx, |settings| {
- settings.always_allow_tool_actions
- })
- .unwrap_or(false)
- {
- let response = PermissionToolResponse {
- behavior: PermissionToolBehavior::Allow,
- updated_input: input.input,
- };
-
- return Ok(ToolResponse {
- content: vec![ToolResponseContent::Text {
- text: serde_json::to_string(&response)?,
- }],
- structured_content: (),
- });
- }
-
- let mut thread_rx = self.thread_rx.clone();
- let Some(thread) = thread_rx.recv().await?.upgrade() else {
- anyhow::bail!("Thread closed");
- };
-
- let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
- let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
-
- const ALWAYS_ALLOW: &str = "always_allow";
- const ALLOW: &str = "allow";
- const REJECT: &str = "reject";
-
- let chosen_option = thread
- .update(cx, |thread, cx| {
- thread.request_tool_call_authorization(
- claude_tool.as_acp(tool_call_id).into(),
- vec![
- acp::PermissionOption {
- id: acp::PermissionOptionId(ALWAYS_ALLOW.into()),
- name: "Always Allow".into(),
- kind: acp::PermissionOptionKind::AllowAlways,
- },
- acp::PermissionOption {
- id: acp::PermissionOptionId(ALLOW.into()),
- name: "Allow".into(),
- kind: acp::PermissionOptionKind::AllowOnce,
- },
- acp::PermissionOption {
- id: acp::PermissionOptionId(REJECT.into()),
- name: "Reject".into(),
- kind: acp::PermissionOptionKind::RejectOnce,
- },
- ],
- cx,
- )
- })??
- .await?;
-
- let response = match chosen_option.0.as_ref() {
- ALWAYS_ALLOW => {
- cx.update(|cx| {
- update_settings_file::(self.fs.clone(), cx, |settings, _| {
- settings.set_always_allow_tool_actions(true);
- });
- })?;
-
- PermissionToolResponse {
- behavior: PermissionToolBehavior::Allow,
- updated_input: input.input,
- }
- }
- ALLOW => PermissionToolResponse {
- behavior: PermissionToolBehavior::Allow,
- updated_input: input.input,
- },
- REJECT => PermissionToolResponse {
- behavior: PermissionToolBehavior::Deny,
- updated_input: input.input,
- },
- opt => {
- debug_panic!("Unexpected option: {}", opt);
- PermissionToolResponse {
- behavior: PermissionToolBehavior::Deny,
- updated_input: input.input,
- }
- }
- };
-
- Ok(ToolResponse {
- content: vec![ToolResponseContent::Text {
- text: serde_json::to_string(&response)?,
- }],
- structured_content: (),
- })
- }
-}
diff --git a/crates/agent_servers/src/claude/read_tool.rs b/crates/agent_servers/src/claude/read_tool.rs
deleted file mode 100644
index cbe25876b3deabc32d1d30f7d8ad4de90fb21494..0000000000000000000000000000000000000000
--- a/crates/agent_servers/src/claude/read_tool.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use acp_thread::AcpThread;
-use anyhow::Result;
-use context_server::{
- listener::{McpServerTool, ToolResponse},
- types::{ToolAnnotations, ToolResponseContent},
-};
-use gpui::{AsyncApp, WeakEntity};
-
-use crate::tools::ReadToolParams;
-
-#[derive(Clone)]
-pub struct ReadTool {
- thread_rx: watch::Receiver>,
-}
-
-impl ReadTool {
- pub fn new(thread_rx: watch::Receiver>) -> Self {
- Self { thread_rx }
- }
-}
-
-impl McpServerTool for ReadTool {
- type Input = ReadToolParams;
- type Output = ();
-
- const NAME: &'static str = "Read";
-
- fn annotations(&self) -> ToolAnnotations {
- ToolAnnotations {
- title: Some("Read file".to_string()),
- read_only_hint: Some(true),
- destructive_hint: Some(false),
- open_world_hint: Some(false),
- idempotent_hint: None,
- }
- }
-
- async fn run(
- &self,
- input: Self::Input,
- cx: &mut AsyncApp,
- ) -> Result> {
- let mut thread_rx = self.thread_rx.clone();
- let Some(thread) = thread_rx.recv().await?.upgrade() else {
- anyhow::bail!("Thread closed");
- };
-
- let content = thread
- .update(cx, |thread, cx| {
- thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
- })?
- .await?;
-
- Ok(ToolResponse {
- content: vec![ToolResponseContent::Text { text: content }],
- structured_content: (),
- })
- }
-}
diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs
deleted file mode 100644
index 323190300131c87a443b95e4bb18424cf034e667..0000000000000000000000000000000000000000
--- a/crates/agent_servers/src/claude/tools.rs
+++ /dev/null
@@ -1,688 +0,0 @@
-use std::path::PathBuf;
-
-use agent_client_protocol as acp;
-use itertools::Itertools;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use util::ResultExt;
-
-pub enum ClaudeTool {
- Task(Option),
- NotebookRead(Option),
- NotebookEdit(Option),
- Edit(Option),
- MultiEdit(Option),
- ReadFile(Option),
- Write(Option),
- Ls(Option),
- Glob(Option),
- Grep(Option),
- Terminal(Option),
- WebFetch(Option),
- WebSearch(Option),
- TodoWrite(Option),
- ExitPlanMode(Option),
- Other {
- name: String,
- input: serde_json::Value,
- },
-}
-
-impl ClaudeTool {
- pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
- match tool_name {
- // Known tools
- "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
- "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
- "mcp__zed__Write" => Self::Write(serde_json::from_value(input).log_err()),
- "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
- "Write" => Self::Write(serde_json::from_value(input).log_err()),
- "LS" => Self::Ls(serde_json::from_value(input).log_err()),
- "Glob" => Self::Glob(serde_json::from_value(input).log_err()),
- "Grep" => Self::Grep(serde_json::from_value(input).log_err()),
- "Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
- "WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
- "WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
- "TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
- "exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
- "Task" => Self::Task(serde_json::from_value(input).log_err()),
- "NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
- "NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
- // Inferred from name
- _ => {
- let tool_name = tool_name.to_lowercase();
-
- if tool_name.contains("edit") || tool_name.contains("write") {
- Self::Edit(None)
- } else if tool_name.contains("terminal") {
- Self::Terminal(None)
- } else {
- Self::Other {
- name: tool_name,
- input,
- }
- }
- }
- }
- }
-
- pub fn label(&self) -> String {
- match &self {
- Self::Task(Some(params)) => params.description.clone(),
- Self::Task(None) => "Task".into(),
- Self::NotebookRead(Some(params)) => {
- format!("Read Notebook {}", params.notebook_path.display())
- }
- Self::NotebookRead(None) => "Read Notebook".into(),
- Self::NotebookEdit(Some(params)) => {
- format!("Edit Notebook {}", params.notebook_path.display())
- }
- Self::NotebookEdit(None) => "Edit Notebook".into(),
- Self::Terminal(Some(params)) => format!("`{}`", params.command),
- Self::Terminal(None) => "Terminal".into(),
- Self::ReadFile(_) => "Read File".into(),
- Self::Ls(Some(params)) => {
- format!("List Directory {}", params.path.display())
- }
- Self::Ls(None) => "List Directory".into(),
- Self::Edit(Some(params)) => {
- format!("Edit {}", params.abs_path.display())
- }
- Self::Edit(None) => "Edit".into(),
- Self::MultiEdit(Some(params)) => {
- format!("Multi Edit {}", params.file_path.display())
- }
- Self::MultiEdit(None) => "Multi Edit".into(),
- Self::Write(Some(params)) => {
- format!("Write {}", params.abs_path.display())
- }
- Self::Write(None) => "Write".into(),
- Self::Glob(Some(params)) => {
- format!("Glob `{params}`")
- }
- Self::Glob(None) => "Glob".into(),
- Self::Grep(Some(params)) => format!("`{params}`"),
- Self::Grep(None) => "Grep".into(),
- Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
- Self::WebFetch(None) => "Fetch".into(),
- Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
- Self::WebSearch(None) => "Web Search".into(),
- Self::TodoWrite(Some(params)) => format!(
- "Update TODOs: {}",
- params.todos.iter().map(|todo| &todo.content).join(", ")
- ),
- Self::TodoWrite(None) => "Update TODOs".into(),
- Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
- Self::Other { name, .. } => name.clone(),
- }
- }
- pub fn content(&self) -> Vec {
- match &self {
- Self::Other { input, .. } => vec![
- format!(
- "```json\n{}```",
- serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
- )
- .into(),
- ],
- Self::Task(Some(params)) => vec![params.prompt.clone().into()],
- Self::NotebookRead(Some(params)) => {
- vec![params.notebook_path.display().to_string().into()]
- }
- Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
- Self::Terminal(Some(params)) => vec![
- format!(
- "`{}`\n\n{}",
- params.command,
- params.description.as_deref().unwrap_or_default()
- )
- .into(),
- ],
- Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
- Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
- Self::Glob(Some(params)) => vec![params.to_string().into()],
- Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
- Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
- Self::WebSearch(Some(params)) => vec![params.to_string().into()],
- Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
- Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
- diff: acp::Diff {
- path: params.abs_path.clone(),
- old_text: Some(params.old_text.clone()),
- new_text: params.new_text.clone(),
- },
- }],
- Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
- diff: acp::Diff {
- path: params.abs_path.clone(),
- old_text: None,
- new_text: params.content.clone(),
- },
- }],
- Self::MultiEdit(Some(params)) => {
- // todo: show multiple edits in a multibuffer?
- params
- .edits
- .first()
- .map(|edit| {
- vec![acp::ToolCallContent::Diff {
- diff: acp::Diff {
- path: params.file_path.clone(),
- old_text: Some(edit.old_string.clone()),
- new_text: edit.new_string.clone(),
- },
- }]
- })
- .unwrap_or_default()
- }
- Self::TodoWrite(Some(_)) => {
- // These are mapped to plan updates later
- vec![]
- }
- Self::Task(None)
- | Self::NotebookRead(None)
- | Self::NotebookEdit(None)
- | Self::Terminal(None)
- | Self::ReadFile(None)
- | Self::Ls(None)
- | Self::Glob(None)
- | Self::Grep(None)
- | Self::WebFetch(None)
- | Self::WebSearch(None)
- | Self::TodoWrite(None)
- | Self::ExitPlanMode(None)
- | Self::Edit(None)
- | Self::Write(None)
- | Self::MultiEdit(None) => vec![],
- }
- }
-
- pub fn kind(&self) -> acp::ToolKind {
- match self {
- Self::Task(_) => acp::ToolKind::Think,
- Self::NotebookRead(_) => acp::ToolKind::Read,
- Self::NotebookEdit(_) => acp::ToolKind::Edit,
- Self::Edit(_) => acp::ToolKind::Edit,
- Self::MultiEdit(_) => acp::ToolKind::Edit,
- Self::Write(_) => acp::ToolKind::Edit,
- Self::ReadFile(_) => acp::ToolKind::Read,
- Self::Ls(_) => acp::ToolKind::Search,
- Self::Glob(_) => acp::ToolKind::Search,
- Self::Grep(_) => acp::ToolKind::Search,
- Self::Terminal(_) => acp::ToolKind::Execute,
- Self::WebSearch(_) => acp::ToolKind::Search,
- Self::WebFetch(_) => acp::ToolKind::Fetch,
- Self::TodoWrite(_) => acp::ToolKind::Think,
- Self::ExitPlanMode(_) => acp::ToolKind::Think,
- Self::Other { .. } => acp::ToolKind::Other,
- }
- }
-
- pub fn locations(&self) -> Vec {
- match &self {
- Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
- path: abs_path.clone(),
- line: None,
- }],
- Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
- vec![acp::ToolCallLocation {
- path: file_path.clone(),
- line: None,
- }]
- }
- Self::Write(Some(WriteToolParams {
- abs_path: file_path,
- ..
- })) => {
- vec![acp::ToolCallLocation {
- path: file_path.clone(),
- line: None,
- }]
- }
- Self::ReadFile(Some(ReadToolParams {
- abs_path, offset, ..
- })) => vec![acp::ToolCallLocation {
- path: abs_path.clone(),
- line: *offset,
- }],
- Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
- vec![acp::ToolCallLocation {
- path: notebook_path.clone(),
- line: None,
- }]
- }
- Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
- vec![acp::ToolCallLocation {
- path: notebook_path.clone(),
- line: None,
- }]
- }
- Self::Glob(Some(GlobToolParams {
- path: Some(path), ..
- })) => vec![acp::ToolCallLocation {
- path: path.clone(),
- line: None,
- }],
- Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
- path: path.clone(),
- line: None,
- }],
- Self::Grep(Some(GrepToolParams {
- path: Some(path), ..
- })) => vec![acp::ToolCallLocation {
- path: PathBuf::from(path),
- line: None,
- }],
- Self::Task(_)
- | Self::NotebookRead(None)
- | Self::NotebookEdit(None)
- | Self::Edit(None)
- | Self::MultiEdit(None)
- | Self::Write(None)
- | Self::ReadFile(None)
- | Self::Ls(None)
- | Self::Glob(_)
- | Self::Grep(_)
- | Self::Terminal(_)
- | Self::WebFetch(_)
- | Self::WebSearch(_)
- | Self::TodoWrite(_)
- | Self::ExitPlanMode(_)
- | Self::Other { .. } => vec![],
- }
- }
-
- pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
- acp::ToolCall {
- id,
- kind: self.kind(),
- status: acp::ToolCallStatus::InProgress,
- title: self.label(),
- content: self.content(),
- locations: self.locations(),
- raw_input: None,
- raw_output: None,
- }
- }
-}
-
-/// Edit a file.
-///
-/// In sessions with mcp__zed__Edit always use it instead of Edit as it will
-/// allow the user to conveniently review changes.
-///
-/// File editing instructions:
-/// - The `old_text` param must match existing file content, including indentation.
-/// - The `old_text` param must come from the actual file, not an outline.
-/// - The `old_text` section must not be empty.
-/// - Be minimal with replacements:
-/// - For unique lines, include only those lines.
-/// - For non-unique lines, include enough context to identify them.
-/// - Do not escape quotes, newlines, or other characters.
-/// - Only edit the specified file.
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct EditToolParams {
- /// The absolute path to the file to read.
- pub abs_path: PathBuf,
- /// The old text to replace (must be unique in the file)
- pub old_text: String,
- /// The new text.
- pub new_text: String,
-}
-
-/// Reads the content of the given file in the project.
-///
-/// Never attempt to read a path that hasn't been previously mentioned.
-///
-/// In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents.
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct ReadToolParams {
- /// The absolute path to the file to read.
- pub abs_path: PathBuf,
- /// Which line to start reading from. Omit to start from the beginning.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub offset: Option,
- /// How many lines to read. Omit for the whole file.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub limit: Option,
-}
-
-/// Writes content to the specified file in the project.
-///
-/// In sessions with mcp__zed__Write always use it instead of Write as it will
-/// allow the user to conveniently review changes.
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct WriteToolParams {
- /// The absolute path of the file to write.
- pub abs_path: PathBuf,
- /// The full content to write.
- pub content: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct BashToolParams {
- /// Shell command to execute
- pub command: String,
- /// 5-10 word description of what command does
- #[serde(skip_serializing_if = "Option::is_none")]
- pub description: Option,
- /// Timeout in ms (max 600000ms/10min, default 120000ms)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub timeout: Option,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct GlobToolParams {
- /// Glob pattern like **/*.js or src/**/*.ts
- pub pattern: String,
- /// Directory to search in (omit for current directory)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub path: Option,
-}
-
-impl std::fmt::Display for GlobToolParams {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- if let Some(path) = &self.path {
- write!(f, "{}", path.display())?;
- }
- write!(f, "{}", self.pattern)
- }
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct LsToolParams {
- /// Absolute path to directory
- pub path: PathBuf,
- /// Array of glob patterns to ignore
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub ignore: Vec,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct GrepToolParams {
- /// Regex pattern to search for
- pub pattern: String,
- /// File/directory to search (defaults to current directory)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub path: Option,
- /// "content" (shows lines), "files_with_matches" (default), "count"
- #[serde(skip_serializing_if = "Option::is_none")]
- pub output_mode: Option,
- /// Filter files with glob pattern like "*.js"
- #[serde(skip_serializing_if = "Option::is_none")]
- pub glob: Option,
- /// File type filter like "js", "py", "rust"
- #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
- pub file_type: Option,
- /// Case insensitive search
- #[serde(rename = "-i", default, skip_serializing_if = "is_false")]
- pub case_insensitive: bool,
- /// Show line numbers (content mode only)
- #[serde(rename = "-n", default, skip_serializing_if = "is_false")]
- pub line_numbers: bool,
- /// Lines after match (content mode only)
- #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
- pub after_context: Option,
- /// Lines before match (content mode only)
- #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
- pub before_context: Option,
- /// Lines before and after match (content mode only)
- #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
- pub context: Option,
- /// Enable multiline/cross-line matching
- #[serde(default, skip_serializing_if = "is_false")]
- pub multiline: bool,
- /// Limit output to first N results
- #[serde(skip_serializing_if = "Option::is_none")]
- pub head_limit: Option,
-}
-
-impl std::fmt::Display for GrepToolParams {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "grep")?;
-
- // Boolean flags
- if self.case_insensitive {
- write!(f, " -i")?;
- }
- if self.line_numbers {
- write!(f, " -n")?;
- }
-
- // Context options
- if let Some(after) = self.after_context {
- write!(f, " -A {}", after)?;
- }
- if let Some(before) = self.before_context {
- write!(f, " -B {}", before)?;
- }
- if let Some(context) = self.context {
- write!(f, " -C {}", context)?;
- }
-
- // Output mode
- if let Some(mode) = &self.output_mode {
- match mode {
- GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
- GrepOutputMode::Count => write!(f, " -c")?,
- GrepOutputMode::Content => {} // Default mode
- }
- }
-
- // Head limit
- if let Some(limit) = self.head_limit {
- write!(f, " | head -{}", limit)?;
- }
-
- // Glob pattern
- if let Some(glob) = &self.glob {
- write!(f, " --include=\"{}\"", glob)?;
- }
-
- // File type
- if let Some(file_type) = &self.file_type {
- write!(f, " --type={}", file_type)?;
- }
-
- // Multiline
- if self.multiline {
- write!(f, " -P")?; // Perl-compatible regex for multiline
- }
-
- // Pattern (escaped if contains special characters)
- write!(f, " \"{}\"", self.pattern)?;
-
- // Path
- if let Some(path) = &self.path {
- write!(f, " {}", path)?;
- }
-
- Ok(())
- }
-}
-
-#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum TodoPriority {
- High,
- #[default]
- Medium,
- Low,
-}
-
-impl Into for TodoPriority {
- fn into(self) -> acp::PlanEntryPriority {
- match self {
- TodoPriority::High => acp::PlanEntryPriority::High,
- TodoPriority::Medium => acp::PlanEntryPriority::Medium,
- TodoPriority::Low => acp::PlanEntryPriority::Low,
- }
- }
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum TodoStatus {
- Pending,
- InProgress,
- Completed,
-}
-
-impl Into for TodoStatus {
- fn into(self) -> acp::PlanEntryStatus {
- match self {
- TodoStatus::Pending => acp::PlanEntryStatus::Pending,
- TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
- TodoStatus::Completed => acp::PlanEntryStatus::Completed,
- }
- }
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-pub struct Todo {
- /// Task description
- pub content: String,
- /// Current status of the todo
- pub status: TodoStatus,
- /// Priority level of the todo
- #[serde(default)]
- pub priority: TodoPriority,
-}
-
-impl Into for Todo {
- fn into(self) -> acp::PlanEntry {
- acp::PlanEntry {
- content: self.content,
- priority: self.priority.into(),
- status: self.status.into(),
- }
- }
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct TodoWriteToolParams {
- pub todos: Vec,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct ExitPlanModeToolParams {
- /// Implementation plan in markdown format
- pub plan: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct TaskToolParams {
- /// Short 3-5 word description of task
- pub description: String,
- /// Detailed task for agent to perform
- pub prompt: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct NotebookReadToolParams {
- /// Absolute path to .ipynb file
- pub notebook_path: PathBuf,
- /// Specific cell ID to read
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cell_id: Option,
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum CellType {
- Code,
- Markdown,
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum EditMode {
- Replace,
- Insert,
- Delete,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct NotebookEditToolParams {
- /// Absolute path to .ipynb file
- pub notebook_path: PathBuf,
- /// New cell content
- pub new_source: String,
- /// Cell ID to edit
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cell_id: Option,
- /// Type of cell (code or markdown)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cell_type: Option,
- /// Edit operation mode
- #[serde(skip_serializing_if = "Option::is_none")]
- pub edit_mode: Option,
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-pub struct MultiEditItem {
- /// The text to search for and replace
- pub old_string: String,
- /// The replacement text
- pub new_string: String,
- /// Whether to replace all occurrences or just the first
- #[serde(default, skip_serializing_if = "is_false")]
- pub replace_all: bool,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct MultiEditToolParams {
- /// Absolute path to file
- pub file_path: PathBuf,
- /// List of edits to apply
- pub edits: Vec,
-}
-
-fn is_false(v: &bool) -> bool {
- !*v
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum GrepOutputMode {
- Content,
- FilesWithMatches,
- Count,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct WebFetchToolParams {
- /// Valid URL to fetch
- #[serde(rename = "url")]
- pub url: String,
- /// What to extract from content
- pub prompt: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct WebSearchToolParams {
- /// Search query (min 2 chars)
- pub query: String,
- /// Only include these domains
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub allowed_domains: Vec,
- /// Exclude these domains
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub blocked_domains: Vec,
-}
-
-impl std::fmt::Display for WebSearchToolParams {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "\"{}\"", self.query)?;
-
- if !self.allowed_domains.is_empty() {
- write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
- }
-
- if !self.blocked_domains.is_empty() {
- write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
- }
-
- Ok(())
- }
-}
diff --git a/crates/agent_servers/src/claude/write_tool.rs b/crates/agent_servers/src/claude/write_tool.rs
deleted file mode 100644
index 39479a9c38ba616b3d0f2e4197c112bcbea68261..0000000000000000000000000000000000000000
--- a/crates/agent_servers/src/claude/write_tool.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use acp_thread::AcpThread;
-use anyhow::Result;
-use context_server::{
- listener::{McpServerTool, ToolResponse},
- types::ToolAnnotations,
-};
-use gpui::{AsyncApp, WeakEntity};
-
-use crate::tools::WriteToolParams;
-
-#[derive(Clone)]
-pub struct WriteTool {
- thread_rx: watch::Receiver>,
-}
-
-impl WriteTool {
- pub fn new(thread_rx: watch::Receiver>) -> Self {
- Self { thread_rx }
- }
-}
-
-impl McpServerTool for WriteTool {
- type Input = WriteToolParams;
- type Output = ();
-
- const NAME: &'static str = "Write";
-
- fn annotations(&self) -> ToolAnnotations {
- ToolAnnotations {
- title: Some("Write file".to_string()),
- read_only_hint: Some(false),
- destructive_hint: Some(false),
- open_world_hint: Some(false),
- idempotent_hint: Some(false),
- }
- }
-
- async fn run(
- &self,
- input: Self::Input,
- cx: &mut AsyncApp,
- ) -> Result> {
- let mut thread_rx = self.thread_rx.clone();
- let Some(thread) = thread_rx.recv().await?.upgrade() else {
- anyhow::bail!("Thread closed");
- };
-
- thread
- .update(cx, |thread, cx| {
- thread.write_text_file(input.abs_path, input.content, cx)
- })?
- .await?;
-
- Ok(ToolResponse {
- content: vec![],
- structured_content: (),
- })
- }
-}
diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs
index 72823026d7ce353e76485fbe76783b3cc2bbeb56..8d9670473a619eea9dc6730d04a4c807937aa393 100644
--- a/crates/agent_servers/src/custom.rs
+++ b/crates/agent_servers/src/custom.rs
@@ -1,8 +1,7 @@
-use crate::{AgentServerCommand, AgentServerSettings};
+use crate::{AgentServerCommand, AgentServerDelegate};
use acp_thread::AgentConnection;
use anyhow::Result;
-use gpui::{App, Entity, SharedString, Task};
-use project::Project;
+use gpui::{App, SharedString, Task};
use std::{path::Path, rc::Rc};
use ui::IconName;
@@ -13,11 +12,8 @@ pub struct CustomAgentServer {
}
impl CustomAgentServer {
- pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self {
- Self {
- name,
- command: settings.command.clone(),
- }
+ pub fn new(name: SharedString, command: AgentServerCommand) -> Self {
+ Self { name, command }
}
}
@@ -34,27 +30,16 @@ impl crate::AgentServer for CustomAgentServer {
IconName::Terminal
}
- fn empty_state_headline(&self) -> SharedString {
- "No conversations yet".into()
- }
-
- fn empty_state_message(&self) -> SharedString {
- format!("Start a conversation with {}", self.name).into()
- }
-
fn connect(
&self,
root_dir: &Path,
- _project: &Entity,
+ _delegate: AgentServerDelegate,
cx: &mut App,
) -> Task>> {
let server_name = self.name();
let command = self.command.clone();
let root_dir = root_dir.to_path_buf();
-
- cx.spawn(async move |mut cx| {
- crate::acp::connect(server_name, command, &root_dir, &mut cx).await
- })
+ cx.spawn(async move |cx| crate::acp::connect(server_name, command, &root_dir, cx).await)
}
fn into_any(self: Rc) -> Rc {
diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs
index 42264b4b4f747e11cab11a21f7cde2ad0c43fee3..5d2becf0ccc4b30cfeca27f4eb5ee08c2d0bb7d1 100644
--- a/crates/agent_servers/src/e2e_tests.rs
+++ b/crates/agent_servers/src/e2e_tests.rs
@@ -1,4 +1,6 @@
-use crate::AgentServer;
+use crate::{AgentServer, AgentServerDelegate};
+#[cfg(test)]
+use crate::{AgentServerCommand, CustomAgentServerSettings};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
@@ -471,12 +473,14 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc {
#[cfg(test)]
crate::AllAgentServersSettings::override_global(
crate::AllAgentServersSettings {
- claude: Some(crate::AgentServerSettings {
- command: crate::claude::tests::local_command(),
- }),
- gemini: Some(crate::AgentServerSettings {
- command: crate::gemini::tests::local_command(),
+ claude: Some(CustomAgentServerSettings {
+ command: AgentServerCommand {
+ path: "claude-code-acp".into(),
+ args: vec![],
+ env: None,
+ },
}),
+ gemini: Some(crate::gemini::tests::local_command().into()),
custom: collections::HashMap::default(),
},
cx,
@@ -494,8 +498,10 @@ pub async fn new_test_thread(
current_dir: impl AsRef,
cx: &mut TestAppContext,
) -> Entity {
+ let delegate = AgentServerDelegate::new(project.clone(), watch::channel("".into()).0);
+
let connection = cx
- .update(|cx| server.connect(current_dir.as_ref(), &project, cx))
+ .update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
.await
.unwrap();
diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs
index 5d6a70fa64d981b2f25aee13c9d1d3ac7f94468f..5e958f686959d78e6ceaf8b8ea7d8404ffba166a 100644
--- a/crates/agent_servers/src/gemini.rs
+++ b/crates/agent_servers/src/gemini.rs
@@ -1,12 +1,12 @@
use std::rc::Rc;
use std::{any::Any, path::Path};
-use crate::{AgentServer, AgentServerCommand};
+use crate::acp::AcpConnection;
+use crate::{AgentServer, AgentServerDelegate};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
-use gpui::{App, Entity, SharedString, Task};
+use gpui::{App, AppContext as _, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
-use project::Project;
use settings::SettingsStore;
use crate::AllAgentServersSettings;
@@ -25,14 +25,6 @@ impl AgentServer for Gemini {
"Gemini CLI".into()
}
- fn empty_state_headline(&self) -> SharedString {
- self.name()
- }
-
- fn empty_state_message(&self) -> SharedString {
- "Ask questions, edit files, run commands".into()
- }
-
fn logo(&self) -> ui::IconName {
ui::IconName::AiGemini
}
@@ -40,60 +32,99 @@ impl AgentServer for Gemini {
fn connect(
&self,
root_dir: &Path,
- project: &Entity,
+ delegate: AgentServerDelegate,
cx: &mut App,
) -> Task>> {
- let project = project.clone();
let root_dir = root_dir.to_path_buf();
let server_name = self.name();
+ let settings = cx.read_global(|settings: &SettingsStore, _| {
+ settings.get::(None).gemini.clone()
+ });
+
cx.spawn(async move |cx| {
- let settings = cx.read_global(|settings: &SettingsStore, _| {
- settings.get::(None).gemini.clone()
- })?;
-
- let Some(mut command) =
- AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
- else {
- return Err(LoadError::NotInstalled {
- error_message: "Failed to find Gemini CLI binary".into(),
- install_message: "Install Gemini CLI".into(),
- install_command: Self::install_command().into(),
- }.into());
+ let ignore_system_version = settings
+ .as_ref()
+ .and_then(|settings| settings.ignore_system_version)
+ .unwrap_or(true);
+ let mut command = if let Some(settings) = settings
+ && let Some(command) = settings.custom_command()
+ {
+ command
+ } else {
+ cx.update(|cx| {
+ delegate.get_or_npm_install_builtin_agent(
+ Self::BINARY_NAME.into(),
+ Self::PACKAGE_NAME.into(),
+ format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
+ ignore_system_version,
+ Some(Self::MINIMUM_VERSION.parse().unwrap()),
+ cx,
+ )
+ })?
+ .await?
};
+ command.args.push("--experimental-acp".into());
- if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
- command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key);
+ if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
+ command
+ .env
+ .get_or_insert_default()
+ .insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
- if result.is_err() {
- let version_fut = util::command::new_smol_command(&command.path)
- .args(command.args.iter())
- .arg("--version")
- .kill_on_drop(true)
- .output();
-
- let help_fut = util::command::new_smol_command(&command.path)
- .args(command.args.iter())
- .arg("--help")
- .kill_on_drop(true)
- .output();
-
- let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
-
- let current_version = String::from_utf8(version_output?.stdout)?;
- let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
-
- if !supported {
- return Err(LoadError::Unsupported {
- error_message: format!(
- "Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).",
- command.path.to_string_lossy(),
- current_version
- ).into(),
- upgrade_message: "Upgrade Gemini CLI to latest".into(),
- upgrade_command: Self::upgrade_command().into(),
- }.into())
+ match &result {
+ Ok(connection) => {
+ if let Some(connection) = connection.clone().downcast::()
+ && !connection.prompt_capabilities().image
+ {
+ let version_output = util::command::new_smol_command(&command.path)
+ .args(command.args.iter())
+ .arg("--version")
+ .kill_on_drop(true)
+ .output()
+ .await;
+ let current_version =
+ String::from_utf8(version_output?.stdout)?.trim().to_owned();
+ if !connection.prompt_capabilities().image {
+ return Err(LoadError::Unsupported {
+ current_version: current_version.into(),
+ command: command.path.to_string_lossy().to_string().into(),
+ minimum_version: Self::MINIMUM_VERSION.into(),
+ }
+ .into());
+ }
+ }
+ }
+ Err(_) => {
+ let version_fut = util::command::new_smol_command(&command.path)
+ .args(command.args.iter())
+ .arg("--version")
+ .kill_on_drop(true)
+ .output();
+
+ let help_fut = util::command::new_smol_command(&command.path)
+ .args(command.args.iter())
+ .arg("--help")
+ .kill_on_drop(true)
+ .output();
+
+ let (version_output, help_output) =
+ futures::future::join(version_fut, help_fut).await;
+
+ let current_version = std::str::from_utf8(&version_output?.stdout)?
+ .trim()
+ .to_string();
+ let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
+
+ if !supported {
+ return Err(LoadError::Unsupported {
+ current_version: current_version.into(),
+ command: command.path.to_string_lossy().to_string().into(),
+ minimum_version: Self::MINIMUM_VERSION.into(),
+ }
+ .into());
+ }
}
}
result
@@ -106,17 +137,11 @@ impl AgentServer for Gemini {
}
impl Gemini {
- pub fn binary_name() -> &'static str {
- "gemini"
- }
+ const PACKAGE_NAME: &str = "@google/gemini-cli";
- pub fn install_command() -> &'static str {
- "npm install -g @google/gemini-cli@preview"
- }
+ const MINIMUM_VERSION: &str = "0.2.1";
- pub fn upgrade_command() -> &'static str {
- "npm install -g @google/gemini-cli@preview"
- }
+ const BINARY_NAME: &str = "gemini";
}
#[cfg(test)]
diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs
index 96ac6e3cbe7dcd8a03aef5c6ec79c884bf99ae67..81f80a7d7d9581b8c1862ae3393c4a5d5e6706b6 100644
--- a/crates/agent_servers/src/settings.rs
+++ b/crates/agent_servers/src/settings.rs
@@ -1,3 +1,5 @@
+use std::path::PathBuf;
+
use crate::AgentServerCommand;
use anyhow::Result;
use collections::HashMap;
@@ -12,16 +14,62 @@ pub fn init(cx: &mut App) {
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
pub struct AllAgentServersSettings {
- pub gemini: Option,
- pub claude: Option,
+ pub gemini: Option,
+ pub claude: Option,
/// Custom agent servers configured by the user
#[serde(flatten)]
- pub custom: HashMap,
+ pub custom: HashMap,
+}
+
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+pub struct BuiltinAgentServerSettings {
+ /// Absolute path to a binary to be used when launching this agent.
+ ///
+ /// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
+ #[serde(rename = "command")]
+ pub path: Option,
+ /// If a binary is specified in `command`, it will be passed these arguments.
+ pub args: Option>,
+ /// If a binary is specified in `command`, it will be passed these environment variables.
+ pub env: Option>,
+ /// Whether to skip searching `$PATH` for an agent server binary when
+ /// launching this agent.
+ ///
+ /// This has no effect if a `command` is specified. Otherwise, when this is
+ /// `false`, Zed will search `$PATH` for an agent server binary and, if one
+ /// is found, use it for threads with this agent. If no agent binary is
+ /// found on `$PATH`, Zed will automatically install and use its own binary.
+ /// When this is `true`, Zed will not search `$PATH`, and will always use
+ /// its own binary.
+ ///
+ /// Default: true
+ pub ignore_system_version: Option,
+}
+
+impl BuiltinAgentServerSettings {
+ pub(crate) fn custom_command(self) -> Option {
+ self.path.map(|path| AgentServerCommand {
+ path,
+ args: self.args.unwrap_or_default(),
+ env: self.env,
+ })
+ }
+}
+
+impl From for BuiltinAgentServerSettings {
+ fn from(value: AgentServerCommand) -> Self {
+ BuiltinAgentServerSettings {
+ path: Some(value.path),
+ args: Some(value.args),
+ env: value.env,
+ ..Default::default()
+ }
+ }
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
-pub struct AgentServerSettings {
+pub struct CustomAgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
}
diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs
index 0e4080d689bd4ae4ff67bdd7c6a9beb3f220f2b9..76b3709325a0c84a72bc71db8a67a3d4bd72dd06 100644
--- a/crates/agent_ui/src/acp/entry_view_state.rs
+++ b/crates/agent_ui/src/acp/entry_view_state.rs
@@ -6,8 +6,8 @@ use agent2::HistoryStore;
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
- AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
- TextStyleRefinement, WeakEntity, Window,
+ AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
+ ScrollHandle, TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
use project::Project;
@@ -154,10 +154,22 @@ impl EntryViewState {
});
}
}
- AgentThreadEntry::AssistantMessage(_) => {
- if index == self.entries.len() {
- self.entries.push(Entry::empty())
- }
+ AgentThreadEntry::AssistantMessage(message) => {
+ let entry = if let Some(Entry::AssistantMessage(entry)) =
+ self.entries.get_mut(index)
+ {
+ entry
+ } else {
+ self.set_entry(
+ index,
+ Entry::AssistantMessage(AssistantMessageEntry::default()),
+ );
+ let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
+ unreachable!()
+ };
+ entry
+ };
+ entry.sync(message);
}
};
}
@@ -177,7 +189,7 @@ impl EntryViewState {
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
- Entry::UserMessage { .. } => {}
+ Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
Entry::Content(response_views) => {
for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::() {
@@ -208,17 +220,44 @@ pub enum ViewEvent {
MessageEditorEvent(Entity, MessageEditorEvent),
}
+#[derive(Default, Debug)]
+pub struct AssistantMessageEntry {
+ scroll_handles_by_chunk_index: HashMap,
+}
+
+impl AssistantMessageEntry {
+ pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option {
+ self.scroll_handles_by_chunk_index.get(&ix).cloned()
+ }
+
+ pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
+ if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
+ let ix = message.chunks.len() - 1;
+ let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
+ handle.scroll_to_bottom();
+ }
+ }
+}
+
#[derive(Debug)]
pub enum Entry {
UserMessage(Entity),
+ AssistantMessage(AssistantMessageEntry),
Content(HashMap),
}
impl Entry {
+ pub fn focus_handle(&self, cx: &App) -> Option {
+ match self {
+ Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)),
+ Self::AssistantMessage(_) | Self::Content(_) => None,
+ }
+ }
+
pub fn message_editor(&self) -> Option<&Entity> {
match self {
Self::UserMessage(editor) => Some(editor),
- Entry::Content(_) => None,
+ Self::AssistantMessage(_) | Self::Content(_) => None,
}
}
@@ -239,6 +278,16 @@ impl Entry {
.map(|entity| entity.downcast::().unwrap())
}
+ pub fn scroll_handle_for_assistant_message_chunk(
+ &self,
+ chunk_ix: usize,
+ ) -> Option {
+ match self {
+ Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
+ Self::UserMessage(_) | Self::Content(_) => None,
+ }
+ }
+
fn content_map(&self) -> Option<&HashMap> {
match self {
Self::Content(map) => Some(map),
@@ -254,7 +303,7 @@ impl Entry {
pub fn has_content(&self) -> bool {
match self {
Self::Content(map) => !map.is_empty(),
- Self::UserMessage(_) => false,
+ Self::UserMessage(_) | Self::AssistantMessage(_) => false,
}
}
}
diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs
index 44403d5ffaceac466bff4c69d339e8194c6395fe..0b6a856f70b9837b146d6f89a6de60d9b0088db1 100644
--- a/crates/agent_ui/src/acp/message_editor.rs
+++ b/crates/agent_ui/src/acp/message_editor.rs
@@ -4,7 +4,7 @@ use crate::{
};
use acp_thread::{MentionUri, selection_name};
use agent_client_protocol as acp;
-use agent_servers::AgentServer;
+use agent_servers::{AgentServer, AgentServerDelegate};
use agent2::HistoryStore;
use anyhow::{Result, anyhow};
use assistant_slash_commands::codeblock_fence_for_path;
@@ -648,7 +648,8 @@ impl MessageEditor {
self.project.read(cx).fs().clone(),
self.history_store.clone(),
));
- let connection = server.connect(Path::new(""), &self.project, cx);
+ let delegate = AgentServerDelegate::new(self.project.clone(), watch::channel("".into()).0);
+ let connection = server.connect(Path::new(""), delegate, cx);
cx.spawn(async move |_, cx| {
let agent = connection.await?;
let agent = agent.downcast::().unwrap();
diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs
index 77c88c461d6e6fadcefd8eb7319bbbe2ff05fef4..cbb513696d88bbfcd95e15e051fc69322fd11281 100644
--- a/crates/agent_ui/src/acp/model_selector.rs
+++ b/crates/agent_ui/src/acp/model_selector.rs
@@ -73,11 +73,8 @@ impl AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.models = models.ok();
this.delegate.selected_model = selected_model.ok();
- this.delegate.update_matches(this.query(cx), window, cx)
- })?
- .await;
-
- Ok(())
+ this.refresh(window, cx)
+ })
}
refresh(&this, &session_id, cx).await.log_err();
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index e4f6eaf5f693306b1ff8415ee6171d59d63b8a11..ed3d9ef45d76c540c40fcf6146bab0056cf6e83a 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/crates/agent_ui/src/acp/thread_view.rs
@@ -6,10 +6,10 @@ use acp_thread::{
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
use agent_client_protocol::{self as acp, PromptCapabilities};
-use agent_servers::{AgentServer, ClaudeCode};
+use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
-use anyhow::bail;
+use anyhow::{Result, anyhow, bail};
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
use client::zed_urls;
@@ -18,13 +18,14 @@ use editor::scroll::Autoscroll;
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
use file_icons::FileIcons;
use fs::Fs;
+use futures::FutureExt as _;
use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
- EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
- ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription,
- Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window,
- WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point,
- prelude::*, pulsating_between,
+ CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
+ ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
+ Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
+ Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
+ point, prelude::*, pulsating_between,
};
use language::Buffer;
@@ -39,6 +40,8 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use std::{collections::BTreeMap, rc::Rc, time::Duration};
+use task::SpawnInTerminal;
+use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme::ThemeSettings;
use ui::{
@@ -66,7 +69,6 @@ use crate::{
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
};
-const RESPONSE_PADDING_X: Pixels = px(19.);
pub const MIN_EDITOR_LINES: usize = 4;
pub const MAX_EDITOR_LINES: usize = 8;
@@ -94,6 +96,10 @@ impl ThreadError {
error.downcast_ref::()
{
Self::ModelRequestLimitReached(error.plan)
+ } else if let Some(acp_error) = error.downcast_ref::()
+ && acp_error.code == acp::ErrorCode::AUTH_REQUIRED.code
+ {
+ Self::AuthenticationRequired(acp_error.message.clone().into())
} else {
let string = error.to_string();
// TODO: we should have Gemini return better errors here.
@@ -284,9 +290,7 @@ pub struct AcpThreadView {
}
enum ThreadState {
- Loading {
- _task: Task<()>,
- },
+ Loading(Entity),
Ready {
thread: Entity,
title_editor: Option>,
@@ -302,6 +306,12 @@ enum ThreadState {
},
}
+struct LoadingView {
+ title: SharedString,
+ _load_task: Task<()>,
+ _update_title_task: Task>,
+}
+
impl AcpThreadView {
pub fn new(
agent: Rc,
@@ -412,8 +422,10 @@ impl AcpThreadView {
.next()
.map(|worktree| worktree.read(cx).abs_path())
.unwrap_or_else(|| paths::home_dir().as_path().into());
+ let (tx, mut rx) = watch::channel("Loading…".into());
+ let delegate = AgentServerDelegate::new(project.clone(), tx);
- let connect_task = agent.connect(&root_dir, &project, cx);
+ let connect_task = agent.connect(&root_dir, delegate, cx);
let load_task = cx.spawn_in(window, async move |this, cx| {
let connection = match connect_task.await {
Ok(connection) => connection,
@@ -478,11 +490,14 @@ impl AcpThreadView {
.set(thread.read(cx).prompt_capabilities());
let count = thread.read(cx).entries().len();
- this.list_state.splice(0..0, count);
this.entry_view_state.update(cx, |view_state, cx| {
for ix in 0..count {
view_state.sync_entry(ix, &thread, window, cx);
}
+ this.list_state.splice_focusable(
+ 0..0,
+ (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)),
+ );
});
if let Some(resume) = resume_thread {
@@ -563,7 +578,25 @@ impl AcpThreadView {
.log_err();
});
- ThreadState::Loading { _task: load_task }
+ let loading_view = cx.new(|cx| {
+ let update_title_task = cx.spawn(async move |this, cx| {
+ loop {
+ let status = rx.recv().await?;
+ this.update(cx, |this: &mut LoadingView, cx| {
+ this.title = status;
+ cx.notify();
+ })?;
+ }
+ });
+
+ LoadingView {
+ title: "Loading…".into(),
+ _load_task: load_task,
+ _update_title_task: update_title_task,
+ }
+ });
+
+ ThreadState::Loading(loading_view)
}
fn handle_auth_required(
@@ -663,11 +696,18 @@ impl AcpThreadView {
}
}
- pub fn title(&self) -> SharedString {
+ pub fn title(&self, cx: &App) -> SharedString {
match &self.thread_state {
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
- ThreadState::Loading { .. } => "Loading…".into(),
- ThreadState::LoadError(_) => "Failed to load".into(),
+ ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(),
+ ThreadState::LoadError(error) => match error {
+ LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
+ LoadError::FailedToInstall(_) => {
+ format!("Failed to Install {}", self.agent.name()).into()
+ }
+ LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
+ LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
+ },
}
}
@@ -889,7 +929,7 @@ impl AcpThreadView {
fn send_impl(
&mut self,
- contents: Task, Vec>)>>,
+ contents: Task, Vec>)>>,
window: &mut Window,
cx: &mut Context,
) {
@@ -899,9 +939,10 @@ impl AcpThreadView {
self.editing_message.take();
self.thread_feedback.clear();
- let Some(thread) = self.thread().cloned() else {
+ let Some(thread) = self.thread() else {
return;
};
+ let thread = thread.downgrade();
if self.should_be_following {
self.workspace
.update(cx, |workspace, cx| {
@@ -1109,9 +1150,14 @@ impl AcpThreadView {
let len = thread.read(cx).entries().len();
let index = len - 1;
self.entry_view_state.update(cx, |view_state, cx| {
- view_state.sync_entry(index, thread, window, cx)
+ view_state.sync_entry(index, thread, window, cx);
+ self.list_state.splice_focusable(
+ index..index,
+ [view_state
+ .entry(index)
+ .and_then(|entry| entry.focus_handle(cx))],
+ );
});
- self.list_state.splice(index..index, 1);
}
AcpThreadEvent::EntryUpdated(index) => {
self.entry_view_state.update(cx, |view_state, cx| {
@@ -1219,6 +1265,31 @@ impl AcpThreadView {
});
return;
}
+ } else if method.0.as_ref() == "anthropic-api-key" {
+ let registry = LanguageModelRegistry::global(cx);
+ let provider = registry
+ .read(cx)
+ .provider(&language_model::ANTHROPIC_PROVIDER_ID)
+ .unwrap();
+ if !provider.is_authenticated(cx) {
+ let this = cx.weak_entity();
+ let agent = self.agent.clone();
+ let connection = connection.clone();
+ window.defer(cx, |window, cx| {
+ Self::handle_auth_required(
+ this,
+ AuthRequired {
+ description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
+ provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
+ },
+ agent,
+ connection,
+ window,
+ cx,
+ );
+ });
+ return;
+ }
} else if method.0.as_ref() == "vertex-ai"
&& std::env::var("GOOGLE_API_KEY").is_err()
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -1250,7 +1321,15 @@ impl AcpThreadView {
self.thread_error.take();
configuration_view.take();
pending_auth_method.replace(method.clone());
- let authenticate = connection.authenticate(method, cx);
+ let authenticate = if method.0.as_ref() == "claude-login" {
+ if let Some(workspace) = self.workspace.upgrade() {
+ Self::spawn_claude_login(&workspace, window, cx)
+ } else {
+ Task::ready(Ok(()))
+ }
+ } else {
+ connection.authenticate(method, cx)
+ };
cx.notify();
self.auth_task =
Some(cx.spawn_in(window, {
@@ -1274,6 +1353,13 @@ impl AcpThreadView {
this.update_in(cx, |this, window, cx| {
if let Err(err) = result {
+ if let ThreadState::Unauthenticated {
+ pending_auth_method,
+ ..
+ } = &mut this.thread_state
+ {
+ pending_auth_method.take();
+ }
this.handle_thread_error(err, cx);
} else {
this.thread_state = Self::initial_state(
@@ -1292,6 +1378,76 @@ impl AcpThreadView {
}));
}
+ fn spawn_claude_login(
+ workspace: &Entity,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Task> {
+ let Some(terminal_panel) = workspace.read(cx).panel::(cx) else {
+ return Task::ready(Ok(()));
+ };
+ let project = workspace.read(cx).project().read(cx);
+ let cwd = project.first_project_directory(cx);
+ let shell = project.terminal_settings(&cwd, cx).shell.clone();
+
+ let terminal = terminal_panel.update(cx, |terminal_panel, cx| {
+ terminal_panel.spawn_task(
+ &SpawnInTerminal {
+ id: task::TaskId("claude-login".into()),
+ full_label: "claude /login".to_owned(),
+ label: "claude /login".to_owned(),
+ command: Some("claude".to_owned()),
+ args: vec!["/login".to_owned()],
+ command_label: "claude /login".to_owned(),
+ cwd,
+ use_new_terminal: true,
+ allow_concurrent_runs: true,
+ hide: task::HideStrategy::Always,
+ shell,
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
+ });
+ cx.spawn(async move |cx| {
+ let terminal = terminal.await?;
+ let mut exit_status = terminal
+ .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
+ .fuse();
+
+ let logged_in = cx
+ .spawn({
+ let terminal = terminal.clone();
+ async move |cx| {
+ loop {
+ cx.background_executor().timer(Duration::from_secs(1)).await;
+ let content =
+ terminal.update(cx, |terminal, _cx| terminal.get_content())?;
+ if content.contains("Login successful") {
+ return anyhow::Ok(());
+ }
+ }
+ }
+ })
+ .fuse();
+ futures::pin_mut!(logged_in);
+ futures::select_biased! {
+ result = logged_in => {
+ if let Err(e) = result {
+ log::error!("{e}");
+ return Err(anyhow!("exited before logging in"));
+ }
+ }
+ _ = exit_status => {
+ return Err(anyhow!("exited before logging in"));
+ }
+ }
+ terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
+ Ok(())
+ })
+ }
+
fn authorize_tool_call(
&mut self,
tool_call_id: acp::ToolCallId,
@@ -1367,14 +1523,14 @@ impl AcpThreadView {
.id(("user_message", entry_ix))
.map(|this| {
if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() {
- this.pt_4()
+ this.pt(rems_from_px(18.))
} else if rules_item.is_some() {
this.pt_3()
} else {
this.pt_2()
}
})
- .pb_4()
+ .pb_3()
.px_2()
.gap_1p5()
.w_full()
@@ -1493,10 +1649,12 @@ impl AcpThreadView {
.into_any()
}
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
+ let is_last = entry_ix + 1 == total_entries;
+
let style = default_markdown_style(false, false, window, cx);
let message_body = v_flex()
.w_full()
- .gap_2p5()
+ .gap_3()
.children(chunks.iter().enumerate().filter_map(
|(chunk_ix, chunk)| match chunk {
AssistantMessageChunk::Message { block } => {
@@ -1523,8 +1681,8 @@ impl AcpThreadView {
v_flex()
.px_5()
- .py_1()
- .when(entry_ix + 1 == total_entries, |this| this.pb_4())
+ .py_1p5()
+ .when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(message_body)
@@ -1533,7 +1691,7 @@ impl AcpThreadView {
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
- div().w_full().py_1().px_5().map(|this| {
+ div().w_full().map(|this| {
if has_terminals {
this.children(tool_call.terminals().map(|terminal| {
self.render_terminal_tool_call(
@@ -1614,59 +1772,72 @@ impl AcpThreadView {
) -> AnyElement {
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-card-header");
+
let key = (entry_ix, chunk_ix);
+
let is_open = self.expanded_thinking_blocks.contains(&key);
+ let scroll_handle = self
+ .entry_view_state
+ .read(cx)
+ .entry(entry_ix)
+ .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
+
+ let thinking_content = {
+ div()
+ .id(("thinking-content", chunk_ix))
+ .when_some(scroll_handle, |this, scroll_handle| {
+ this.track_scroll(&scroll_handle)
+ })
+ .text_ui_sm(cx)
+ .overflow_hidden()
+ .child(
+ self.render_markdown(chunk, default_markdown_style(false, false, window, cx)),
+ )
+ };
+
v_flex()
+ .gap_1()
.child(
h_flex()
.id(header_id)
.group(&card_header_id)
.relative()
.w_full()
- .gap_1p5()
+ .pr_1()
+ .justify_between()
.child(
h_flex()
- .size_4()
- .justify_center()
+ .h(window.line_height() - px(2.))
+ .gap_1p5()
+ .overflow_hidden()
.child(
- div()
- .group_hover(&card_header_id, |s| s.invisible().w_0())
- .child(
- Icon::new(IconName::ToolThink)
- .size(IconSize::Small)
- .color(Color::Muted),
- ),
+ Icon::new(IconName::ToolThink)
+ .size(IconSize::Small)
+ .color(Color::Muted),
)
.child(
- h_flex()
- .absolute()
- .inset_0()
- .invisible()
- .justify_center()
- .group_hover(&card_header_id, |s| s.visible())
- .child(
- Disclosure::new(("expand", entry_ix), is_open)
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronRight)
- .on_click(cx.listener({
- move |this, _event, _window, cx| {
- if is_open {
- this.expanded_thinking_blocks.remove(&key);
- } else {
- this.expanded_thinking_blocks.insert(key);
- }
- cx.notify();
- }
- })),
- ),
+ div()
+ .text_size(self.tool_name_font_size())
+ .text_color(cx.theme().colors().text_muted)
+ .child("Thinking"),
),
)
.child(
- div()
- .text_size(self.tool_name_font_size())
- .text_color(cx.theme().colors().text_muted)
- .child("Thinking"),
+ Disclosure::new(("expand", entry_ix), is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown)
+ .visible_on_hover(&card_header_id)
+ .on_click(cx.listener({
+ move |this, _event, _window, cx| {
+ if is_open {
+ this.expanded_thinking_blocks.remove(&key);
+ } else {
+ this.expanded_thinking_blocks.insert(key);
+ }
+ cx.notify();
+ }
+ })),
)
.on_click(cx.listener({
move |this, _event, _window, cx| {
@@ -1682,17 +1853,11 @@ impl AcpThreadView {
.when(is_open, |this| {
this.child(
div()
- .relative()
- .mt_1p5()
- .ml(px(7.))
- .pl_4()
+ .ml_1p5()
+ .pl_3p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
- .text_ui_sm(cx)
- .child(self.render_markdown(
- chunk,
- default_markdown_style(false, false, window, cx),
- )),
+ .child(thinking_content),
)
})
.into_any_element()
@@ -1705,7 +1870,6 @@ impl AcpThreadView {
window: &Window,
cx: &Context,
) -> Div {
- let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-tool-call-header");
let tool_icon =
@@ -1734,11 +1898,7 @@ impl AcpThreadView {
_ => false,
};
- let failed_tool_call = matches!(
- tool_call.status,
- ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
- );
-
+ let has_location = tool_call.locations.len() == 1;
let needs_confirmation = matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
@@ -1751,46 +1911,56 @@ impl AcpThreadView {
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
- let gradient_overlay = |color: Hsla| {
+ let gradient_overlay = {
div()
.absolute()
.top_0()
.right_0()
.w_12()
.h_full()
- .bg(linear_gradient(
- 90.,
- linear_color_stop(color, 1.),
- linear_color_stop(color.opacity(0.2), 0.),
- ))
- };
- let gradient_color = if use_card_layout {
- self.tool_card_header_bg(cx)
- } else {
- cx.theme().colors().panel_background
+ .map(|this| {
+ if use_card_layout {
+ this.bg(linear_gradient(
+ 90.,
+ linear_color_stop(self.tool_card_header_bg(cx), 1.),
+ linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
+ ))
+ } else {
+ this.bg(linear_gradient(
+ 90.,
+ linear_color_stop(cx.theme().colors().panel_background, 1.),
+ linear_color_stop(
+ cx.theme().colors().panel_background.opacity(0.2),
+ 0.,
+ ),
+ ))
+ }
+ })
};
let tool_output_display = if is_open {
match &tool_call.status {
- ToolCallStatus::WaitingForConfirmation { options, .. } => {
- v_flex()
- .w_full()
- .children(tool_call.content.iter().map(|content| {
- div()
- .child(self.render_tool_call_content(
- entry_ix, content, tool_call, window, cx,
- ))
- .into_any_element()
- }))
- .child(self.render_permission_buttons(
- options,
- entry_ix,
- tool_call.id.clone(),
- tool_call.content.is_empty(),
- cx,
- ))
- .into_any()
- }
+ ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
+ .w_full()
+ .children(tool_call.content.iter().map(|content| {
+ div()
+ .child(self.render_tool_call_content(
+ entry_ix,
+ content,
+ tool_call,
+ use_card_layout,
+ window,
+ cx,
+ ))
+ .into_any_element()
+ }))
+ .child(self.render_permission_buttons(
+ options,
+ entry_ix,
+ tool_call.id.clone(),
+ cx,
+ ))
+ .into_any(),
ToolCallStatus::Pending | ToolCallStatus::InProgress
if is_edit
&& tool_call.content.is_empty()
@@ -1805,9 +1975,14 @@ impl AcpThreadView {
| ToolCallStatus::Canceled => v_flex()
.w_full()
.children(tool_call.content.iter().map(|content| {
- div().child(
- self.render_tool_call_content(entry_ix, content, tool_call, window, cx),
- )
+ div().child(self.render_tool_call_content(
+ entry_ix,
+ content,
+ tool_call,
+ use_card_layout,
+ window,
+ cx,
+ ))
}))
.into_any(),
ToolCallStatus::Rejected => Empty.into_any(),
@@ -1818,30 +1993,36 @@ impl AcpThreadView {
};
v_flex()
- .when(use_card_layout, |this| {
- this.rounded_md()
- .border_1()
- .border_color(self.tool_card_border_color(cx))
- .bg(cx.theme().colors().editor_background)
- .overflow_hidden()
+ .map(|this| {
+ if use_card_layout {
+ this.my_1p5()
+ .rounded_md()
+ .border_1()
+ .border_color(self.tool_card_border_color(cx))
+ .bg(cx.theme().colors().editor_background)
+ .overflow_hidden()
+ } else {
+ this.my_1()
+ }
+ })
+ .map(|this| {
+ if has_location && !use_card_layout {
+ this.ml_4()
+ } else {
+ this.ml_5()
+ }
})
+ .mr_5()
.child(
h_flex()
- .id(header_id)
.group(&card_header_id)
.relative()
.w_full()
- .max_w_full()
.gap_1()
+ .justify_between()
.when(use_card_layout, |this| {
- this.pl_1p5()
- .pr_1()
- .py_0p5()
- .rounded_t_md()
- .when(is_open && !failed_tool_call, |this| {
- this.border_b_1()
- .border_color(self.tool_card_border_color(cx))
- })
+ this.p_0p5()
+ .rounded_t(rems_from_px(5.))
.bg(self.tool_card_header_bg(cx))
})
.child(
@@ -1850,8 +2031,16 @@ impl AcpThreadView {
.w_full()
.h(window.line_height() - px(2.))
.text_size(self.tool_name_font_size())
+ .gap_1p5()
+ .when(has_location || use_card_layout, |this| this.px_1())
+ .when(has_location, |this| {
+ this.cursor(CursorStyle::PointingHand)
+ .rounded(rems_from_px(3.)) // Concentric border radius
+ .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
+ })
+ .overflow_hidden()
.child(tool_icon)
- .child(if tool_call.locations.len() == 1 {
+ .child(if has_location {
let name = tool_call.locations[0]
.path
.file_name()
@@ -1862,13 +2051,6 @@ impl AcpThreadView {
h_flex()
.id(("open-tool-call-location", entry_ix))
.w_full()
- .max_w_full()
- .px_1p5()
- .rounded_sm()
- .overflow_x_scroll()
- .hover(|label| {
- label.bg(cx.theme().colors().element_hover.opacity(0.5))
- })
.map(|this| {
if use_card_layout {
this.text_color(cx.theme().colors().text)
@@ -1878,31 +2060,28 @@ impl AcpThreadView {
})
.child(name)
.tooltip(Tooltip::text("Jump to File"))
- .cursor(gpui::CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
}))
.into_any_element()
} else {
h_flex()
- .relative()
.w_full()
- .max_w_full()
- .ml_1p5()
- .overflow_hidden()
- .child(h_flex().pr_8().child(self.render_markdown(
+ .child(self.render_markdown(
tool_call.label.clone(),
default_markdown_style(false, true, window, cx),
- )))
- .child(gradient_overlay(gradient_color))
+ ))
.into_any()
- }),
+ })
+ .when(!has_location, |this| this.child(gradient_overlay)),
)
- .child(
- h_flex()
- .gap_px()
- .when(is_collapsible, |this| {
- this.child(
+ .when(is_collapsible || failed_or_canceled, |this| {
+ this.child(
+ h_flex()
+ .px_1()
+ .gap_px()
+ .when(is_collapsible, |this| {
+ this.child(
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
@@ -1919,15 +2098,16 @@ impl AcpThreadView {
}
})),
)
- })
- .when(failed_or_canceled, |this| {
- this.child(
- Icon::new(IconName::Close)
- .color(Color::Error)
- .size(IconSize::Small),
- )
- }),
- ),
+ })
+ .when(failed_or_canceled, |this| {
+ this.child(
+ Icon::new(IconName::Close)
+ .color(Color::Error)
+ .size(IconSize::Small),
+ )
+ }),
+ )
+ }),
)
.children(tool_output_display)
}
@@ -1937,6 +2117,7 @@ impl AcpThreadView {
entry_ix: usize,
content: &ToolCallContent,
tool_call: &ToolCall,
+ card_layout: bool,
window: &Window,
cx: &Context,
) -> AnyElement {
@@ -1945,7 +2126,13 @@ impl AcpThreadView {
if let Some(resource_link) = content.resource_link() {
self.render_resource_link(resource_link, cx)
} else if let Some(markdown) = content.markdown() {
- self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx)
+ self.render_markdown_output(
+ markdown.clone(),
+ tool_call.id.clone(),
+ card_layout,
+ window,
+ cx,
+ )
} else {
Empty.into_any_element()
}
@@ -1961,6 +2148,7 @@ impl AcpThreadView {
&self,
markdown: Entity,
tool_call_id: acp::ToolCallId,
+ card_layout: bool,
window: &Window,
cx: &Context,
) -> AnyElement {
@@ -1968,26 +2156,35 @@ impl AcpThreadView {
v_flex()
.mt_1p5()
- .ml(px(7.))
- .px_3p5()
.gap_2()
- .border_l_1()
- .border_color(self.tool_card_border_color(cx))
+ .when(!card_layout, |this| {
+ this.ml(rems(0.4))
+ .px_3p5()
+ .border_l_1()
+ .border_color(self.tool_card_border_color(cx))
+ })
+ .when(card_layout, |this| {
+ this.p_2()
+ .border_t_1()
+ .border_color(self.tool_card_border_color(cx))
+ })
.text_sm()
.text_color(cx.theme().colors().text_muted)
.child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx)))
- .child(
- IconButton::new(button_id, IconName::ChevronUp)
- .full_width()
- .style(ButtonStyle::Outlined)
- .icon_color(Color::Muted)
- .on_click(cx.listener({
- move |this: &mut Self, _, _, cx: &mut Context| {
- this.expanded_tool_calls.remove(&tool_call_id);
- cx.notify();
- }
- })),
- )
+ .when(!card_layout, |this| {
+ this.child(
+ IconButton::new(button_id, IconName::ChevronUp)
+ .full_width()
+ .style(ButtonStyle::Outlined)
+ .icon_color(Color::Muted)
+ .on_click(cx.listener({
+ move |this: &mut Self, _, _, cx: &mut Context| {
+ this.expanded_tool_calls.remove(&tool_call_id);
+ cx.notify();
+ }
+ })),
+ )
+ })
.into_any_element()
}
@@ -2025,7 +2222,7 @@ impl AcpThreadView {
let button_id = SharedString::from(format!("item-{}", uri));
div()
- .ml(px(7.))
+ .ml(rems(0.4))
.pl_2p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
@@ -2055,7 +2252,6 @@ impl AcpThreadView {
options: &[acp::PermissionOption],
entry_ix: usize,
tool_call_id: acp::ToolCallId,
- empty_content: bool,
cx: &Context,
) -> Div {
h_flex()
@@ -2065,10 +2261,8 @@ impl AcpThreadView {
.gap_1()
.justify_between()
.flex_wrap()
- .when(!empty_content, |this| {
- this.border_t_1()
- .border_color(self.tool_card_border_color(cx))
- })
+ .border_t_1()
+ .border_color(self.tool_card_border_color(cx))
.child(
div()
.min_w(rems_from_px(145.))
@@ -2166,6 +2360,8 @@ impl AcpThreadView {
v_flex()
.h_full()
+ .border_t_1()
+ .border_color(self.tool_card_border_color(cx))
.child(
if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix)
&& let Some(editor) = entry.editor_for_diff(diff)
@@ -2213,6 +2409,12 @@ impl AcpThreadView {
started_at.elapsed()
};
+ let header_id =
+ SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
+ let header_group = SharedString::from(format!(
+ "terminal-tool-header-group-{}",
+ terminal.entity_id()
+ ));
let header_bg = cx
.theme()
.colors()
@@ -2228,10 +2430,7 @@ impl AcpThreadView {
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let header = h_flex()
- .id(SharedString::from(format!(
- "terminal-tool-header-{}",
- terminal.entity_id()
- )))
+ .id(header_id)
.flex_none()
.gap_1()
.justify_between()
@@ -2295,23 +2494,28 @@ impl AcpThreadView {
),
)
})
- .when(tool_failed || command_failed, |header| {
- header.child(
- div()
- .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
- .child(
- Icon::new(IconName::Close)
- .size(IconSize::Small)
- .color(Color::Error),
- )
- .when_some(output.and_then(|o| o.exit_status), |this, status| {
- this.tooltip(Tooltip::text(format!(
- "Exited with code {}",
- status.code().unwrap_or(-1),
- )))
- }),
+ .child(
+ Disclosure::new(
+ SharedString::from(format!(
+ "terminal-tool-disclosure-{}",
+ terminal.entity_id()
+ )),
+ is_expanded,
)
- })
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown)
+ .visible_on_hover(&header_group)
+ .on_click(cx.listener({
+ let id = tool_call.id.clone();
+ move |this, _event, _window, _cx| {
+ if is_expanded {
+ this.expanded_tool_calls.remove(&id);
+ } else {
+ this.expanded_tool_calls.insert(id.clone());
+ }
+ }
+ })),
+ )
.when(truncated_output, |header| {
let tooltip = if let Some(output) = output {
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
@@ -2354,26 +2558,23 @@ impl AcpThreadView {
.size(LabelSize::XSmall),
)
})
- .child(
- Disclosure::new(
- SharedString::from(format!(
- "terminal-tool-disclosure-{}",
- terminal.entity_id()
- )),
- is_expanded,
+ .when(tool_failed || command_failed, |header| {
+ header.child(
+ div()
+ .id(("terminal-tool-error-code-indicator", terminal.entity_id()))
+ .child(
+ Icon::new(IconName::Close)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .when_some(output.and_then(|o| o.exit_status), |this, status| {
+ this.tooltip(Tooltip::text(format!(
+ "Exited with code {}",
+ status.code().unwrap_or(-1),
+ )))
+ }),
)
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronDown)
- .on_click(cx.listener({
- let id = tool_call.id.clone();
- move |this, _event, _window, _cx| {
- if is_expanded {
- this.expanded_tool_calls.remove(&id);
- } else {
- this.expanded_tool_calls.insert(id.clone());
- }
- }})),
- );
+ });
let terminal_view = self
.entry_view_state
@@ -2383,7 +2584,8 @@ impl AcpThreadView {
let show_output = is_expanded && terminal_view.is_some();
v_flex()
- .mb_2()
+ .my_1p5()
+ .mx_5()
.border_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
@@ -2391,9 +2593,10 @@ impl AcpThreadView {
.overflow_hidden()
.child(
v_flex()
+ .group(&header_group)
.py_1p5()
- .pl_2()
.pr_1p5()
+ .pl_2()
.gap_0p5()
.bg(header_bg)
.text_xs()
@@ -2765,128 +2968,32 @@ impl AcpThreadView {
)
}
- fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement {
- let (message, action_slot) = match e {
- LoadError::NotInstalled {
- error_message,
- install_message,
- install_command,
- } => {
- let install_command = install_command.clone();
- let button = Button::new("install", install_message)
- .tooltip(Tooltip::text(install_command.clone()))
- .style(ButtonStyle::Outlined)
- .label_size(LabelSize::Small)
- .icon(IconName::Download)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(cx.listener(move |this, _, window, cx| {
- telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id());
-
- let task = this
- .workspace
- .update(cx, |workspace, cx| {
- let project = workspace.project().read(cx);
- let cwd = project.first_project_directory(cx);
- let shell = project.terminal_settings(&cwd, cx).shell.clone();
- let spawn_in_terminal = task::SpawnInTerminal {
- id: task::TaskId(install_command.clone()),
- full_label: install_command.clone(),
- label: install_command.clone(),
- command: Some(install_command.clone()),
- args: Vec::new(),
- command_label: install_command.clone(),
- cwd,
- env: Default::default(),
- use_new_terminal: true,
- allow_concurrent_runs: true,
- reveal: Default::default(),
- reveal_target: Default::default(),
- hide: Default::default(),
- shell,
- show_summary: true,
- show_command: true,
- show_rerun: false,
- };
- workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
- })
- .ok();
- let Some(task) = task else { return };
- cx.spawn_in(window, async move |this, cx| {
- if let Some(Ok(_)) = task.await {
- this.update_in(cx, |this, window, cx| {
- this.reset(window, cx);
- })
- .ok();
- }
- })
- .detach()
- }));
-
- (error_message.clone(), Some(button.into_any_element()))
- }
+ fn render_load_error(
+ &self,
+ e: &LoadError,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> AnyElement {
+ let (title, message, action_slot): (_, SharedString, _) = match e {
LoadError::Unsupported {
- error_message,
- upgrade_message,
- upgrade_command,
+ command: path,
+ current_version,
+ minimum_version,
} => {
- let upgrade_command = upgrade_command.clone();
- let button = Button::new("upgrade", upgrade_message)
- .tooltip(Tooltip::text(upgrade_command.clone()))
- .style(ButtonStyle::Outlined)
- .label_size(LabelSize::Small)
- .icon(IconName::Download)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(cx.listener(move |this, _, window, cx| {
- telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id());
-
- let task = this
- .workspace
- .update(cx, |workspace, cx| {
- let project = workspace.project().read(cx);
- let cwd = project.first_project_directory(cx);
- let shell = project.terminal_settings(&cwd, cx).shell.clone();
- let spawn_in_terminal = task::SpawnInTerminal {
- id: task::TaskId(upgrade_command.to_string()),
- full_label: upgrade_command.clone(),
- label: upgrade_command.clone(),
- command: Some(upgrade_command.clone()),
- args: Vec::new(),
- command_label: upgrade_command.clone(),
- cwd,
- env: Default::default(),
- use_new_terminal: true,
- allow_concurrent_runs: true,
- reveal: Default::default(),
- reveal_target: Default::default(),
- hide: Default::default(),
- shell,
- show_summary: true,
- show_command: true,
- show_rerun: false,
- };
- workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
- })
- .ok();
- let Some(task) = task else { return };
- cx.spawn_in(window, async move |this, cx| {
- if let Some(Ok(_)) = task.await {
- this.update_in(cx, |this, window, cx| {
- this.reset(window, cx);
- })
- .ok();
- }
- })
- .detach()
- }));
-
- (error_message.clone(), Some(button.into_any_element()))
+ return self.render_unsupported(path, current_version, minimum_version, window, cx);
}
- LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
+ LoadError::FailedToInstall(msg) => (
+ "Failed to Install",
+ msg.into(),
+ Some(self.create_copy_button(msg.to_string()).into_any_element()),
+ ),
+ LoadError::Exited { status } => (
+ "Failed to Launch",
+ format!("Server exited with status {status}").into(),
+ None,
+ ),
LoadError::Other(msg) => (
+ "Failed to Launch",
msg.into(),
Some(self.create_copy_button(msg.to_string()).into_any_element()),
),
@@ -2895,12 +3002,56 @@ impl AcpThreadView {
Callout::new()
.severity(Severity::Error)
.icon(IconName::XCircleFilled)
- .title("Failed to Launch")
+ .title(title)
.description(message)
.actions_slot(div().children(action_slot))
.into_any_element()
}
+ fn render_unsupported(
+ &self,
+ path: &SharedString,
+ version: &SharedString,
+ minimum_version: &SharedString,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) -> AnyElement {
+ let (heading_label, description_label) = (
+ format!("Upgrade {} to work with Zed", self.agent.name()),
+ if version.is_empty() {
+ format!(
+ "Currently using {}, which does not report a valid --version",
+ path,
+ )
+ } else {
+ format!(
+ "Currently using {}, which is only version {} (need at least {minimum_version})",
+ path, version
+ )
+ },
+ );
+
+ v_flex()
+ .w_full()
+ .p_3p5()
+ .gap_2p5()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .bg(linear_gradient(
+ 180.,
+ linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
+ linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
+ ))
+ .child(
+ v_flex().gap_0p5().child(Label::new(heading_label)).child(
+ Label::new(description_label)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .into_any_element()
+ }
+
fn render_activity_bar(
&self,
thread_entity: &Entity,
@@ -2920,7 +3071,12 @@ impl AcpThreadView {
let active_color = cx.theme().colors().element_selected;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
- let pending_edits = thread.has_pending_edit_tool_calls();
+ // Temporarily always enable ACP edit controls. This is temporary, to lessen the
+ // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
+ // be, which blocks you from being able to accept or reject edits. This switches the
+ // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't
+ // block you from using the panel.
+ let pending_edits = false;
v_flex()
.mt_1()
@@ -3283,7 +3439,6 @@ impl AcpThreadView {
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
- .relative()
.py_1()
.pl_2()
.pr_1()
@@ -3295,6 +3450,7 @@ impl AcpThreadView {
})
.child(
h_flex()
+ .relative()
.id(("file-name", index))
.pr_8()
.gap_1p5()
@@ -3302,6 +3458,16 @@ impl AcpThreadView {
.overflow_x_scroll()
.child(file_icon)
.child(h_flex().gap_0p5().children(file_name).children(file_path))
+ .child(
+ div()
+ .absolute()
+ .h_full()
+ .w_12()
+ .top_0()
+ .bottom_0()
+ .right_0()
+ .bg(overlay_gradient),
+ )
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
@@ -3362,17 +3528,6 @@ impl AcpThreadView {
}
}),
),
- )
- .child(
- div()
- .id("gradient-overlay")
- .absolute()
- .h_full()
- .w_12()
- .top_0()
- .bottom_0()
- .right(px(152.))
- .bg(overlay_gradient),
);
Some(element)
@@ -3932,7 +4087,7 @@ impl AcpThreadView {
workspace: Entity,
window: &mut Window,
cx: &mut App,
- ) -> Task> {
+ ) -> Task> {
let markdown_language_task = workspace
.read(cx)
.app_state()
@@ -4150,13 +4305,14 @@ impl AcpThreadView {
) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating {
- return h_flex().id("thread-controls-container").ml_1().child(
+ return h_flex().id("thread-controls-container").child(
div()
.py_2()
.px(rems_from_px(22.))
.child(SpinnerLabel::new().size(LabelSize::Small)),
);
}
+
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
@@ -4182,12 +4338,10 @@ impl AcpThreadView {
.id("thread-controls-container")
.group("thread-controls-container")
.w_full()
- .mr_1()
- .pt_1()
- .pb_2()
- .px(RESPONSE_PADDING_X)
+ .py_2()
+ .px_5()
.gap_px()
- .opacity(0.4)
+ .opacity(0.6)
.hover(|style| style.opacity(1.))
.flex_wrap()
.justify_end();
@@ -4198,56 +4352,50 @@ impl AcpThreadView {
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
{
let feedback = self.thread_feedback.feedback;
- container = container.child(
- div().visible_on_hover("thread-controls-container").child(
- Label::new(
- match feedback {
+
+ container = container
+ .child(
+ div().visible_on_hover("thread-controls-container").child(
+ Label::new(match feedback {
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
- Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.",
- None => "Rating the thread sends all of your current conversation to the Zed team.",
- }
- )
- .color(Color::Muted)
- .size(LabelSize::XSmall)
- .truncate(),
- ),
- ).child(
- h_flex()
- .child(
- IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
- .shape(ui::IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .icon_color(match feedback {
- Some(ThreadFeedback::Positive) => Color::Accent,
- _ => Color::Ignored,
- })
- .tooltip(Tooltip::text("Helpful Response"))
- .on_click(cx.listener(move |this, _, window, cx| {
- this.handle_feedback_click(
- ThreadFeedback::Positive,
- window,
- cx,
- );
- })),
- )
- .child(
- IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
- .shape(ui::IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .icon_color(match feedback {
- Some(ThreadFeedback::Negative) => Color::Accent,
- _ => Color::Ignored,
- })
- .tooltip(Tooltip::text("Not Helpful"))
- .on_click(cx.listener(move |this, _, window, cx| {
- this.handle_feedback_click(
- ThreadFeedback::Negative,
- window,
- cx,
- );
- })),
- )
- )
+ Some(ThreadFeedback::Negative) => {
+ "We appreciate your feedback and will use it to improve."
+ }
+ None => {
+ "Rating the thread sends all of your current conversation to the Zed team."
+ }
+ })
+ .color(Color::Muted)
+ .size(LabelSize::XSmall)
+ .truncate(),
+ ),
+ )
+ .child(
+ IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
+ .shape(ui::IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ .icon_color(match feedback {
+ Some(ThreadFeedback::Positive) => Color::Accent,
+ _ => Color::Ignored,
+ })
+ .tooltip(Tooltip::text("Helpful Response"))
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
+ })),
+ )
+ .child(
+ IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
+ .shape(ui::IconButtonShape::Square)
+ .icon_size(IconSize::Small)
+ .icon_color(match feedback {
+ Some(ThreadFeedback::Negative) => Color::Accent,
+ _ => Color::Ignored,
+ })
+ .tooltip(Tooltip::text("Not Helpful"))
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
+ })),
+ );
}
container.child(open_as_markdown).child(scroll_to_top)
@@ -4792,18 +4940,6 @@ impl AcpThreadView {
}))
}
- fn reset(&mut self, window: &mut Window, cx: &mut Context) {
- self.thread_state = Self::initial_state(
- self.agent.clone(),
- None,
- self.workspace.clone(),
- self.project.clone(),
- window,
- cx,
- );
- cx.notify();
- }
-
pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context) {
let task = match entry {
HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| {
@@ -4879,7 +5015,7 @@ impl Render for AcpThreadView {
.size_full()
.items_center()
.justify_end()
- .child(self.render_load_error(e, cx)),
+ .child(self.render_load_error(e, window, cx)),
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
if has_messages {
this.child(
@@ -5332,18 +5468,10 @@ pub(crate) mod tests {
"Test".into()
}
- fn empty_state_headline(&self) -> SharedString {
- "Test".into()
- }
-
- fn empty_state_message(&self) -> SharedString {
- "Test".into()
- }
-
fn connect(
&self,
_root_dir: &Path,
- _project: &Entity,
+ _delegate: AgentServerDelegate,
_cx: &mut App,
) -> Task>> {
Task::ready(Ok(Rc::new(self.connection.clone())))
diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
index 52fb7eed4b96e2ff92097f415276ffeceb4fa11d..23b6e69a56886ca2e5d7c4bdbd27ee8fb1307629 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -3,20 +3,23 @@ mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
-use std::{sync::Arc, time::Duration};
+use std::{ops::Range, sync::Arc, time::Duration};
-use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
+use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
use agent_settings::AgentSettings;
+use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan;
use collections::HashMap;
use context_server::ContextServerId;
+use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
- Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
- Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
+ Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
+ EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
+ WeakEntity, percentage,
};
use language::LanguageRegistry;
use language_model::{
@@ -24,7 +27,6 @@ use language_model::{
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
- Project,
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
@@ -34,7 +36,7 @@ use ui::{
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
};
use util::ResultExt as _;
-use workspace::Workspace;
+use workspace::{Workspace, create_and_open_local_file};
use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
@@ -49,7 +51,6 @@ pub struct AgentConfiguration {
fs: Arc,
language_registry: Arc,
workspace: WeakEntity,
- project: WeakEntity,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap,
context_server_store: Entity,
@@ -59,7 +60,6 @@ pub struct AgentConfiguration {
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
- gemini_is_installed: bool,
_check_for_gemini: Task<()>,
}
@@ -70,7 +70,6 @@ impl AgentConfiguration {
tools: Entity,
language_registry: Arc,
workspace: WeakEntity,
- project: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> Self {
@@ -95,11 +94,6 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach();
- cx.observe_global_in::(window, |this, _, cx| {
- this.check_for_gemini(cx);
- cx.notify();
- })
- .detach();
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
@@ -108,7 +102,6 @@ impl AgentConfiguration {
fs,
language_registry,
workspace,
- project,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_store,
@@ -118,11 +111,9 @@ impl AgentConfiguration {
_registry_subscription: registry_subscription,
scroll_handle,
scrollbar_state,
- gemini_is_installed: false,
_check_for_gemini: Task::ready(()),
};
this.build_provider_configuration_views(window, cx);
- this.check_for_gemini(cx);
this
}
@@ -152,34 +143,6 @@ impl AgentConfiguration {
self.configuration_views_by_provider
.insert(provider.id(), configuration_view);
}
-
- fn check_for_gemini(&mut self, cx: &mut Context) {
- let project = self.project.clone();
- let settings = AllAgentServersSettings::get_global(cx).clone();
- self._check_for_gemini = cx.spawn({
- async move |this, cx| {
- let Some(project) = project.upgrade() else {
- return;
- };
- let gemini_is_installed = AgentServerCommand::resolve(
- Gemini::binary_name(),
- &[],
- // TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
- None,
- settings.gemini,
- &project,
- cx,
- )
- .await
- .is_some();
- this.update(cx, |this, cx| {
- this.gemini_is_installed = gemini_is_installed;
- cx.notify();
- })
- .ok();
- }
- });
- }
}
impl Focusable for AgentConfiguration {
@@ -1038,9 +1001,8 @@ impl AgentConfiguration {
name.clone(),
ExternalAgent::Custom {
name: name.clone(),
- settings: settings.clone(),
+ command: settings.command.clone(),
},
- None,
cx,
)
.into_any_element()
@@ -1058,10 +1020,39 @@ impl AgentConfiguration {
.child(
v_flex()
.gap_0p5()
- .child(Headline::new("External Agents"))
+ .child(
+ h_flex()
+ .w_full()
+ .gap_2()
+ .justify_between()
+ .child(Headline::new("External Agents"))
+ .child(
+ Button::new("add-agent", "Add Agent")
+ .icon_position(IconPosition::Start)
+ .icon(IconName::Plus)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .label_size(LabelSize::Small)
+ .on_click(
+ move |_, window, cx| {
+ if let Some(workspace) = window.root().flatten() {
+ let workspace = workspace.downgrade();
+ window
+ .spawn(cx, async |cx| {
+ open_new_agent_servers_entry_in_settings_editor(
+ workspace,
+ cx,
+ ).await
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+ ),
+ )
+ )
.child(
Label::new(
- "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
+ "Bring the agent of your choice to Zed via our new Agent Client Protocol.",
)
.color(Color::Muted),
),
@@ -1070,7 +1061,6 @@ impl AgentConfiguration {
IconName::AiGemini,
"Gemini CLI",
ExternalAgent::Gemini,
- (!self.gemini_is_installed).then_some(Gemini::install_command().into()),
cx,
))
// TODO add CC
@@ -1083,7 +1073,6 @@ impl AgentConfiguration {
icon: IconName,
name: impl Into,
agent: ExternalAgent,
- install_command: Option,
cx: &mut Context,
) -> impl IntoElement {
let name = name.into();
@@ -1103,88 +1092,28 @@ impl AgentConfiguration {
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.clone())),
)
- .map(|this| {
- if let Some(install_command) = install_command {
- this.child(
- Button::new(
- SharedString::from(format!("install_external_agent-{name}")),
- "Install Agent",
- )
- .label_size(LabelSize::Small)
- .icon(IconName::Plus)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .tooltip(Tooltip::text(install_command.clone()))
- .on_click(cx.listener(
- move |this, _, window, cx| {
- let Some(project) = this.project.upgrade() else {
- return;
- };
- let Some(workspace) = this.workspace.upgrade() else {
- return;
- };
- let cwd = project.read(cx).first_project_directory(cx);
- let shell =
- project.read(cx).terminal_settings(&cwd, cx).shell.clone();
- let spawn_in_terminal = task::SpawnInTerminal {
- id: task::TaskId(install_command.to_string()),
- full_label: install_command.to_string(),
- label: install_command.to_string(),
- command: Some(install_command.to_string()),
- args: Vec::new(),
- command_label: install_command.to_string(),
- cwd,
- env: Default::default(),
- use_new_terminal: true,
- allow_concurrent_runs: true,
- reveal: Default::default(),
- reveal_target: Default::default(),
- hide: Default::default(),
- shell,
- show_summary: true,
- show_command: true,
- show_rerun: false,
- };
- let task = workspace.update(cx, |workspace, cx| {
- workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
- });
- cx.spawn(async move |this, cx| {
- task.await;
- this.update(cx, |this, cx| {
- this.check_for_gemini(cx);
- })
- .ok();
- })
- .detach();
- },
- )),
- )
- } else {
- this.child(
- h_flex().gap_1().child(
- Button::new(
- SharedString::from(format!("start_acp_thread-{name}")),
- "Start New Thread",
- )
- .label_size(LabelSize::Small)
- .icon(IconName::Thread)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .on_click(move |_, window, cx| {
- window.dispatch_action(
- NewExternalAgentThread {
- agent: Some(agent.clone()),
- }
- .boxed_clone(),
- cx,
- );
- }),
- ),
+ .child(
+ h_flex().gap_1().child(
+ Button::new(
+ SharedString::from(format!("start_acp_thread-{name}")),
+ "Start New Thread",
)
- }
- })
+ .label_size(LabelSize::Small)
+ .icon(IconName::Thread)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(
+ NewExternalAgentThread {
+ agent: Some(agent.clone()),
+ }
+ .boxed_clone(),
+ cx,
+ );
+ }),
+ ),
+ )
}
}
@@ -1324,3 +1253,109 @@ fn show_unable_to_uninstall_extension_with_context_server(
workspace.toggle_status_toast(status_toast, cx);
}
+
+async fn open_new_agent_servers_entry_in_settings_editor(
+ workspace: WeakEntity,
+ cx: &mut AsyncWindowContext,
+) -> Result<()> {
+ let settings_editor = workspace
+ .update_in(cx, |_, window, cx| {
+ create_and_open_local_file(paths::settings_file(), window, cx, || {
+ settings::initial_user_settings_content().as_ref().into()
+ })
+ })?
+ .await?
+ .downcast::()
+ .unwrap();
+
+ settings_editor
+ .downgrade()
+ .update_in(cx, |item, window, cx| {
+ let text = item.buffer().read(cx).snapshot(cx).text();
+
+ let settings = cx.global::();
+
+ let mut unique_server_name = None;
+ let edits = settings.edits_for_update::(&text, |file| {
+ let server_name: Option = (0..u8::MAX)
+ .map(|i| {
+ if i == 0 {
+ "your_agent".into()
+ } else {
+ format!("your_agent_{}", i).into()
+ }
+ })
+ .find(|name| !file.custom.contains_key(name));
+ if let Some(server_name) = server_name {
+ unique_server_name = Some(server_name.clone());
+ file.custom.insert(
+ server_name,
+ CustomAgentServerSettings {
+ command: AgentServerCommand {
+ path: "path_to_executable".into(),
+ args: vec![],
+ env: Some(HashMap::default()),
+ },
+ },
+ );
+ }
+ });
+
+ if edits.is_empty() {
+ return;
+ }
+
+ let ranges = edits
+ .iter()
+ .map(|(range, _)| range.clone())
+ .collect::>();
+
+ item.edit(edits, cx);
+ if let Some((unique_server_name, buffer)) =
+ unique_server_name.zip(item.buffer().read(cx).as_singleton())
+ {
+ let snapshot = buffer.read(cx).snapshot();
+ if let Some(range) =
+ find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
+ {
+ item.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |selections| {
+ selections.select_ranges(vec![range]);
+ },
+ );
+ }
+ }
+ })
+}
+
+fn find_text_in_buffer(
+ text: &str,
+ start: usize,
+ snapshot: &language::BufferSnapshot,
+) -> Option> {
+ let chars = text.chars().collect::>();
+
+ let mut offset = start;
+ let mut char_offset = 0;
+ for c in snapshot.chars_at(start) {
+ if char_offset >= chars.len() {
+ break;
+ }
+ offset += 1;
+
+ if c == chars[char_offset] {
+ char_offset += 1;
+ } else {
+ char_offset = 0;
+ }
+ }
+
+ if char_offset == chars.len() {
+ Some(offset.saturating_sub(chars.len())..offset)
+ } else {
+ None
+ }
+}
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index 269aec33659a5c39c87137b79f4c32d9da930d4a..232311c5b02cdaa9edad4c0e9053163f450378e8 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Duration;
use acp_thread::AcpThread;
-use agent_servers::AgentServerSettings;
+use agent_servers::AgentServerCommand;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
@@ -14,6 +14,7 @@ use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread;
+use crate::ui::AcpOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -28,7 +29,6 @@ use crate::{
slash_command::SlashCommandCompletionProvider,
text_thread_editor::{
AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
- render_remaining_tokens,
},
thread_history::{HistoryEntryElement, ThreadHistory},
ui::{AgentOnboardingModal, EndTrialUpsell},
@@ -77,7 +77,10 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
- agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
+ agent::{
+ OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
+ ToggleModelSelector,
+ },
assistant::{OpenRulesLibrary, ToggleFocus},
};
@@ -201,6 +204,9 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx)
})
+ .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
+ AcpOnboardingModal::toggle(workspace, window, cx)
+ })
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
@@ -253,7 +259,7 @@ pub enum AgentType {
NativeAgent,
Custom {
name: SharedString,
- settings: AgentServerSettings,
+ command: AgentServerCommand,
},
}
@@ -591,17 +597,6 @@ impl AgentPanel {
None
};
- // Wait for the Gemini/Native feature flag to be available.
- let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
- if !client.status().borrow().is_signed_out() {
- cx.update(|_, cx| {
- cx.wait_for_flag_or_timeout::(
- Duration::from_secs(2),
- )
- })?
- .await;
- }
-
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = cx.new(|cx| {
Self::new(
@@ -622,6 +617,10 @@ impl AgentPanel {
}
cx.notify();
});
+ } else {
+ panel.update(cx, |panel, cx| {
+ panel.new_agent_thread(AgentType::NativeAgent, window, cx);
+ });
}
panel
})?;
@@ -1480,7 +1479,6 @@ impl AgentPanel {
tools,
self.language_registry.clone(),
self.workspace.clone(),
- self.project.downgrade(),
window,
cx,
)
@@ -1852,19 +1850,6 @@ impl AgentPanel {
menu
}
- pub fn set_selected_agent(
- &mut self,
- agent: AgentType,
- window: &mut Window,
- cx: &mut Context,
- ) {
- if self.selected_agent != agent {
- self.selected_agent = agent.clone();
- self.serialize(cx);
- }
- self.new_agent_thread(agent, window, cx);
- }
-
pub fn selected_agent(&self) -> AgentType {
self.selected_agent.clone()
}
@@ -1875,6 +1860,11 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context,
) {
+ if self.selected_agent != agent {
+ self.selected_agent = agent.clone();
+ self.serialize(cx);
+ }
+
match agent {
AgentType::Zed => {
window.dispatch_action(
@@ -1905,8 +1895,8 @@ impl AgentPanel {
window,
cx,
),
- AgentType::Custom { name, settings } => self.external_thread(
- Some(crate::ExternalAgent::Custom { name, settings }),
+ AgentType::Custom { name, command } => self.external_thread(
+ Some(crate::ExternalAgent::Custom { name, command }),
None,
None,
window,
@@ -2124,7 +2114,7 @@ impl AgentPanel {
.child(title_editor)
.into_any_element()
} else {
- Label::new(thread_view.read(cx).title())
+ Label::new(thread_view.read(cx).title(cx))
.color(Color::Muted)
.truncate()
.into_any_element()
@@ -2555,7 +2545,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::NativeAgent,
window,
cx,
@@ -2581,7 +2571,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::TextThread,
window,
cx,
@@ -2609,7 +2599,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::Gemini,
window,
cx,
@@ -2636,7 +2626,7 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::ClaudeCode,
window,
cx,
@@ -2669,13 +2659,13 @@ impl AgentPanel {
workspace.panel::(cx)
{
panel.update(cx, |panel, cx| {
- panel.set_selected_agent(
+ panel.new_agent_thread(
AgentType::Custom {
name: agent_name
.clone(),
- settings:
- agent_settings
- .clone(),
+ command: agent_settings
+ .command
+ .clone(),
},
window,
cx,
@@ -2693,9 +2683,9 @@ impl AgentPanel {
})
.when(cx.has_flag::(), |menu| {
menu.separator().link(
- "Add Your Own Agent",
+ "Add Other Agents",
OpenBrowser {
- url: "https://agentclientprotocol.com/".into(),
+ url: zed_urls::external_agents_docs(cx),
}
.boxed_clone(),
)
@@ -2883,12 +2873,8 @@ impl AgentPanel {
Some(token_count)
}
- ActiveView::TextThread { context_editor, .. } => {
- let element = render_remaining_tokens(context_editor, cx)?;
-
- Some(element.into_any_element())
- }
ActiveView::ExternalAgentThread { .. }
+ | ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
}
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index 110c432df3932f902b6ffcdac505a86e88550b28..93a4a8f748eefc933f809669af841f443888f7ed 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -28,7 +28,7 @@ use std::rc::Rc;
use std::sync::Arc;
use agent::{Thread, ThreadId};
-use agent_servers::AgentServerSettings;
+use agent_servers::AgentServerCommand;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
@@ -170,7 +170,7 @@ enum ExternalAgent {
NativeAgent,
Custom {
name: SharedString,
- settings: AgentServerSettings,
+ command: AgentServerCommand,
},
}
@@ -193,9 +193,9 @@ impl ExternalAgent {
Self::Gemini => Rc::new(agent_servers::Gemini),
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
- Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
+ Self::Custom { name, command } => Rc::new(agent_servers::CustomAgentServer::new(
name.clone(),
- settings,
+ command.clone(),
)),
}
}
diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs
index a626122769f656cf6627d104d00f4fa3a368e7db..3abefac8e8964ffdddc1397132541d0056f33ea8 100644
--- a/crates/agent_ui/src/inline_prompt_editor.rs
+++ b/crates/agent_ui/src/inline_prompt_editor.rs
@@ -334,7 +334,7 @@ impl PromptEditor {
EditorEvent::Edited { .. } => {
if let Some(workspace) = window.root::().flatten() {
workspace.update(cx, |workspace, cx| {
- let is_via_ssh = workspace.project().read(cx).is_via_ssh();
+ let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
workspace
.client()
diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs
index aceca79dbf95cd64bbf68b89907d0903f4aba9ff..3633e533da97b2b80e5c8d62c271da7121d3582b 100644
--- a/crates/agent_ui/src/language_model_selector.rs
+++ b/crates/agent_ui/src/language_model_selector.rs
@@ -6,7 +6,8 @@ use feature_flags::ZedProFeatureFlag;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{
- ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
+ AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
+ LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
@@ -76,6 +77,7 @@ pub struct LanguageModelPickerDelegate {
all_models: Arc,
filtered_entries: Vec,
selected_index: usize,
+ _authenticate_all_providers_task: Task<()>,
_subscriptions: Vec,
}
@@ -96,6 +98,7 @@ impl LanguageModelPickerDelegate {
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
filtered_entries: entries,
get_active_model: Arc::new(get_active_model),
+ _authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
@@ -139,6 +142,56 @@ impl LanguageModelPickerDelegate {
.unwrap_or(0)
}
+ /// Authenticates all providers in the [`LanguageModelRegistry`].
+ ///
+ /// We do this so that we can populate the language selector with all of the
+ /// models from the configured providers.
+ fn authenticate_all_providers(cx: &mut App) -> Task<()> {
+ let authenticate_all_providers = LanguageModelRegistry::global(cx)
+ .read(cx)
+ .providers()
+ .iter()
+ .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
+ .collect::>();
+
+ cx.spawn(async move |_cx| {
+ for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
+ if let Err(err) = authenticate_task.await {
+ if matches!(err, AuthenticateError::CredentialsNotFound) {
+ // Since we're authenticating these providers in the
+ // background for the purposes of populating the
+ // language selector, we don't care about providers
+ // where the credentials are not found.
+ } else {
+ // Some providers have noisy failure states that we
+ // don't want to spam the logs with every time the
+ // language model selector is initialized.
+ //
+ // Ideally these should have more clear failure modes
+ // that we know are safe to ignore here, like what we do
+ // with `CredentialsNotFound` above.
+ match provider_id.0.as_ref() {
+ "lmstudio" | "ollama" => {
+ // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
+ //
+ // These fail noisily, so we don't log them.
+ }
+ "copilot_chat" => {
+ // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
+ }
+ _ => {
+ log::error!(
+ "Failed to authenticate provider: {}: {err}",
+ provider_name.0
+ );
+ }
+ }
+ }
+ }
+ }
+ })
+ }
+
pub fn active_model(&self, cx: &App) -> Option