diff --git a/Cargo.lock b/Cargo.lock
index e40189c3fc0a9566b6de3d9e69a23e7ce0ba41e7..58be8d591565a1fad66627d8f116478971a535b8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -278,7 +278,6 @@ dependencies = [
"chrono",
"client",
"collections",
- "credentials_provider",
"env_logger 0.11.8",
"feature_flags",
"fs",
@@ -307,6 +306,7 @@ dependencies = [
"util",
"uuid",
"watch",
+ "zed_credentials_provider",
]
[[package]]
@@ -2880,6 +2880,7 @@ dependencies = [
"chrono",
"clock",
"cloud_api_client",
+ "cloud_api_types",
"cloud_llm_client",
"collections",
"credentials_provider",
@@ -2893,6 +2894,7 @@ dependencies = [
"http_client",
"http_client_tls",
"httparse",
+ "language_model",
"log",
"objc2-foundation",
"parking_lot",
@@ -2924,6 +2926,7 @@ dependencies = [
"util",
"windows 0.61.3",
"worktree",
+ "zed_credentials_provider",
]
[[package]]
@@ -3083,6 +3086,7 @@ dependencies = [
"serde",
"serde_json",
"text",
+ "zed_credentials_provider",
"zeta_prompt",
]
@@ -4059,12 +4063,8 @@ name = "credentials_provider"
version = "0.1.0"
dependencies = [
"anyhow",
- "futures 0.3.32",
"gpui",
- "paths",
- "release_channel",
"serde",
- "serde_json",
]
[[package]]
@@ -5173,6 +5173,7 @@ dependencies = [
"collections",
"copilot",
"copilot_ui",
+ "credentials_provider",
"ctor",
"db",
"edit_prediction_context",
@@ -5215,6 +5216,7 @@ dependencies = [
"workspace",
"worktree",
"zed_actions",
+ "zed_credentials_provider",
"zeta_prompt",
"zlog",
"zstd",
@@ -5641,6 +5643,13 @@ dependencies = [
"log",
]
+[[package]]
+name = "env_var"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+]
+
[[package]]
name = "envy"
version = "0.4.2"
@@ -7190,7 +7199,6 @@ dependencies = [
"collections",
"db",
"editor",
- "feature_flags",
"fs",
"git",
"git_ui",
@@ -7248,7 +7256,6 @@ dependencies = [
"ctor",
"db",
"editor",
- "feature_flags",
"file_icons",
"futures 0.3.32",
"fuzzy",
@@ -9385,12 +9392,12 @@ dependencies = [
"anthropic",
"anyhow",
"base64 0.22.1",
- "client",
"cloud_api_client",
"cloud_api_types",
"cloud_llm_client",
"collections",
"credentials_provider",
+ "env_var",
"futures 0.3.32",
"gpui",
"http_client",
@@ -9406,7 +9413,6 @@ dependencies = [
"smol",
"thiserror 2.0.17",
"util",
- "zed_env_vars",
]
[[package]]
@@ -10221,6 +10227,7 @@ dependencies = [
"language",
"log",
"markdown",
+ "project",
"settings",
"tempfile",
"theme_settings",
@@ -13213,6 +13220,7 @@ dependencies = [
"wax",
"which 6.0.3",
"worktree",
+ "zed_credentials_provider",
"zeroize",
"zlog",
"ztracing",
@@ -15858,6 +15866,7 @@ dependencies = [
"util",
"workspace",
"zed_actions",
+ "zed_credentials_provider",
]
[[package]]
@@ -15973,7 +15982,6 @@ dependencies = [
"agent_ui",
"anyhow",
"chrono",
- "collections",
"editor",
"feature_flags",
"fs",
@@ -22292,10 +22300,24 @@ dependencies = [
]
[[package]]
-name = "zed_env_vars"
+name = "zed_credentials_provider"
version = "0.1.0"
dependencies = [
+ "anyhow",
+ "credentials_provider",
+ "futures 0.3.32",
"gpui",
+ "paths",
+ "release_channel",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "zed_env_vars"
+version = "0.1.0"
+dependencies = [
+ "env_var",
]
[[package]]
@@ -22331,7 +22353,7 @@ dependencies = [
[[package]]
name = "zed_glsl"
-version = "0.2.2"
+version = "0.2.3"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -22345,7 +22367,7 @@ dependencies = [
[[package]]
name = "zed_proto"
-version = "0.3.1"
+version = "0.3.2"
dependencies = [
"zed_extension_api 0.7.0",
]
diff --git a/Cargo.toml b/Cargo.toml
index efbbd9f7f3f631a6d79dd8393559f07a9553e5ef..f441ca2f1d75675afbed891a396c0600f3c686e2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -61,6 +61,7 @@ members = [
"crates/edit_prediction_ui",
"crates/editor",
"crates/encoding_selector",
+ "crates/env_var",
"crates/etw_tracing",
"crates/eval_cli",
"crates/eval_utils",
@@ -220,6 +221,7 @@ members = [
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
+ "crates/zed_credentials_provider",
"crates/zed_env_vars",
"crates/zeta_prompt",
"crates/zlog",
@@ -309,6 +311,7 @@ dev_container = { path = "crates/dev_container" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
encoding_selector = { path = "crates/encoding_selector" }
+env_var = { path = "crates/env_var" }
etw_tracing = { path = "crates/etw_tracing" }
eval_utils = { path = "crates/eval_utils" }
extension = { path = "crates/extension" }
@@ -465,6 +468,7 @@ worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
+zed_credentials_provider = { path = "crates/zed_credentials_provider" }
zed_env_vars = { path = "crates/zed_env_vars" }
edit_prediction = { path = "crates/edit_prediction" }
zeta_prompt = { path = "crates/zeta_prompt" }
diff --git a/assets/icons/diff_split.svg b/assets/icons/diff_split.svg
index de2056466f7ef1081ee00dabb8b4d5baa8fc9217..dcafeb8df5c28bcac1f1fe8cf5783eebd8d8cd8a 100644
--- a/assets/icons/diff_split.svg
+++ b/assets/icons/diff_split.svg
@@ -1,5 +1,4 @@
diff --git a/assets/icons/diff_split_auto.svg b/assets/icons/diff_split_auto.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f9dd7076be75aaf3e90286140a60deece5016114
--- /dev/null
+++ b/assets/icons/diff_split_auto.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/diff_unified.svg b/assets/icons/diff_unified.svg
index b2d3895ae5466454e9cefc4e77e3c3f2a19cde8c..28735c16f682159b6b0a099176d6fc3b75cd248e 100644
--- a/assets/icons/diff_unified.svg
+++ b/assets/icons/diff_unified.svg
@@ -1,4 +1,4 @@
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 98053432c5a186ecc886318f2d677f73a62295a2..5ecca68e0404b400af2c285dc51df0a65d6fe07a 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -284,12 +284,36 @@
"context": "AcpThread",
"bindings": {
"ctrl--": "pane::GoBack",
+ "pageup": "agent::ScrollOutputPageUp",
+ "pagedown": "agent::ScrollOutputPageDown",
+ "home": "agent::ScrollOutputToTop",
+ "end": "agent::ScrollOutputToBottom",
+ "up": "agent::ScrollOutputLineUp",
+ "down": "agent::ScrollOutputLineDown",
+ "shift-pageup": "agent::ScrollOutputToPreviousMessage",
+ "shift-pagedown": "agent::ScrollOutputToNextMessage",
+ "ctrl-alt-pageup": "agent::ScrollOutputPageUp",
+ "ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
+ "ctrl-alt-home": "agent::ScrollOutputToTop",
+ "ctrl-alt-end": "agent::ScrollOutputToBottom",
+ "ctrl-alt-up": "agent::ScrollOutputLineUp",
+ "ctrl-alt-down": "agent::ScrollOutputLineDown",
+ "ctrl-alt-shift-pageup": "agent::ScrollOutputToPreviousMessage",
+ "ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
+ "ctrl-alt-pageup": "agent::ScrollOutputPageUp",
+ "ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
+ "ctrl-alt-home": "agent::ScrollOutputToTop",
+ "ctrl-alt-end": "agent::ScrollOutputToBottom",
+ "ctrl-alt-up": "agent::ScrollOutputLineUp",
+ "ctrl-alt-down": "agent::ScrollOutputLineDown",
+ "ctrl-alt-shift-pageup": "agent::ScrollOutputToPreviousMessage",
+ "ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-d": "git::Diff",
"shift-alt-y": "agent::KeepAll",
@@ -574,6 +598,7 @@
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
"alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": false }],
+ "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
"alt-shift-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
// Change to open path modal for existing remote connection by setting the parameter
// "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
@@ -1251,6 +1276,8 @@
"alt-down": "markdown::ScrollDownByItem",
"ctrl-home": "markdown::ScrollToTop",
"ctrl-end": "markdown::ScrollToBottom",
+ "find": "buffer_search::Deploy",
+ "ctrl-f": "buffer_search::Deploy",
},
},
{
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index f0835a139a39602547d9d8da1cba93eaa7ee82a9..c74b5900001a2c798076783b2741aba84ffc4b15 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -327,12 +327,36 @@
"context": "AcpThread",
"bindings": {
"ctrl--": "pane::GoBack",
+ "pageup": "agent::ScrollOutputPageUp",
+ "pagedown": "agent::ScrollOutputPageDown",
+ "home": "agent::ScrollOutputToTop",
+ "end": "agent::ScrollOutputToBottom",
+ "up": "agent::ScrollOutputLineUp",
+ "down": "agent::ScrollOutputLineDown",
+ "shift-pageup": "agent::ScrollOutputToPreviousMessage",
+ "shift-pagedown": "agent::ScrollOutputToNextMessage",
+ "ctrl-pageup": "agent::ScrollOutputPageUp",
+ "ctrl-pagedown": "agent::ScrollOutputPageDown",
+ "ctrl-home": "agent::ScrollOutputToTop",
+ "ctrl-end": "agent::ScrollOutputToBottom",
+ "ctrl-alt-up": "agent::ScrollOutputLineUp",
+ "ctrl-alt-down": "agent::ScrollOutputLineDown",
+ "ctrl-alt-pageup": "agent::ScrollOutputToPreviousMessage",
+ "ctrl-alt-pagedown": "agent::ScrollOutputToNextMessage",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
+ "ctrl-pageup": "agent::ScrollOutputPageUp",
+ "ctrl-pagedown": "agent::ScrollOutputPageDown",
+ "ctrl-home": "agent::ScrollOutputToTop",
+ "ctrl-end": "agent::ScrollOutputToBottom",
+ "ctrl-alt-up": "agent::ScrollOutputLineUp",
+ "ctrl-alt-down": "agent::ScrollOutputLineDown",
+ "ctrl-alt-pageup": "agent::ScrollOutputToPreviousMessage",
+ "ctrl-alt-pagedown": "agent::ScrollOutputToNextMessage",
"shift-ctrl-r": "agent::OpenAgentDiff",
"shift-ctrl-d": "git::Diff",
"shift-alt-y": "agent::KeepAll",
@@ -644,6 +668,7 @@
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
"alt-cmd-o": ["projects::OpenRecent", { "create_new_window": false }],
+ "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
"ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }],
"cmd-ctrl-b": "branches::OpenRecent",
@@ -1351,6 +1376,7 @@
"alt-down": "markdown::ScrollDownByItem",
"cmd-up": "markdown::ScrollToTop",
"cmd-down": "markdown::ScrollToBottom",
+ "cmd-f": "buffer_search::Deploy",
},
},
{
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
index 41f36638e1dec40890ddecc6a808c669672e9317..a9eb3933423ff60fe60ac391b12773ce7146fb0d 100644
--- a/assets/keymaps/default-windows.json
+++ b/assets/keymaps/default-windows.json
@@ -285,12 +285,36 @@
"context": "AcpThread",
"bindings": {
"ctrl--": "pane::GoBack",
+ "pageup": "agent::ScrollOutputPageUp",
+ "pagedown": "agent::ScrollOutputPageDown",
+ "home": "agent::ScrollOutputToTop",
+ "end": "agent::ScrollOutputToBottom",
+ "up": "agent::ScrollOutputLineUp",
+ "down": "agent::ScrollOutputLineDown",
+ "shift-pageup": "agent::ScrollOutputToPreviousMessage",
+ "shift-pagedown": "agent::ScrollOutputToNextMessage",
+ "ctrl-alt-pageup": "agent::ScrollOutputPageUp",
+ "ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
+ "ctrl-alt-home": "agent::ScrollOutputToTop",
+ "ctrl-alt-end": "agent::ScrollOutputToBottom",
+ "ctrl-alt-up": "agent::ScrollOutputLineUp",
+ "ctrl-alt-down": "agent::ScrollOutputLineDown",
+ "ctrl-alt-shift-pageup": "agent::ScrollOutputToPreviousMessage",
+ "ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
+ "ctrl-alt-pageup": "agent::ScrollOutputPageUp",
+ "ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
+ "ctrl-alt-home": "agent::ScrollOutputToTop",
+ "ctrl-alt-end": "agent::ScrollOutputToBottom",
+ "ctrl-alt-up": "agent::ScrollOutputLineUp",
+ "ctrl-alt-down": "agent::ScrollOutputLineDown",
+ "ctrl-alt-shift-pageup": "agent::ScrollOutputToPreviousMessage",
+ "ctrl-alt-shift-pagedown": "agent::ScrollOutputToNextMessage",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-d": "git::Diff",
"shift-alt-y": "agent::KeepAll",
@@ -1276,6 +1300,8 @@
"alt-down": "markdown::ScrollDownByItem",
"ctrl-home": "markdown::ScrollToTop",
"ctrl-end": "markdown::ScrollToBottom",
+ "find": "buffer_search::Deploy",
+ "ctrl-f": "buffer_search::Deploy",
},
},
{
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index 1a7e7bf77248b6f863d4a6dbc1e268b4c5ae3576..220b44ff537ffa791b23c0c5b7d86b6768d74dc2 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -1096,6 +1096,7 @@
"ctrl-e": "markdown::ScrollDown",
"g g": "markdown::ScrollToTop",
"shift-g": "markdown::ScrollToBottom",
+ "/": "buffer_search::Deploy",
},
},
{
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 74a4e15a044fa5686441f2e8a587595936ea08fb..5e1eb0e68d2f8a17f89422597aa29b99516333e8 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -225,6 +225,11 @@
// 3. Hide on both typing and cursor movement:
// "on_typing_and_movement"
"hide_mouse": "on_typing_and_movement",
+ // Determines whether the focused panel follows the mouse location.
+ "focus_follows_mouse": {
+ "enabled": false,
+ "debounce_ms": 250,
+ },
// Determines how snippets are sorted relative to other completion items.
//
// 1. Place snippets at the top of the completion list:
@@ -1139,6 +1144,11 @@
//
// Default: false
"show_turn_stats": false,
+ // Whether to show the merge conflict indicator in the status bar
+ // that offers to resolve conflicts using the agent.
+ //
+ // Default: true
+ "show_merge_conflict_indicator": true,
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json
index 0d6f4471320e443f3c4a483f53f6901c76e7dc72..bb6c9c04ae14db8f2d01adabd8d1494caa7d7407 100644
--- a/assets/settings/initial_tasks.json
+++ b/assets/settings/initial_tasks.json
@@ -50,9 +50,9 @@
"show_command": true,
// Which edited buffers to save before running the task:
// * `all` — save all edited buffers
- // * `current` — save current buffer only
+ // * `current` — save currently active buffer only
// * `none` — don't save any buffers
- "save": "all",
+ "save": "none",
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
// "tags": []
},
diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json
index 3450e35bf62d780bdaf0cff2c6bc9f8bdfea7c1e..f27566c4f72cac3938a752c64d95d0500c595306 100644
--- a/assets/themes/ayu/ayu.json
+++ b/assets/themes/ayu/ayu.json
@@ -283,7 +283,7 @@
"font_weight": null
},
"preproc": {
- "color": "#bfbdb6ff",
+ "color": "#ff8f3fff",
"font_style": null,
"font_weight": null
},
@@ -391,6 +391,16 @@
"color": "#5ac1feff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#aad94cff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#f07178ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
@@ -675,7 +685,7 @@
"font_weight": null
},
"preproc": {
- "color": "#5c6166ff",
+ "color": "#fa8d3eff",
"font_style": null,
"font_weight": null
},
@@ -783,6 +793,16 @@
"color": "#3b9ee5ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#6cbf43ff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#ff6666ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
@@ -1067,7 +1087,7 @@
"font_weight": null
},
"preproc": {
- "color": "#cccac2ff",
+ "color": "#ffad65ff",
"font_style": null,
"font_weight": null
},
@@ -1175,6 +1195,16 @@
"color": "#72cffeff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#aad94cff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#f07178ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json
index 16ae188712f7a800ab4fb8a81a2d24cac99da56b..4330df54fccae55e7ca077c0da9a891ee71ebe3a 100644
--- a/assets/themes/gruvbox/gruvbox.json
+++ b/assets/themes/gruvbox/gruvbox.json
@@ -293,7 +293,7 @@
"font_weight": null
},
"preproc": {
- "color": "#fbf1c7ff",
+ "color": "#fb4833ff",
"font_style": null,
"font_weight": null
},
@@ -406,6 +406,16 @@
"color": "#83a598ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#b8bb26ff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#fb4934ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
@@ -700,7 +710,7 @@
"font_weight": null
},
"preproc": {
- "color": "#fbf1c7ff",
+ "color": "#fb4833ff",
"font_style": null,
"font_weight": null
},
@@ -813,6 +823,16 @@
"color": "#83a598ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#b8bb26ff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#fb4934ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
@@ -1107,7 +1127,7 @@
"font_weight": null
},
"preproc": {
- "color": "#fbf1c7ff",
+ "color": "#fb4833ff",
"font_style": null,
"font_weight": null
},
@@ -1220,6 +1240,16 @@
"color": "#83a598ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#b8bb26ff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#fb4934ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
@@ -1514,7 +1544,7 @@
"font_weight": null
},
"preproc": {
- "color": "#282828ff",
+ "color": "#9d0006ff",
"font_style": null,
"font_weight": null
},
@@ -1627,6 +1657,16 @@
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#79740eff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#9d0006ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
@@ -1921,7 +1961,7 @@
"font_weight": null
},
"preproc": {
- "color": "#282828ff",
+ "color": "#9d0006ff",
"font_style": null,
"font_weight": null
},
@@ -2034,6 +2074,16 @@
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#79740eff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#9d0006ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
@@ -2328,7 +2378,7 @@
"font_weight": null
},
"preproc": {
- "color": "#282828ff",
+ "color": "#9d0006ff",
"font_style": null,
"font_weight": null
},
@@ -2441,6 +2491,16 @@
"color": "#0b6678ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#79740eff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#9d0006ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json
index 05af3f5cfeec7d4a24c4fe6d684fb21d04e2d81c..e60b6314b9595ac02bd6a43be4580ba9331ae769 100644
--- a/assets/themes/one/one.json
+++ b/assets/themes/one/one.json
@@ -290,7 +290,7 @@
"font_weight": null
},
"preproc": {
- "color": "#dce0e5ff",
+ "color": "#b477cfff",
"font_style": null,
"font_weight": null
},
@@ -403,6 +403,16 @@
"color": "#73ade9ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#98c379ff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#e06c75ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
@@ -692,7 +702,7 @@
"font_weight": null
},
"preproc": {
- "color": "#242529ff",
+ "color": "#a449abff",
"font_style": null,
"font_weight": null
},
@@ -805,6 +815,16 @@
"color": "#5b79e3ff",
"font_style": null,
"font_weight": null
+ },
+ "diff.plus": {
+ "color": "#50a14fff",
+ "font_style": null,
+ "font_weight": null
+ },
+ "diff.minus": {
+ "color": "#e45649ff",
+ "font_style": null,
+ "font_weight": null
}
}
}
diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs
index b1eb42996af29c739b0b3a9e198d543850693d99..ac7b2d23cb796634fc61022411bb583808f697ef 100644
--- a/crates/acp_thread/src/mention.rs
+++ b/crates/acp_thread/src/mention.rs
@@ -19,7 +19,9 @@ pub enum MentionUri {
File {
abs_path: PathBuf,
},
- PastedImage,
+ PastedImage {
+ name: String,
+ },
Directory {
abs_path: PathBuf,
},
@@ -155,7 +157,9 @@ impl MentionUri {
include_warnings,
})
} else if path.starts_with("/agent/pasted-image") {
- Ok(Self::PastedImage)
+ let name =
+ single_query_param(&url, "name")?.unwrap_or_else(|| "Image".to_string());
+ Ok(Self::PastedImage { name })
} else if path.starts_with("/agent/untitled-buffer") {
let fragment = url
.fragment()
@@ -227,7 +231,7 @@ impl MentionUri {
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
- MentionUri::PastedImage => "Image".to_string(),
+ MentionUri::PastedImage { name } => name.clone(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
@@ -296,7 +300,7 @@ impl MentionUri {
MentionUri::File { abs_path } => {
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
}
- MentionUri::PastedImage => IconName::Image.path().into(),
+ MentionUri::PastedImage { .. } => IconName::Image.path().into(),
MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx)
.unwrap_or_else(|| IconName::Folder.path().into()),
MentionUri::Symbol { .. } => IconName::Code.path().into(),
@@ -322,10 +326,18 @@ impl MentionUri {
url.set_path(&abs_path.to_string_lossy());
url
}
- MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
+ MentionUri::PastedImage { name } => {
+ let mut url = Url::parse("zed:///agent/pasted-image").unwrap();
+ url.query_pairs_mut().append_pair("name", name);
+ url
+ }
MentionUri::Directory { abs_path } => {
let mut url = Url::parse("file:///").unwrap();
- url.set_path(&abs_path.to_string_lossy());
+ let mut path = abs_path.to_string_lossy().into_owned();
+ if !path.ends_with('/') && !path.ends_with('\\') {
+ path.push('/');
+ }
+ url.set_path(&path);
url
}
MentionUri::Symbol {
@@ -490,6 +502,21 @@ mod tests {
assert_eq!(uri.to_uri().to_string(), expected);
}
+ #[test]
+ fn test_directory_uri_round_trip_without_trailing_slash() {
+ let uri = MentionUri::Directory {
+ abs_path: PathBuf::from(path!("/path/to/dir")),
+ };
+ let serialized = uri.to_uri().to_string();
+ assert!(serialized.ends_with('/'), "directory URI must end with /");
+ let parsed = MentionUri::parse(&serialized, PathStyle::local()).unwrap();
+ assert!(
+ matches!(parsed, MentionUri::Directory { .. }),
+ "expected Directory variant, got {:?}",
+ parsed
+ );
+ }
+
#[test]
fn test_parse_symbol_uri() {
let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
diff --git a/crates/agent/src/edit_agent.rs b/crates/agent/src/edit_agent.rs
index f0dae2a7b39dcad0fea280a2354f2f3c5c61600b..afaa124de066d92e5a1d1a1670f762017f086d01 100644
--- a/crates/agent/src/edit_agent.rs
+++ b/crates/agent/src/edit_agent.rs
@@ -1519,7 +1519,7 @@ mod tests {
stream: &mut UnboundedReceiver,
) -> Vec {
let mut events = Vec::new();
- while let Ok(Some(event)) = stream.try_next() {
+ while let Ok(event) = stream.try_recv() {
events.push(event);
}
events
diff --git a/crates/agent/src/edit_agent/evals.rs b/crates/agent/src/edit_agent/evals.rs
index e7b67e37bf4a8b71664a78b99b757c6985794ec6..ba8b7ed867ea26bcdcdee7f8bf20390c2f9592b3 100644
--- a/crates/agent/src/edit_agent/evals.rs
+++ b/crates/agent/src/edit_agent/evals.rs
@@ -4,7 +4,7 @@ use crate::{
ListDirectoryTool, ListDirectoryToolInput, ReadFileTool, ReadFileToolInput,
};
use Role::*;
-use client::{Client, UserStore};
+use client::{Client, RefreshLlmTokenListener, UserStore};
use eval_utils::{EvalOutput, EvalOutputProcessor, OutcomeKind};
use fs::FakeFs;
use futures::{FutureExt, future::LocalBoxFuture};
@@ -1423,7 +1423,8 @@ impl EditAgentTest {
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
settings::init(cx);
- language_model::init(user_store.clone(), client.clone(), cx);
+ language_model::init(cx);
+ RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
language_models::init(user_store, client.clone(), cx);
});
diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs
index 49d2d050611e29357401791f49b10f3b0bd1df30..3ffcb016ae2664104962ea722788977fb1fe65ac 100644
--- a/crates/agent/src/tests/mod.rs
+++ b/crates/agent/src/tests/mod.rs
@@ -6,7 +6,7 @@ use acp_thread::{
use agent_client_protocol::schema as acp;
use agent_settings::AgentProfileId;
use anyhow::Result;
-use client::{Client, UserStore};
+use client::{Client, RefreshLlmTokenListener, UserStore};
use collections::IndexMap;
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
use feature_flags::FeatureFlagAppExt as _;
@@ -3253,7 +3253,8 @@ 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));
- language_model::init(user_store.clone(), client.clone(), cx);
+ language_model::init(cx);
+ RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
language_models::init(user_store, client.clone(), cx);
LanguageModelRegistry::test(cx);
});
@@ -3982,7 +3983,8 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
cx.set_http_client(Arc::new(http_client));
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
- language_model::init(user_store.clone(), client.clone(), cx);
+ language_model::init(cx);
+ RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
language_models::init(user_store, client.clone(), cx);
}
};
@@ -6206,9 +6208,9 @@ async fn test_edit_file_tool_allow_rule_skips_confirmation(cx: &mut TestAppConte
cx.run_until_parked();
- let event = rx.try_next();
+ let event = rx.try_recv();
assert!(
- !matches!(event, Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(_))))),
+ !matches!(event, Ok(Ok(ThreadEvent::ToolCallAuthorization(_)))),
"expected no authorization request for allowed .md file"
);
}
@@ -6350,9 +6352,9 @@ async fn test_fetch_tool_allow_rule_skips_confirmation(cx: &mut TestAppContext)
cx.run_until_parked();
- let event = rx.try_next();
+ let event = rx.try_recv();
assert!(
- !matches!(event, Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(_))))),
+ !matches!(event, Ok(Ok(ThreadEvent::ToolCallAuthorization(_)))),
"expected no authorization request for allowed docs.rs URL"
);
}
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index 479dcfc01b699151119bfa4ef66a94e9b63f8563..eb014d2464cdb15cbddb52ab69b23cc3e28fa950 100644
--- a/crates/agent/src/thread.rs
+++ b/crates/agent/src/thread.rs
@@ -253,7 +253,7 @@ impl UserMessage {
)
.ok();
}
- MentionUri::PastedImage => {
+ MentionUri::PastedImage { .. } => {
debug_panic!("pasted image URI should not be used in mention content")
}
MentionUri::Directory { .. } => {
diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs
index c67942e5cd3769f814fad62f7311bf7967f3317a..58e779da59aef176464839ed6f2d6a5c16e4bc12 100644
--- a/crates/agent/src/tool_permissions.rs
+++ b/crates/agent/src/tool_permissions.rs
@@ -595,6 +595,7 @@ mod tests {
message_editor_min_lines: 1,
tool_permissions,
show_turn_stats: false,
+ show_merge_conflict_indicator: true,
new_thread_location: Default::default(),
sidebar_side: Default::default(),
thinking_display: Default::default(),
diff --git a/crates/agent/src/tools/copy_path_tool.rs b/crates/agent/src/tools/copy_path_tool.rs
index 32d48d2e1435b6e8b454557c337a4603857304c7..063742cef8b888a5d8716afa87787ddfc3c1f9b0 100644
--- a/crates/agent/src/tools/copy_path_tool.rs
+++ b/crates/agent/src/tools/copy_path_tool.rs
@@ -382,8 +382,8 @@ mod tests {
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Expected a single authorization prompt",
);
@@ -449,8 +449,8 @@ mod tests {
assert!(result.is_err(), "Tool should fail when policy denies");
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Deny policy should not emit symlink authorization prompt",
);
diff --git a/crates/agent/src/tools/create_directory_tool.rs b/crates/agent/src/tools/create_directory_tool.rs
index 631870b4236af2f1140d6510ba7ae31e28c8e206..0e5261d0715907e084e89cd57fb1547431aef785 100644
--- a/crates/agent/src/tools/create_directory_tool.rs
+++ b/crates/agent/src/tools/create_directory_tool.rs
@@ -369,8 +369,8 @@ mod tests {
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Expected a single authorization prompt",
);
@@ -439,8 +439,8 @@ mod tests {
assert!(result.is_err(), "Tool should fail when policy denies");
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Deny policy should not emit symlink authorization prompt",
);
diff --git a/crates/agent/src/tools/delete_path_tool.rs b/crates/agent/src/tools/delete_path_tool.rs
index 3051ffc29eba8ed96929c002df2cbb81b1a20c2e..d790896425885e568019588a6a522412f2ec7fc0 100644
--- a/crates/agent/src/tools/delete_path_tool.rs
+++ b/crates/agent/src/tools/delete_path_tool.rs
@@ -438,8 +438,8 @@ mod tests {
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Expected a single authorization prompt",
);
@@ -512,8 +512,8 @@ mod tests {
assert!(result.is_err(), "Tool should fail when policy denies");
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Deny policy should not emit symlink authorization prompt",
);
diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs
index 8f9d23f0007391779a7ae0bd41360a70d034f745..85c17c58e8f2543097a3881cb93e840ee09834a9 100644
--- a/crates/agent/src/tools/edit_file_tool.rs
+++ b/crates/agent/src/tools/edit_file_tool.rs
@@ -1188,7 +1188,7 @@ mod tests {
})
.await
.unwrap();
- assert!(stream_rx.try_next().is_err());
+ assert!(stream_rx.try_recv().is_err());
// Test 4: Path with .zed in the middle should require confirmation
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@@ -1251,7 +1251,7 @@ mod tests {
})
.await
.unwrap();
- assert!(stream_rx.try_next().is_err());
+ assert!(stream_rx.try_recv().is_err());
// 5.3: Normal in-project path with allow — no confirmation needed
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@@ -1268,7 +1268,7 @@ mod tests {
})
.await
.unwrap();
- assert!(stream_rx.try_next().is_err());
+ assert!(stream_rx.try_recv().is_err());
// 5.4: With Confirm default, non-project paths still prompt
cx.update(|cx| {
@@ -1586,8 +1586,8 @@ mod tests {
assert!(result.is_err(), "Tool should fail when policy denies");
assert!(
!matches!(
- stream_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ stream_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Deny policy should not emit symlink authorization prompt",
);
@@ -1658,7 +1658,7 @@ mod tests {
} else {
auth.await.unwrap();
assert!(
- stream_rx.try_next().is_err(),
+ stream_rx.try_recv().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
@@ -1769,7 +1769,7 @@ mod tests {
} else {
auth.await.unwrap();
assert!(
- stream_rx.try_next().is_err(),
+ stream_rx.try_recv().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
@@ -1862,7 +1862,7 @@ mod tests {
stream_rx.expect_authorization().await;
} else {
assert!(
- stream_rx.try_next().is_err(),
+ stream_rx.try_recv().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
@@ -1963,7 +1963,7 @@ mod tests {
})
.await
.unwrap();
- assert!(stream_rx.try_next().is_err());
+ assert!(stream_rx.try_recv().is_err());
}
}
diff --git a/crates/agent/src/tools/evals/streaming_edit_file.rs b/crates/agent/src/tools/evals/streaming_edit_file.rs
index 6a55517037e54ae4166cd22427201d9325ef0f76..0c6290ec098f9c37a0f6a077daf0a041c013d8ff 100644
--- a/crates/agent/src/tools/evals/streaming_edit_file.rs
+++ b/crates/agent/src/tools/evals/streaming_edit_file.rs
@@ -6,7 +6,7 @@ use crate::{
};
use Role::*;
use anyhow::{Context as _, Result};
-use client::{Client, UserStore};
+use client::{Client, RefreshLlmTokenListener, UserStore};
use fs::FakeFs;
use futures::{FutureExt, StreamExt, future::LocalBoxFuture};
use gpui::{AppContext as _, AsyncApp, Entity, TestAppContext, UpdateGlobal as _};
@@ -274,7 +274,8 @@ impl StreamingEditToolTest {
cx.set_http_client(http_client);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
- language_model::init(user_store.clone(), client.clone(), cx);
+ language_model::init(cx);
+ RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
language_models::init(user_store, client, cx);
});
diff --git a/crates/agent/src/tools/list_directory_tool.rs b/crates/agent/src/tools/list_directory_tool.rs
index be51fcf9188308bd7f15481ce3dc6db208e2c39b..8431648b64a8a082cf27bd7bc216935169f586f8 100644
--- a/crates/agent/src/tools/list_directory_tool.rs
+++ b/crates/agent/src/tools/list_directory_tool.rs
@@ -981,13 +981,11 @@ mod tests {
"Expected private path validation error, got: {error}"
);
- let event = event_rx.try_next();
+ let event = event_rx.try_recv();
assert!(
!matches!(
event,
- Ok(Some(Ok(crate::thread::ThreadEvent::ToolCallAuthorization(
- _
- ))))
+ Ok(Ok(crate::thread::ThreadEvent::ToolCallAuthorization(_)))
),
"No authorization should be requested when validation fails before listing",
);
@@ -1029,13 +1027,11 @@ mod tests {
"Normal path should succeed without authorization"
);
- let event = event_rx.try_next();
+ let event = event_rx.try_recv();
assert!(
!matches!(
event,
- Ok(Some(Ok(crate::thread::ThreadEvent::ToolCallAuthorization(
- _
- ))))
+ Ok(Ok(crate::thread::ThreadEvent::ToolCallAuthorization(_)))
),
"No authorization should be requested for normal paths",
);
@@ -1086,13 +1082,11 @@ mod tests {
"Intra-project symlink should succeed without authorization: {result:?}",
);
- let event = event_rx.try_next();
+ let event = event_rx.try_recv();
assert!(
!matches!(
event,
- Ok(Some(Ok(crate::thread::ThreadEvent::ToolCallAuthorization(
- _
- ))))
+ Ok(Ok(crate::thread::ThreadEvent::ToolCallAuthorization(_)))
),
"No authorization should be requested for intra-project symlinks",
);
diff --git a/crates/agent/src/tools/move_path_tool.rs b/crates/agent/src/tools/move_path_tool.rs
index fcdbd3c59406c35968cab8e92116c17d6dd4d591..4a8aad8455019e776b77e326d5438cedd4518c70 100644
--- a/crates/agent/src/tools/move_path_tool.rs
+++ b/crates/agent/src/tools/move_path_tool.rs
@@ -389,8 +389,8 @@ mod tests {
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Expected a single authorization prompt",
);
@@ -456,8 +456,8 @@ mod tests {
assert!(result.is_err(), "Tool should fail when policy denies");
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Deny policy should not emit symlink authorization prompt",
);
diff --git a/crates/agent/src/tools/read_file_tool.rs b/crates/agent/src/tools/read_file_tool.rs
index 5b5f9ca1af6eced21d4bda4b63fe99be8cda040a..37517f6a5cdcf7c1af2ee940b56f1909f823cbd2 100644
--- a/crates/agent/src/tools/read_file_tool.rs
+++ b/crates/agent/src/tools/read_file_tool.rs
@@ -1316,13 +1316,11 @@ mod test {
"Expected private-files validation error, got: {error}"
);
- let event = event_rx.try_next();
+ let event = event_rx.try_recv();
assert!(
!matches!(
event,
- Ok(Some(Ok(crate::thread::ThreadEvent::ToolCallAuthorization(
- _
- ))))
+ Ok(Ok(crate::thread::ThreadEvent::ToolCallAuthorization(_)))
),
"No authorization should be requested when validation fails before read",
);
diff --git a/crates/agent/src/tools/restore_file_from_disk_tool.rs b/crates/agent/src/tools/restore_file_from_disk_tool.rs
index 311ca664333453b4d439e56b194df30ca5cbd987..6953e234e9574c1745e4fd204849584d74b72a88 100644
--- a/crates/agent/src/tools/restore_file_from_disk_tool.rs
+++ b/crates/agent/src/tools/restore_file_from_disk_tool.rs
@@ -589,8 +589,8 @@ mod tests {
assert!(result.is_err(), "Tool should fail when policy denies");
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Deny policy should not emit symlink authorization prompt",
);
@@ -662,8 +662,8 @@ mod tests {
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Expected a single authorization prompt",
);
diff --git a/crates/agent/src/tools/save_file_tool.rs b/crates/agent/src/tools/save_file_tool.rs
index 7eeeea961f6e5076b60a5663d93f85fe8de816f0..904e9ba8642f1b6cd15d6fa7d285d2969c11d81b 100644
--- a/crates/agent/src/tools/save_file_tool.rs
+++ b/crates/agent/src/tools/save_file_tool.rs
@@ -584,8 +584,8 @@ mod tests {
assert!(result.is_err(), "Tool should fail when policy denies");
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Deny policy should not emit symlink authorization prompt",
);
@@ -657,8 +657,8 @@ mod tests {
assert!(
!matches!(
- event_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ event_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Expected a single authorization prompt",
);
diff --git a/crates/agent/src/tools/streaming_edit_file_tool.rs b/crates/agent/src/tools/streaming_edit_file_tool.rs
index dfd3310f143d2ad5c8221a2af8d97a0b27c27e89..f977b4d13ceb11679ab63f013d2aef8b6a41b781 100644
--- a/crates/agent/src/tools/streaming_edit_file_tool.rs
+++ b/crates/agent/src/tools/streaming_edit_file_tool.rs
@@ -2493,7 +2493,7 @@ mod tests {
})
.await
.unwrap();
- assert!(stream_rx.try_next().is_err());
+ assert!(stream_rx.try_recv().is_err());
// Test 4: Path with .zed in the middle should require confirmation
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@@ -2540,7 +2540,7 @@ mod tests {
cx.update(|cx| tool.authorize(&PathBuf::from("/etc/hosts"), "test 5.2", &stream_tx, cx))
.await
.unwrap();
- assert!(stream_rx.try_next().is_err());
+ assert!(stream_rx.try_recv().is_err());
// 5.3: Normal in-project path with allow — no confirmation needed
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@@ -2554,7 +2554,7 @@ mod tests {
})
.await
.unwrap();
- assert!(stream_rx.try_next().is_err());
+ assert!(stream_rx.try_recv().is_err());
// 5.4: With Confirm default, non-project paths still prompt
cx.update(|cx| {
@@ -2767,8 +2767,8 @@ mod tests {
assert!(result.is_err(), "Tool should fail when policy denies");
assert!(
!matches!(
- stream_rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ stream_rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"Deny policy should not emit symlink authorization prompt",
);
@@ -2810,7 +2810,7 @@ mod tests {
} else {
auth.await.unwrap();
assert!(
- stream_rx.try_next().is_err(),
+ stream_rx.try_recv().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
@@ -2887,7 +2887,7 @@ mod tests {
} else {
auth.await.unwrap();
assert!(
- stream_rx.try_next().is_err(),
+ stream_rx.try_recv().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
@@ -2947,7 +2947,7 @@ mod tests {
stream_rx.expect_authorization().await;
} else {
assert!(
- stream_rx.try_next().is_err(),
+ stream_rx.try_recv().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
@@ -3015,7 +3015,7 @@ mod tests {
})
.await
.unwrap();
- assert!(stream_rx.try_next().is_err());
+ assert!(stream_rx.try_recv().is_err());
}
}
diff --git a/crates/agent/src/tools/terminal_tool.rs b/crates/agent/src/tools/terminal_tool.rs
index 4d17c0d91dc4d1bff384e89e29a59359151e6fc4..33560f2cf7cc7db6ae0abf6831c1c8050c901308 100644
--- a/crates/agent/src/tools/terminal_tool.rs
+++ b/crates/agent/src/tools/terminal_tool.rs
@@ -681,17 +681,17 @@ mod tests {
);
assert!(
!matches!(
- rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"invalid command should not request authorization"
);
assert!(
!matches!(
- rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallUpdate(
+ rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallUpdate(
acp_thread::ToolCallUpdate::UpdateFields(_)
- ))))
+ )))
),
"invalid command should not emit a terminal card update"
);
@@ -810,8 +810,8 @@ mod tests {
);
assert!(
!matches!(
- rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"hardcoded denial should not request authorization"
);
@@ -1058,8 +1058,8 @@ mod tests {
);
assert!(
!matches!(
- rx.try_next(),
- Ok(Some(Ok(crate::ThreadEvent::ToolCallAuthorization(_))))
+ rx.try_recv(),
+ Ok(Ok(crate::ThreadEvent::ToolCallAuthorization(_)))
),
"rejected command {command:?} should not request authorization"
);
diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml
index 1afd0aa902ed58aa6dc0fea1b18d4bba58864012..e68c1bfd0eddb0e4072ff7952a3c094dcf4a7995 100644
--- a/crates/agent_servers/Cargo.toml
+++ b/crates/agent_servers/Cargo.toml
@@ -31,7 +31,6 @@ futures.workspace = true
gpui.workspace = true
feature_flags.workspace = true
gpui_tokio = { workspace = true, optional = true }
-credentials_provider.workspace = true
google_ai.workspace = true
http_client.workspace = true
indoc.workspace = true
@@ -52,6 +51,7 @@ terminal.workspace = true
uuid.workspace = true
util.workspace = true
watch.workspace = true
+zed_credentials_provider.workspace = true
[target.'cfg(unix)'.dependencies]
libc.workspace = true
diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs
index b07bd4a09a2e8d75402c2fedfd4fe817154d2c2a..b2bc0a9a92cc638386f7ac1eff9f44c278ffe318 100644
--- a/crates/agent_servers/src/custom.rs
+++ b/crates/agent_servers/src/custom.rs
@@ -3,7 +3,6 @@ use acp_thread::AgentConnection;
use agent_client_protocol::schema as acp;
use anyhow::{Context as _, Result};
use collections::HashSet;
-use credentials_provider::CredentialsProvider;
use fs::Fs;
use gpui::{App, AppContext as _, Entity, Task};
use language_model::{ApiKey, EnvVar};
@@ -392,7 +391,7 @@ fn api_key_for_gemini_cli(cx: &mut App) -> Task> {
if let Some(key) = env_var.value {
return Task::ready(Ok(key));
}
- let credentials_provider = ::global(cx);
+ let credentials_provider = zed_credentials_provider::global(cx);
let api_url = google_ai::API_URL.to_string();
cx.spawn(async move |cx| {
Ok(
diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs
index c388569a2c705b91db9511a204a0894cf8158b63..aa9cdb2cc1bd9a09f8905c568aed4ce041cf5570 100644
--- a/crates/agent_servers/src/e2e_tests.rs
+++ b/crates/agent_servers/src/e2e_tests.rs
@@ -1,6 +1,7 @@
use crate::{AgentServer, AgentServerDelegate};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol::schema as acp;
+use client::RefreshLlmTokenListener;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::AppContext;
use gpui::{Entity, TestAppContext};
@@ -413,7 +414,8 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc {
cx.set_http_client(Arc::new(http_client));
let client = client::Client::production(cx);
let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
- language_model::init(user_store, client, cx);
+ language_model::init(cx);
+ RefreshLlmTokenListener::register(client.clone(), user_store, cx);
#[cfg(test)]
project::agent_server_store::AllAgentServersSettings::override_global(
diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs
index c131a686ce10df6c9984ab45bc25a7092cba9295..47ab761027562a7cd5b66fb59c6f2988abfa0701 100644
--- a/crates/agent_settings/src/agent_settings.rs
+++ b/crates/agent_settings/src/agent_settings.rs
@@ -176,6 +176,7 @@ pub struct AgentSettings {
pub use_modifier_to_send: bool,
pub message_editor_min_lines: usize,
pub show_turn_stats: bool,
+ pub show_merge_conflict_indicator: bool,
pub tool_permissions: ToolPermissions,
pub new_thread_location: NewThreadLocation,
}
@@ -629,6 +630,7 @@ impl Settings for AgentSettings {
use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
show_turn_stats: agent.show_turn_stats.unwrap(),
+ show_merge_conflict_indicator: agent.show_merge_conflict_indicator.unwrap(),
tool_permissions: compile_tool_permissions(agent.tool_permissions),
new_thread_location: agent.new_thread_location.unwrap_or_default(),
}
diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs
index 4e3dd63b0337f9be54b550f4f4a6a5ca2e7cdd42..e0df79ba4dfe226652818b120b7bfcc493c73b1e 100644
--- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs
+++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs
@@ -202,6 +202,7 @@ impl ModelInput {
.text(cx)
.parse::()
.map_err(|_| SharedString::from("Max Tokens must be a number"))?,
+ reasoning_effort: None,
capabilities: ModelCapabilities {
tools: self.capabilities.supports_tools.selected(),
images: self.capabilities.supports_images.selected(),
@@ -815,7 +816,7 @@ mod tests {
cx.set_global(store);
theme_settings::init(theme::LoadThemes::JustBase, cx);
- language_model::init_settings(cx);
+ language_model::init(cx);
editor::init(cx);
});
diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs
index d5cf63f6cdde9a85a54daaa29f8fc2c6833bdd77..7b70740dd1ac462614a9d08d9e48d7d13ac2ed32 100644
--- a/crates/agent_ui/src/agent_diff.rs
+++ b/crates/agent_ui/src/agent_diff.rs
@@ -1809,7 +1809,7 @@ mod tests {
cx.set_global(settings_store);
prompt_store::init(cx);
theme_settings::init(theme::LoadThemes::JustBase, cx);
- language_model::init_settings(cx);
+ language_model::init(cx);
});
let fs = FakeFs::new(cx.executor());
@@ -1966,7 +1966,7 @@ mod tests {
cx.set_global(settings_store);
prompt_store::init(cx);
theme_settings::init(theme::LoadThemes::JustBase, cx);
- language_model::init_settings(cx);
+ language_model::init(cx);
workspace::register_project_item::(cx);
});
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index 4792e0dfebb7e854dc99912b88d3f4bd321b4780..4250303884696e68f4026c682c17a95aa30cf6f5 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -2076,6 +2076,10 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context,
) {
+ if let Some(store) = ThreadMetadataStore::try_global(cx) {
+ store.update(cx, |store, cx| store.unarchive(&session_id, cx));
+ }
+
if let Some(conversation_view) = self.background_threads.remove(&session_id) {
self.set_active_view(
ActiveView::AgentThread { conversation_view },
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index 0c831b04a98540ca9461efa7a3875e1ece4b4054..57439cd47f99762b383664af77bd095ac7f03557 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -173,6 +173,22 @@ actions!(
ToggleThinkingEffortMenu,
/// Toggles fast mode for models that support it.
ToggleFastMode,
+ /// Scroll the output by one page up.
+ ScrollOutputPageUp,
+ /// Scroll the output by one page down.
+ ScrollOutputPageDown,
+ /// Scroll the output up by three lines.
+ ScrollOutputLineUp,
+ /// Scroll the output down by three lines.
+ ScrollOutputLineDown,
+ /// Scroll the output to the top.
+ ScrollOutputToTop,
+ /// Scroll the output to the bottom.
+ ScrollOutputToBottom,
+ /// Scroll the output to the previous user message.
+ ScrollOutputToPreviousMessage,
+ /// Scroll the output to the next user message.
+ ScrollOutputToNextMessage,
]
);
@@ -718,6 +734,7 @@ mod tests {
message_editor_min_lines: 1,
tool_permissions: Default::default(),
show_turn_stats: false,
+ show_merge_conflict_indicator: true,
new_thread_location: Default::default(),
sidebar_side: Default::default(),
thinking_display: Default::default(),
diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs
index d618cd2369f4d59834ef838638ae4f3b40de8d02..c42f14f0210f0fe424c2885ce7a1fecbf398634d 100644
--- a/crates/agent_ui/src/completion_provider.rs
+++ b/crates/agent_ui/src/completion_provider.rs
@@ -2144,7 +2144,7 @@ fn build_code_label_for_path(
.theme()
.syntax()
.highlight_id("variable")
- .map(HighlightId);
+ .map(HighlightId::new);
let mut label = CodeLabelBuilder::default();
label.push_str(file, None);
diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs
index 73196e8c01a9196bc6e13d86ffe840cc59e04cae..ded3dc80ba43d801774f8968b73cfbfc1933e196 100644
--- a/crates/agent_ui/src/conversation_view.rs
+++ b/crates/agent_ui/src/conversation_view.rs
@@ -85,8 +85,11 @@ use crate::{
AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector,
CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread,
OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce,
- RemoveFirstQueuedMessage, SendImmediately, SendNextQueuedMessage, ToggleFastMode,
- ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject,
+ RemoveFirstQueuedMessage, ScrollOutputLineDown, ScrollOutputLineUp, ScrollOutputPageDown,
+ ScrollOutputPageUp, ScrollOutputToBottom, ScrollOutputToNextMessage,
+ ScrollOutputToPreviousMessage, ScrollOutputToTop, SendImmediately, SendNextQueuedMessage,
+ ToggleFastMode, ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode,
+ UndoLastReject,
};
const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);
@@ -828,6 +831,8 @@ impl ConversationView {
let count = thread.read(cx).entries().len();
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(2048.0));
+ list_state.set_follow_mode(gpui::FollowMode::Tail);
+
entry_view_state.update(cx, |view_state, cx| {
for ix in 0..count {
view_state.sync_entry(ix, &thread, window, cx);
@@ -841,7 +846,7 @@ impl ConversationView {
if let Some(scroll_position) = thread.read(cx).ui_scroll_position() {
list_state.scroll_to(scroll_position);
} else {
- list_state.set_follow_tail(true);
+ list_state.scroll_to_end();
}
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
@@ -1257,9 +1262,11 @@ impl ConversationView {
AcpThreadEvent::EntryUpdated(index) => {
if let Some(active) = self.thread_view(&thread_id) {
let entry_view_state = active.read(cx).entry_view_state.clone();
+ let list_state = active.read(cx).list_state.clone();
entry_view_state.update(cx, |view_state, cx| {
- view_state.sync_entry(*index, thread, window, cx)
+ view_state.sync_entry(*index, thread, window, cx);
});
+ list_state.remeasure_items(*index..*index + 1);
active.update(cx, |active, cx| {
active.auto_expand_streaming_thought(cx);
});
@@ -1303,7 +1310,6 @@ impl ConversationView {
active.clear_auto_expand_tracking();
if active.list_state.is_following_tail() {
active.list_state.scroll_to_end();
- active.list_state.set_follow_tail(false);
}
}
active.sync_generating_indicator(cx);
@@ -1381,7 +1387,6 @@ impl ConversationView {
active.thread_retry_status.take();
if active.list_state.is_following_tail() {
active.list_state.scroll_to_end();
- active.list_state.set_follow_tail(false);
}
}
active.sync_generating_indicator(cx);
diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs
index e6fdb9a381c05c6563a8539ec7ea2c87262f87da..7aa5a5436de7ad726a4fc48b6dd8b55745ebfc4d 100644
--- a/crates/agent_ui/src/conversation_view/thread_view.rs
+++ b/crates/agent_ui/src/conversation_view/thread_view.rs
@@ -541,31 +541,15 @@ impl ThreadView {
let thread_view = cx.entity().downgrade();
this.list_state
- .set_scroll_handler(move |event, _window, cx| {
+ .set_scroll_handler(move |_event, _window, cx| {
let list_state = list_state_for_scroll.clone();
let thread_view = thread_view.clone();
- let is_following_tail = event.is_following_tail;
// N.B. We must defer because the scroll handler is called while the
// ListState's RefCell is mutably borrowed. Reading logical_scroll_top()
// directly would panic from a double borrow.
cx.defer(move |cx| {
let scroll_top = list_state.logical_scroll_top();
let _ = thread_view.update(cx, |this, cx| {
- if !is_following_tail {
- let is_at_bottom = {
- let current_offset =
- list_state.scroll_px_offset_for_scrollbar().y.abs();
- let max_offset = list_state.max_offset_for_scrollbar().y;
- current_offset >= max_offset - px(1.0)
- };
-
- let is_generating =
- matches!(this.thread.read(cx).status(), ThreadStatus::Generating);
-
- if is_at_bottom && is_generating {
- list_state.set_follow_tail(true);
- }
- }
if let Some(thread) = this.as_native_thread(cx) {
thread.update(cx, |thread, _cx| {
thread.set_ui_scroll_position(Some(scroll_top));
@@ -832,13 +816,10 @@ impl ThreadView {
}
}
}));
- if self.parent_id.is_none() {
- self.suppress_merge_conflict_notification(cx);
- }
generation
}
- pub fn stop_turn(&mut self, generation: usize, cx: &mut Context) {
+ pub fn stop_turn(&mut self, generation: usize, _cx: &mut Context) {
if self.turn_fields.turn_generation != generation {
return;
}
@@ -849,25 +830,6 @@ impl ThreadView {
.map(|started| started.elapsed());
self.turn_fields.last_turn_tokens = self.turn_fields.turn_tokens.take();
self.turn_fields._turn_timer_task = None;
- if self.parent_id.is_none() {
- self.unsuppress_merge_conflict_notification(cx);
- }
- }
-
- fn suppress_merge_conflict_notification(&self, cx: &mut Context) {
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.suppress_notification(&workspace::merge_conflict_notification_id(), cx);
- })
- .ok();
- }
-
- fn unsuppress_merge_conflict_notification(&self, cx: &mut Context) {
- self.workspace
- .update(cx, |workspace, _cx| {
- workspace.unsuppress(workspace::merge_conflict_notification_id());
- })
- .ok();
}
pub fn update_turn_tokens(&mut self, cx: &App) {
@@ -1077,7 +1039,7 @@ impl ThreadView {
})?;
let _ = this.update(cx, |this, cx| {
- this.list_state.set_follow_tail(true);
+ this.list_state.scroll_to_end();
cx.notify();
});
@@ -4978,6 +4940,105 @@ impl ThreadView {
cx.notify();
}
+ fn scroll_output_page_up(
+ &mut self,
+ _: &ScrollOutputPageUp,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let page_height = self.list_state.viewport_bounds().size.height;
+ self.list_state.scroll_by(-page_height * 0.9);
+ cx.notify();
+ }
+
+ fn scroll_output_page_down(
+ &mut self,
+ _: &ScrollOutputPageDown,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let page_height = self.list_state.viewport_bounds().size.height;
+ self.list_state.scroll_by(page_height * 0.9);
+ cx.notify();
+ }
+
+ fn scroll_output_line_up(
+ &mut self,
+ _: &ScrollOutputLineUp,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ self.list_state.scroll_by(-window.line_height() * 3.);
+ cx.notify();
+ }
+
+ fn scroll_output_line_down(
+ &mut self,
+ _: &ScrollOutputLineDown,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ self.list_state.scroll_by(window.line_height() * 3.);
+ cx.notify();
+ }
+
+ fn scroll_output_to_top(
+ &mut self,
+ _: &ScrollOutputToTop,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ self.scroll_to_top(cx);
+ }
+
+ fn scroll_output_to_bottom(
+ &mut self,
+ _: &ScrollOutputToBottom,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ self.scroll_to_end(cx);
+ }
+
+ fn scroll_output_to_previous_message(
+ &mut self,
+ _: &ScrollOutputToPreviousMessage,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let entries = self.thread.read(cx).entries();
+ let current_ix = self.list_state.logical_scroll_top().item_ix;
+ if let Some(target_ix) = (0..current_ix)
+ .rev()
+ .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
+ {
+ self.list_state.scroll_to(ListOffset {
+ item_ix: target_ix,
+ offset_in_item: px(0.),
+ });
+ cx.notify();
+ }
+ }
+
+ fn scroll_output_to_next_message(
+ &mut self,
+ _: &ScrollOutputToNextMessage,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let entries = self.thread.read(cx).entries();
+ let current_ix = self.list_state.logical_scroll_top().item_ix;
+ if let Some(target_ix) = (current_ix + 1..entries.len())
+ .find(|&i| matches!(entries.get(i), Some(AgentThreadEntry::UserMessage(_))))
+ {
+ self.list_state.scroll_to(ListOffset {
+ item_ix: target_ix,
+ offset_in_item: px(0.),
+ });
+ cx.notify();
+ }
+ }
+
pub fn open_thread_as_markdown(
&self,
workspace: Entity,
@@ -8541,6 +8602,14 @@ impl Render for ThreadView {
.on_action(cx.listener(Self::handle_toggle_command_pattern))
.on_action(cx.listener(Self::open_permission_dropdown))
.on_action(cx.listener(Self::open_add_context_menu))
+ .on_action(cx.listener(Self::scroll_output_page_up))
+ .on_action(cx.listener(Self::scroll_output_page_down))
+ .on_action(cx.listener(Self::scroll_output_line_up))
+ .on_action(cx.listener(Self::scroll_output_line_down))
+ .on_action(cx.listener(Self::scroll_output_to_top))
+ .on_action(cx.listener(Self::scroll_output_to_bottom))
+ .on_action(cx.listener(Self::scroll_output_to_previous_message))
+ .on_action(cx.listener(Self::scroll_output_to_next_message))
.on_action(cx.listener(|this, _: &ToggleFastMode, _window, cx| {
this.toggle_fast_mode(cx);
}))
@@ -8728,7 +8797,7 @@ pub(crate) fn open_link(
.open_path(path, None, true, window, cx)
.detach_and_log_err(cx);
}
- MentionUri::PastedImage => {}
+ MentionUri::PastedImage { .. } => {}
MentionUri::Directory { abs_path } => {
let project = workspace.project();
let Some(entry_id) = project.update(cx, |project, cx| {
diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs
index 20e0b702978b7e72a8526b03570854965335310c..39d70790e0d4a18554b2a1c11510e529d921cd1b 100644
--- a/crates/agent_ui/src/inline_assistant.rs
+++ b/crates/agent_ui/src/inline_assistant.rs
@@ -2025,7 +2025,7 @@ fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) {
pub mod evals {
use crate::InlineAssistant;
use agent::ThreadStore;
- use client::{Client, UserStore};
+ use client::{Client, RefreshLlmTokenListener, UserStore};
use editor::{Editor, MultiBuffer, MultiBufferOffset};
use eval_utils::{EvalOutput, NoProcessor};
use fs::FakeFs;
@@ -2091,7 +2091,8 @@ pub mod evals {
client::init(&client, cx);
workspace::init(app_state.clone(), cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
- language_model::init(user_store.clone(), client.clone(), cx);
+ language_model::init(cx);
+ RefreshLlmTokenListener::register(client.clone(), user_store.clone(), cx);
language_models::init(user_store, client.clone(), cx);
cx.set_global(inline_assistant);
diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs
index 1bf524c454ecbca86af9573a77937d2466c36de9..9ed3649a3b15566b1213fa4871426c1a93b7f5b4 100644
--- a/crates/agent_ui/src/mention_set.rs
+++ b/crates/agent_ui/src/mention_set.rs
@@ -154,7 +154,7 @@ impl MentionSet {
MentionUri::Selection { abs_path: None, .. } => Task::ready(Err(anyhow!(
"Untitled buffer selection mentions are not supported for paste"
))),
- MentionUri::PastedImage
+ MentionUri::PastedImage { .. }
| MentionUri::TerminalSelection { .. }
| MentionUri::MergeConflict { .. } => {
Task::ready(Err(anyhow!("Unsupported mention URI type for paste")))
@@ -283,7 +283,7 @@ impl MentionSet {
include_errors,
include_warnings,
} => self.confirm_mention_for_diagnostics(include_errors, include_warnings, cx),
- MentionUri::PastedImage => {
+ MentionUri::PastedImage { .. } => {
debug_panic!("pasted image URI should not be included in completions");
Task::ready(Err(anyhow!(
"pasted imaged URI should not be included in completions"
@@ -739,9 +739,11 @@ pub(crate) async fn insert_images_as_context(
return;
}
- let replacement_text = MentionUri::PastedImage.as_link().to_string();
-
for (image, name) in images {
+ let mention_uri = MentionUri::PastedImage {
+ name: name.to_string(),
+ };
+ let replacement_text = mention_uri.as_link().to_string();
let Some((text_anchor, multibuffer_anchor)) = editor
.update_in(cx, |editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
@@ -804,7 +806,13 @@ pub(crate) async fn insert_images_as_context(
.shared();
mention_set.update(cx, |mention_set, _cx| {
- mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
+ mention_set.insert_mention(
+ crease_id,
+ MentionUri::PastedImage {
+ name: name.to_string(),
+ },
+ task.clone(),
+ )
});
if task
@@ -873,7 +881,7 @@ pub(crate) fn paste_images_as_context(
Some(window.spawn(cx, async move |mut cx| {
use itertools::Itertools;
- let default_name: SharedString = MentionUri::PastedImage.name().into();
+ let default_name: SharedString = "Image".into();
let (mut images, paths): (Vec<(gpui::Image, SharedString)>, Vec<_>) = clipboard
.into_entries()
.filter_map(|entry| match entry {
diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs
index 0edca0ad52b9846f2c4ad28ad3b2bea18d8dd08b..8bd543913b3559e221ceef5fcca5b542a3545767 100644
--- a/crates/agent_ui/src/message_editor.rs
+++ b/crates/agent_ui/src/message_editor.rs
@@ -261,7 +261,7 @@ async fn resolve_pasted_context_items(
) -> (Vec, Vec>) {
let mut items = Vec::new();
let mut added_worktrees = Vec::new();
- let default_image_name: SharedString = MentionUri::PastedImage.name().into();
+ let default_image_name: SharedString = "Image".into();
for entry in entries {
match entry {
@@ -812,7 +812,9 @@ impl MessageEditor {
)
.uri(match uri {
MentionUri::File { .. } => Some(uri.to_uri().to_string()),
- MentionUri::PastedImage => None,
+ MentionUri::PastedImage { .. } => {
+ Some(uri.to_uri().to_string())
+ }
other => {
debug_panic!(
"unexpected mention uri for image: {:?}",
@@ -1638,7 +1640,9 @@ impl MessageEditor {
let mention_uri = if let Some(uri) = uri {
MentionUri::parse(&uri, path_style)
} else {
- Ok(MentionUri::PastedImage)
+ Ok(MentionUri::PastedImage {
+ name: "Image".to_string(),
+ })
};
let Some(mention_uri) = mention_uri.log_err() else {
continue;
@@ -4074,6 +4078,11 @@ mod tests {
&mut cx,
);
+ let image_name = temporary_image_path
+ .file_name()
+ .and_then(|n| n.to_str())
+ .unwrap_or("Image")
+ .to_string();
std::fs::remove_file(&temporary_image_path).expect("remove temp png");
let expected_file_uri = MentionUri::File {
@@ -4081,12 +4090,16 @@ mod tests {
}
.to_uri()
.to_string();
- let expected_image_uri = MentionUri::PastedImage.to_uri().to_string();
+ let expected_image_uri = MentionUri::PastedImage {
+ name: image_name.clone(),
+ }
+ .to_uri()
+ .to_string();
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
- format!("[@Image]({expected_image_uri}) [@file.txt]({expected_file_uri}) ")
+ format!("[@{image_name}]({expected_image_uri}) [@file.txt]({expected_file_uri}) ")
);
});
@@ -4094,7 +4107,7 @@ mod tests {
assert_eq!(contents.len(), 2);
assert!(contents.iter().any(|(uri, mention)| {
- *uri == MentionUri::PastedImage && matches!(mention, Mention::Image(_))
+ matches!(uri, MentionUri::PastedImage { .. }) && matches!(mention, Mention::Image(_))
}));
assert!(contents.iter().any(|(uri, mention)| {
*uri == MentionUri::File {
diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs
index 1bad3c45e4dece2397a2e026d659fd0fad043a24..963e32af55fda90f49edb0787f7327190c92681f 100644
--- a/crates/agent_ui/src/profile_selector.rs
+++ b/crates/agent_ui/src/profile_selector.rs
@@ -90,6 +90,7 @@ impl ProfileSelector {
if let Some((next_profile_id, _)) = profiles.get_index(next_index) {
self.provider.set_profile(next_profile_id.clone(), cx);
+ cx.notify();
}
}
diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs
index c67049d4d58e62b7b63c3acbad07ac9920568d1d..ce32c2e33f210b6a8371890efb2a4e60ec89e7a9 100644
--- a/crates/agent_ui/src/threads_archive_view.rs
+++ b/crates/agent_ui/src/threads_archive_view.rs
@@ -91,14 +91,16 @@ impl TimeBucket {
}
fn fuzzy_match_positions(query: &str, text: &str) -> Option> {
- let query = query.to_lowercase();
- let text_lower = text.to_lowercase();
let mut positions = Vec::new();
let mut query_chars = query.chars().peekable();
- for (i, c) in text_lower.chars().enumerate() {
- if query_chars.peek() == Some(&c) {
- positions.push(i);
- query_chars.next();
+ for (byte_idx, candidate_char) in text.char_indices() {
+ if let Some(&query_char) = query_chars.peek() {
+ if candidate_char.eq_ignore_ascii_case(&query_char) {
+ positions.push(byte_idx);
+ query_chars.next();
+ }
+ } else {
+ break;
}
}
if query_chars.peek().is_none() {
@@ -1283,3 +1285,59 @@ impl PickerDelegate for ProjectPickerDelegate {
)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_fuzzy_match_positions_returns_byte_indices() {
+ // "🔥abc" — the fire emoji is 4 bytes, so 'a' starts at byte 4, 'b' at 5, 'c' at 6.
+ let text = "🔥abc";
+ let positions = fuzzy_match_positions("ab", text).expect("should match");
+ assert_eq!(positions, vec![4, 5]);
+
+ // Verify positions are valid char boundaries (this is the assertion that
+ // panicked before the fix).
+ for &pos in &positions {
+ assert!(
+ text.is_char_boundary(pos),
+ "position {pos} is not a valid UTF-8 boundary in {text:?}"
+ );
+ }
+ }
+
+ #[test]
+ fn test_fuzzy_match_positions_ascii_still_works() {
+ let positions = fuzzy_match_positions("he", "hello").expect("should match");
+ assert_eq!(positions, vec![0, 1]);
+ }
+
+ #[test]
+ fn test_fuzzy_match_positions_case_insensitive() {
+ let positions = fuzzy_match_positions("HE", "hello").expect("should match");
+ assert_eq!(positions, vec![0, 1]);
+ }
+
+ #[test]
+ fn test_fuzzy_match_positions_no_match() {
+ assert!(fuzzy_match_positions("xyz", "hello").is_none());
+ }
+
+ #[test]
+ fn test_fuzzy_match_positions_multi_byte_interior() {
+ // "café" — 'é' is 2 bytes (0xC3 0xA9), so 'f' starts at byte 4, 'é' at byte 5.
+ let text = "café";
+ let positions = fuzzy_match_positions("fé", text).expect("should match");
+ // 'c'=0, 'a'=1, 'f'=2, 'é'=3..4 — wait, let's verify:
+ // Actually: c=1 byte, a=1 byte, f=1 byte, é=2 bytes
+ // So byte positions: c=0, a=1, f=2, é=3
+ assert_eq!(positions, vec![2, 3]);
+ for &pos in &positions {
+ assert!(
+ text.is_char_boundary(pos),
+ "position {pos} is not a valid UTF-8 boundary in {text:?}"
+ );
+ }
+ }
+}
diff --git a/crates/agent_ui/src/ui/mention_crease.rs b/crates/agent_ui/src/ui/mention_crease.rs
index b13f8b6395dc19e8f4b073f40b5c5577d38ee42e..57d4f12f33af2f5d0d09c5fbbd07ae0fde092d91 100644
--- a/crates/agent_ui/src/ui/mention_crease.rs
+++ b/crates/agent_ui/src/ui/mention_crease.rs
@@ -184,7 +184,7 @@ fn open_mention_uri(
MentionUri::Fetch { url } => {
cx.open_url(url.as_str());
}
- MentionUri::PastedImage
+ MentionUri::PastedImage { .. }
| MentionUri::Selection { abs_path: None, .. }
| MentionUri::Diagnostics { .. }
| MentionUri::TerminalSelection { .. }
diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs
index 1a3ce059b8116ac7438f3eb0330b47660cc863de..d8da78c53210230597dab49ce297d9fa694e62f1 100644
--- a/crates/cli/src/cli.rs
+++ b/crates/cli/src/cli.rs
@@ -21,6 +21,7 @@ pub enum CliRequest {
reuse: bool,
env: Option>,
user_data_dir: Option,
+ dev_container: bool,
},
}
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index b8af5896285d3080ca3320a5909b3f58f72de643..41f2d14c1908ac18e7ea297eef19d8d9bd1cf8b5 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -118,6 +118,12 @@ struct Args {
/// Will attempt to give the correct command to run
#[arg(long)]
system_specs: bool,
+ /// Open the project in a dev container.
+ ///
+ /// Automatically triggers "Reopen in Dev Container" if a `.devcontainer/`
+ /// configuration is found in the project directory.
+ #[arg(long)]
+ dev_container: bool,
/// Pairs of file paths to diff. Can be specified multiple times.
/// When directories are provided, recurses into them and shows all changed files in a single multi-diff view.
#[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
@@ -670,6 +676,7 @@ fn main() -> Result<()> {
reuse: args.reuse,
env,
user_data_dir: user_data_dir_for_thread,
+ dev_container: args.dev_container,
})?;
while let Ok(response) = rx.recv() {
diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml
index 1edbb3399e4332e2ebd23f812c66697bda72d587..7bbaccb22e0e6c7508240186103e216f83be2f0c 100644
--- a/crates/client/Cargo.toml
+++ b/crates/client/Cargo.toml
@@ -22,6 +22,7 @@ base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
cloud_api_client.workspace = true
+cloud_api_types.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
credentials_provider.workspace = true
@@ -35,6 +36,7 @@ gpui_tokio.workspace = true
http_client.workspace = true
http_client_tls.workspace = true
httparse = "1.10"
+language_model.workspace = true
log.workspace = true
parking_lot.workspace = true
paths.workspace = true
@@ -60,6 +62,7 @@ tokio.workspace = true
url.workspace = true
util.workspace = true
worktree.workspace = true
+zed_credentials_provider.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }
diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs
index 6a11a6b924eed3dfd79ff379638ed4085e2b7bcb..dfd9963a0ee52d167f8d4edb0b850f4debed7fd4 100644
--- a/crates/client/src/client.rs
+++ b/crates/client/src/client.rs
@@ -1,6 +1,7 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
+mod llm_token;
mod proxy;
pub mod telemetry;
pub mod user;
@@ -13,8 +14,9 @@ use async_tungstenite::tungstenite::{
http::{HeaderValue, Request, StatusCode},
};
use clock::SystemClock;
-use cloud_api_client::CloudApiClient;
use cloud_api_client::websocket_protocol::MessageToClient;
+use cloud_api_client::{ClientApiError, CloudApiClient};
+use cloud_api_types::OrganizationId;
use credentials_provider::CredentialsProvider;
use feature_flags::FeatureFlagAppExt as _;
use futures::{
@@ -24,6 +26,7 @@ use futures::{
};
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
use http_client::{HttpClient, HttpClientWithUrl, http, read_proxy_from_env};
+use language_model::LlmApiToken;
use parking_lot::{Mutex, RwLock};
use postage::watch;
use proxy::connect_proxy_stream;
@@ -51,6 +54,7 @@ use tokio::net::TcpStream;
use url::Url;
use util::{ConnectionResult, ResultExt};
+pub use llm_token::*;
pub use rpc::*;
pub use telemetry_events::Event;
pub use user::*;
@@ -339,7 +343,7 @@ pub struct ClientCredentialsProvider {
impl ClientCredentialsProvider {
pub fn new(cx: &App) -> Self {
Self {
- provider: ::global(cx),
+ provider: zed_credentials_provider::global(cx),
}
}
@@ -568,6 +572,10 @@ impl Client {
self.http.clone()
}
+ pub fn credentials_provider(&self) -> Arc {
+ self.credentials_provider.provider.clone()
+ }
+
pub fn cloud_client(&self) -> Arc {
self.cloud_client.clone()
}
@@ -1513,6 +1521,66 @@ impl Client {
})
}
+ pub async fn acquire_llm_token(
+ &self,
+ llm_token: &LlmApiToken,
+ organization_id: Option,
+ ) -> Result {
+ let system_id = self.telemetry().system_id().map(|x| x.to_string());
+ let cloud_client = self.cloud_client();
+ match llm_token
+ .acquire(&cloud_client, system_id, organization_id)
+ .await
+ {
+ Ok(token) => Ok(token),
+ Err(ClientApiError::Unauthorized) => {
+ self.request_sign_out();
+ Err(ClientApiError::Unauthorized).context("Failed to create LLM token")
+ }
+ Err(err) => Err(anyhow::Error::from(err)),
+ }
+ }
+
+ pub async fn refresh_llm_token(
+ &self,
+ llm_token: &LlmApiToken,
+ organization_id: Option,
+ ) -> Result {
+ let system_id = self.telemetry().system_id().map(|x| x.to_string());
+ let cloud_client = self.cloud_client();
+ match llm_token
+ .refresh(&cloud_client, system_id, organization_id)
+ .await
+ {
+ Ok(token) => Ok(token),
+ Err(ClientApiError::Unauthorized) => {
+ self.request_sign_out();
+ return Err(ClientApiError::Unauthorized).context("Failed to create LLM token");
+ }
+ Err(err) => return Err(anyhow::Error::from(err)),
+ }
+ }
+
+ pub async fn clear_and_refresh_llm_token(
+ &self,
+ llm_token: &LlmApiToken,
+ organization_id: Option,
+ ) -> Result {
+ let system_id = self.telemetry().system_id().map(|x| x.to_string());
+ let cloud_client = self.cloud_client();
+ match llm_token
+ .clear_and_refresh(&cloud_client, system_id, organization_id)
+ .await
+ {
+ Ok(token) => Ok(token),
+ Err(ClientApiError::Unauthorized) => {
+ self.request_sign_out();
+ return Err(ClientApiError::Unauthorized).context("Failed to create LLM token");
+ }
+ Err(err) => return Err(anyhow::Error::from(err)),
+ }
+ }
+
pub async fn sign_out(self: &Arc, cx: &AsyncApp) {
self.state.write().credentials = None;
self.cloud_client.clear_credentials();
diff --git a/crates/client/src/llm_token.rs b/crates/client/src/llm_token.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f62aa6dd4dc3462bc3a0f6f46c35f0e4e5499816
--- /dev/null
+++ b/crates/client/src/llm_token.rs
@@ -0,0 +1,116 @@
+use super::{Client, UserStore};
+use cloud_api_types::websocket_protocol::MessageToClient;
+use cloud_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, OUTDATED_LLM_TOKEN_HEADER_NAME};
+use gpui::{
+ App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _, Subscription,
+};
+use language_model::LlmApiToken;
+use std::sync::Arc;
+
+pub trait NeedsLlmTokenRefresh {
+ /// Returns whether the LLM token needs to be refreshed.
+ fn needs_llm_token_refresh(&self) -> bool;
+}
+
+impl NeedsLlmTokenRefresh for http_client::Response {
+ fn needs_llm_token_refresh(&self) -> bool {
+ self.headers().get(EXPIRED_LLM_TOKEN_HEADER_NAME).is_some()
+ || self.headers().get(OUTDATED_LLM_TOKEN_HEADER_NAME).is_some()
+ }
+}
+
+enum TokenRefreshMode {
+ Refresh,
+ ClearAndRefresh,
+}
+
+pub fn global_llm_token(cx: &App) -> LlmApiToken {
+ RefreshLlmTokenListener::global(cx)
+ .read(cx)
+ .llm_api_token
+ .clone()
+}
+
+struct GlobalRefreshLlmTokenListener(Entity);
+
+impl Global for GlobalRefreshLlmTokenListener {}
+
+pub struct LlmTokenRefreshedEvent;
+
+pub struct RefreshLlmTokenListener {
+ client: Arc,
+ user_store: Entity,
+ llm_api_token: LlmApiToken,
+ _subscription: Subscription,
+}
+
+impl EventEmitter for RefreshLlmTokenListener {}
+
+impl RefreshLlmTokenListener {
+ pub fn register(client: Arc, user_store: Entity, cx: &mut App) {
+ let listener = cx.new(|cx| RefreshLlmTokenListener::new(client, user_store, cx));
+ cx.set_global(GlobalRefreshLlmTokenListener(listener));
+ }
+
+ pub fn global(cx: &App) -> Entity {
+ GlobalRefreshLlmTokenListener::global(cx).0.clone()
+ }
+
+ fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self {
+ client.add_message_to_client_handler({
+ let this = cx.weak_entity();
+ move |message, cx| {
+ if let Some(this) = this.upgrade() {
+ Self::handle_refresh_llm_token(this, message, cx);
+ }
+ }
+ });
+
+ let subscription = cx.subscribe(&user_store, |this, _user_store, event, cx| {
+ if matches!(event, super::user::Event::OrganizationChanged) {
+ this.refresh(TokenRefreshMode::ClearAndRefresh, cx);
+ }
+ });
+
+ Self {
+ client,
+ user_store,
+ llm_api_token: LlmApiToken::default(),
+ _subscription: subscription,
+ }
+ }
+
+ fn refresh(&self, mode: TokenRefreshMode, cx: &mut Context) {
+ let client = self.client.clone();
+ let llm_api_token = self.llm_api_token.clone();
+ let organization_id = self
+ .user_store
+ .read(cx)
+ .current_organization()
+ .map(|organization| organization.id.clone());
+ cx.spawn(async move |this, cx| {
+ match mode {
+ TokenRefreshMode::Refresh => {
+ client
+ .refresh_llm_token(&llm_api_token, organization_id)
+ .await?;
+ }
+ TokenRefreshMode::ClearAndRefresh => {
+ client
+ .clear_and_refresh_llm_token(&llm_api_token, organization_id)
+ .await?;
+ }
+ }
+ this.update(cx, |_this, cx| cx.emit(LlmTokenRefreshedEvent))
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn handle_refresh_llm_token(this: Entity, message: &MessageToClient, cx: &mut App) {
+ match message {
+ MessageToClient::UserUpdated => {
+ this.update(cx, |this, cx| this.refresh(TokenRefreshMode::Refresh, cx));
+ }
+ }
+ }
+}
diff --git a/crates/codestral/Cargo.toml b/crates/codestral/Cargo.toml
index 0daaee8fb1420c76757ca898655e8dd1a5244d7e..801221d3128b8aa2d25175e086a741d5d85da626 100644
--- a/crates/codestral/Cargo.toml
+++ b/crates/codestral/Cargo.toml
@@ -22,6 +22,7 @@ log.workspace = true
serde.workspace = true
serde_json.workspace = true
text.workspace = true
+zed_credentials_provider.workspace = true
zeta_prompt.workspace = true
[dev-dependencies]
diff --git a/crates/codestral/src/codestral.rs b/crates/codestral/src/codestral.rs
index 3930e2e873a91618bfae456bc188bbd90ffa64b9..7685fa8f5b1eae9e98a621484602e199c2b76f96 100644
--- a/crates/codestral/src/codestral.rs
+++ b/crates/codestral/src/codestral.rs
@@ -48,9 +48,10 @@ pub fn codestral_api_key(cx: &App) -> Option> {
}
pub fn load_codestral_api_key(cx: &mut App) -> Task> {
+ let credentials_provider = zed_credentials_provider::global(cx);
let api_url = codestral_api_url(cx);
codestral_api_key_state(cx).update(cx, |key_state, cx| {
- key_state.load_if_needed(api_url, |s| s, cx)
+ key_state.load_if_needed(api_url, |s| s, credentials_provider, cx)
})
}
diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
index 2a87d617ebb19117ca87c00cc0887b07e416c8bd..75175372f24a83cfb50e8f87deae93e3f03e1a8a 100644
--- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
+++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql
@@ -65,6 +65,7 @@ CREATE TABLE "worktrees" (
"scan_id" INTEGER NOT NULL,
"is_complete" BOOL NOT NULL DEFAULT FALSE,
"completed_scan_id" INTEGER NOT NULL,
+ "root_repo_common_dir" VARCHAR,
PRIMARY KEY (project_id, id)
);
diff --git a/crates/collab/migrations/20251208000000_test_schema.sql b/crates/collab/migrations/20251208000000_test_schema.sql
index 8a56b9ce982f9a39a14bfc55fe8a34870ddea1c6..0110dd149b1143a3edcf76a1e0b18fbf1a22287c 100644
--- a/crates/collab/migrations/20251208000000_test_schema.sql
+++ b/crates/collab/migrations/20251208000000_test_schema.sql
@@ -484,7 +484,8 @@ CREATE TABLE public.worktrees (
visible boolean NOT NULL,
scan_id bigint NOT NULL,
is_complete boolean DEFAULT false NOT NULL,
- completed_scan_id bigint
+ completed_scan_id bigint,
+ root_repo_common_dir character varying
);
ALTER TABLE ONLY public.breakpoints ALTER COLUMN id SET DEFAULT nextval('public.breakpoints_id_seq'::regclass);
diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs
index 3e4c36631b29d35871cac101542bcc6904fbb271..44abc37af66e3f169d3af1a7d5e29063e382c620 100644
--- a/crates/collab/src/db.rs
+++ b/crates/collab/src/db.rs
@@ -559,6 +559,7 @@ pub struct RejoinedWorktree {
pub settings_files: Vec,
pub scan_id: u64,
pub completed_scan_id: u64,
+ pub root_repo_common_dir: Option,
}
pub struct LeftRoom {
@@ -638,6 +639,7 @@ pub struct Worktree {
pub settings_files: Vec,
pub scan_id: u64,
pub completed_scan_id: u64,
+ pub root_repo_common_dir: Option,
}
#[derive(Debug)]
diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs
index 3fc59f96332180d7d7bca4b6f71a345d9699e9e2..b1ea638072a30d6b881a711448223449aa9f53e2 100644
--- a/crates/collab/src/db/queries/projects.rs
+++ b/crates/collab/src/db/queries/projects.rs
@@ -87,6 +87,7 @@ impl Database {
visible: ActiveValue::set(worktree.visible),
scan_id: ActiveValue::set(0),
completed_scan_id: ActiveValue::set(0),
+ root_repo_common_dir: ActiveValue::set(None),
}
}))
.exec(&*tx)
@@ -203,6 +204,7 @@ impl Database {
visible: ActiveValue::set(worktree.visible),
scan_id: ActiveValue::set(0),
completed_scan_id: ActiveValue::set(0),
+ root_repo_common_dir: ActiveValue::set(None),
}))
.on_conflict(
OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])
@@ -266,6 +268,7 @@ impl Database {
ActiveValue::default()
},
abs_path: ActiveValue::set(update.abs_path.clone()),
+ root_repo_common_dir: ActiveValue::set(update.root_repo_common_dir.clone()),
..Default::default()
})
.exec(&*tx)
@@ -761,6 +764,7 @@ impl Database {
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
+ root_repo_common_dir: db_worktree.root_repo_common_dir,
legacy_repository_entries: Default::default(),
},
)
diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs
index 3197d142cba7a1969e6fdb9423dc94497f6ca53c..94e003fd2d27c97a53f66606d11ed2e15609b728 100644
--- a/crates/collab/src/db/queries/rooms.rs
+++ b/crates/collab/src/db/queries/rooms.rs
@@ -629,6 +629,7 @@ impl Database {
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
+ root_repo_common_dir: db_worktree.root_repo_common_dir,
};
let rejoined_worktree = rejoined_project
diff --git a/crates/collab/src/db/tables/worktree.rs b/crates/collab/src/db/tables/worktree.rs
index 46d9877dff152cdc3b30531606febec65595fec1..f67a9749a48e51fce81f97ad2faf8609c50a0204 100644
--- a/crates/collab/src/db/tables/worktree.rs
+++ b/crates/collab/src/db/tables/worktree.rs
@@ -15,6 +15,7 @@ pub struct Model {
pub scan_id: i64,
/// The last scan that fully completed.
pub completed_scan_id: i64,
+ pub root_repo_common_dir: Option,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs
index e05df1909db1e8afed0c06425d84799ff985f3c5..7ed488b0ba62c10326a0e2154f0d2ba895e20a4f 100644
--- a/crates/collab/src/rpc.rs
+++ b/crates/collab/src/rpc.rs
@@ -1485,6 +1485,7 @@ fn notify_rejoined_projects(
worktree_id: worktree.id,
abs_path: worktree.abs_path.clone(),
root_name: worktree.root_name,
+ root_repo_common_dir: worktree.root_repo_common_dir,
updated_entries: worktree.updated_entries,
removed_entries: worktree.removed_entries,
scan_id: worktree.scan_id,
@@ -1943,6 +1944,7 @@ async fn join_project(
worktree_id,
abs_path: worktree.abs_path.clone(),
root_name: worktree.root_name,
+ root_repo_common_dir: worktree.root_repo_common_dir,
updated_entries: worktree.entries,
removed_entries: Default::default(),
scan_id: worktree.scan_id,
diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs
index a64233caba014aa49bd64f98634b40abeef88e8e..fdaacd768444bd44d8414247f922f38afb7e81d5 100644
--- a/crates/collab/tests/integration/git_tests.rs
+++ b/crates/collab/tests/integration/git_tests.rs
@@ -1,4 +1,4 @@
-use std::path::{Path, PathBuf};
+use std::path::{self, Path, PathBuf};
use call::ActiveCall;
use client::RECEIVE_TIMEOUT;
@@ -17,6 +17,61 @@ use workspace::{MultiWorkspace, Workspace};
use crate::TestServer;
+#[gpui::test]
+async fn test_root_repo_common_dir_sync(
+ executor: BackgroundExecutor,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(executor.clone()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ // Set up a project whose root IS a git repository.
+ client_a
+ .fs()
+ .insert_tree(
+ path!("/project"),
+ json!({ ".git": {}, "file.txt": "content" }),
+ )
+ .await;
+
+ let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
+ executor.run_until_parked();
+
+ // Host should see root_repo_common_dir pointing to .git at the root.
+ let host_common_dir = project_a.read_with(cx_a, |project, cx| {
+ let worktree = project.worktrees(cx).next().unwrap();
+ worktree.read(cx).snapshot().root_repo_common_dir().cloned()
+ });
+ assert_eq!(
+ host_common_dir.as_deref(),
+ Some(path::Path::new(path!("/project/.git"))),
+ );
+
+ // Share the project and have client B join.
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+ executor.run_until_parked();
+
+ // Guest should see the same root_repo_common_dir as the host.
+ let guest_common_dir = project_b.read_with(cx_b, |project, cx| {
+ let worktree = project.worktrees(cx).next().unwrap();
+ worktree.read(cx).snapshot().root_repo_common_dir().cloned()
+ });
+ assert_eq!(
+ guest_common_dir, host_common_dir,
+ "guest should see the same root_repo_common_dir as host",
+ );
+}
+
fn collect_diff_stats(
panel: &gpui::Entity,
cx: &C,
diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs
index 91385b298dc661c4a79e4fb52d5be0f38672bff5..8d0cdf351163dadf0ac8cbf6a8dc04886f30f583 100644
--- a/crates/collab_ui/src/collab_panel.rs
+++ b/crates/collab_ui/src/collab_panel.rs
@@ -13,12 +13,13 @@ use db::kvp::KeyValueStore;
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
- AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent,
- Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement,
- KeyContext, ListOffset, ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
- Render, SharedString, Styled, Subscription, Task, TextStyle, WeakEntity, Window, actions,
- anchored, canvas, deferred, div, fill, list, point, prelude::*, px,
+ AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div,
+ Empty, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, KeyContext, ListOffset,
+ ListState, MouseDownEvent, Pixels, Point, PromptLevel, SharedString, Subscription, Task,
+ TextStyle, WeakEntity, Window, actions, anchored, canvas, deferred, div, fill, list, point,
+ prelude::*, px,
};
+
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
use project::{Fs, Project};
use rpc::{
@@ -43,6 +44,9 @@ use workspace::{
notifications::{DetachAndPromptErr, NotifyResultExt},
};
+const FILTER_OCCUPIED_CHANNELS_KEY: &str = "filter_occupied_channels";
+const FAVORITE_CHANNELS_KEY: &str = "favorite_channels";
+
actions!(
collab_panel,
[
@@ -243,7 +247,9 @@ pub struct CollabPanel {
fs: Arc,
focus_handle: FocusHandle,
channel_clipboard: Option,
- pending_serialization: Task