settings_ui: Remove feature flag for AI settings and adjust design (#59860)

Closes AI-159
Closes AI-434
Closes AI-435

Release Notes:

- Key agent-related settings now live in the settings editor, close to
all other settings available in Zed. This specifically includes the move
of LLM providers, external agents, and MCP servers to the settings
editor.

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
authored and GitHub committed 2026-07-01 04:25:40 +00:00
1 parent 03903ea28f
commit 1d49ff0d33
46 files changed
+1504 -4676

No files matched your search

-7
View File
@@ -1281,13 +1281,6 @@
"ctrl-enter": "menu::Confirm",
},
},
{
"context": "ContextServerToolsModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
},
},
{
"context": "OnboardingAiConfigurationModal",
"use_key_equivalents": true,
-13
View File
@@ -288,12 +288,6 @@
"alt-enter": "editor::Newline",
},
},
{
"context": "AgentConfiguration",
"bindings": {
"ctrl--": "pane::GoBack",
},
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
@@ -1379,13 +1373,6 @@
"cmd-enter": "menu::Confirm",
},
},
{
"context": "ContextServerToolsModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
},
},
{
"context": "OnboardingAiConfigurationModal",
"use_key_equivalents": true,
-7
View File
@@ -1303,13 +1303,6 @@
"ctrl-enter": "menu::Confirm",
},
},
{
"context": "ContextServerToolsModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
},
},
{
"context": "OnboardingAiConfigurationModal",
"use_key_equivalents": true,
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -3,6 +3,7 @@ use collections::HashMap;
use context_server::{ContextServerCommand, ContextServerId};
use editor::{Editor, EditorElement, EditorStyle};
use extension_host::ExtensionStore;
use gpui::{
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle,
Subscription, Task, TaskExt, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity,
@@ -402,7 +403,12 @@ fn resolve_context_server_extension(
return Task::ready(None);
};
let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx);
let extension = ExtensionStore::global(cx)
.read(cx)
.installed_extensions()
.iter()
.find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
.map(|(id, entry)| (id.clone(), entry.manifest.clone()));
cx.spawn(async move |cx| {
let installation = descriptor
.configuration(worktree_store, cx)
@@ -1,175 +0,0 @@
use agent::ContextServerRegistry;
use collections::HashMap;
use context_server::ContextServerId;
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
};
use ui::{Divider, DividerColor, Modal, ModalHeader, WithScrollbar, prelude::*};
use workspace::{ModalView, Workspace};
pub struct ConfigureContextServerToolsModal {
context_server_id: ContextServerId,
context_server_registry: Entity<ContextServerRegistry>,
focus_handle: FocusHandle,
expanded_tools: HashMap<SharedString, bool>,
scroll_handle: ScrollHandle,
}
impl ConfigureContextServerToolsModal {
fn new(
context_server_id: ContextServerId,
context_server_registry: Entity<ContextServerRegistry>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
Self {
context_server_id,
context_server_registry,
focus_handle: cx.focus_handle(),
expanded_tools: HashMap::default(),
scroll_handle: ScrollHandle::new(),
}
}
pub fn toggle(
context_server_id: ContextServerId,
context_server_registry: Entity<ContextServerRegistry>,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
workspace.toggle_modal(window, cx, |window, cx| {
Self::new(context_server_id, context_server_registry, window, cx)
});
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent)
}
fn render_modal_content(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let tools = self
.context_server_registry
.read(cx)
.tools_for_server(&self.context_server_id)
.collect::<Vec<_>>();
div()
.size_full()
.pb_2()
.child(
v_flex()
.id("modal_content")
.px_2()
.gap_1()
.max_h_128()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.children(tools.iter().enumerate().flat_map(|(index, tool)| {
let tool_name = tool.name();
let is_expanded = self
.expanded_tools
.get(tool_name.as_ref())
.copied()
.unwrap_or(false);
let icon = if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
};
let mut items = vec![
v_flex()
.child(
h_flex()
.id(format!("tool-header-{}", index))
.py_1()
.pl_1()
.pr_2()
.w_full()
.justify_between()
.rounded_sm()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.child(
Label::new(tool_name.clone())
.buffer_font(cx)
.size(LabelSize::Small),
)
.child(
Icon::new(icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.on_click(cx.listener({
move |this, _event, _window, _cx| {
let current = this
.expanded_tools
.get(tool_name.as_ref())
.copied()
.unwrap_or(false);
this.expanded_tools
.insert(tool_name.clone(), !current);
_cx.notify();
}
})),
)
.when(is_expanded, |this| {
this.child(
Label::new(tool.description()).color(Color::Muted).mx_1(),
)
})
.into_any_element(),
];
if index < tools.len() - 1 {
items.push(
h_flex()
.w_full()
.child(Divider::horizontal().color(DividerColor::BorderVariant))
.into_any_element(),
);
}
items
})),
)
.vertical_scrollbar_for(&self.scroll_handle, window, cx)
.into_any_element()
}
}
impl ModalView for ConfigureContextServerToolsModal {}
impl Focusable for ConfigureContextServerToolsModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for ConfigureContextServerToolsModal {}
impl Render for ConfigureContextServerToolsModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context("ContextServerToolsModal")
.occlude()
.elevation_3(cx)
.w(rems(34.))
.on_action(cx.listener(Self::cancel))
.track_focus(&self.focus_handle)
.child(
Modal::new("configure-context-server-tools", None::<ScrollHandle>)
.header(
ModalHeader::new()
.headline(format!("Tools from {}", self.context_server_id.0))
.show_dismiss_button(true),
)
.child(self.render_modal_content(window, cx)),
)
}
}
+15 -345
View File
@@ -20,7 +20,6 @@ use db::kvp::{Dismissable, KeyValueStore};
use itertools::Itertools;
use project::{AgentId, ProjectItem};
use serde::{Deserialize, Serialize};
use settings::{LanguageModelProviderSetting, LanguageModelSelection};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
@@ -49,7 +48,6 @@ use crate::{
LoadThreadFromClipboard, NewTerminalThread, NewThread, OpenActiveThreadAsMarkdown,
OpenAgentDiff, ResetFastModeWarnings, ResetTrialEndUpsell, ResetTrialUpsell,
ShowAllSidebarThreadMetadata, ShowThreadMetadata, ToggleNewThreadMenu, ToggleOptionsMenu,
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
conversation_view::{
AcpThreadViewEvent, RootThreadUpdated, ThreadView, reset_fast_mode_warnings,
},
@@ -70,9 +68,7 @@ use cloud_api_types::Plan;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use extension_host::ExtensionStore;
use feature_flags::{
AgentSettingsUiFeatureFlag, CreateThreadToolFeatureFlag, FeatureFlagAppExt as _,
};
use feature_flags::{CreateThreadToolFeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use futures::FutureExt as _;
@@ -1124,15 +1120,10 @@ impl From<AgentThread> for BaseView {
}
}
enum OverlayView {
Configuration,
}
enum VisibleSurface<'a> {
Uninitialized,
AgentThread(&'a Entity<ConversationView>),
Terminal(&'a Entity<TerminalView>),
Configuration(Option<&'a Entity<AgentConfiguration>>),
}
enum WhichFontSize {
@@ -1149,14 +1140,6 @@ impl BaseView {
}
}
impl OverlayView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
OverlayView::Configuration => WhichFontSize::None,
}
}
}
pub struct AgentPanel {
workspace: WeakEntity<Workspace>,
/// Workspace id is used as a database key
@@ -1168,12 +1151,9 @@ pub struct AgentPanel {
thread_store: Entity<ThreadStore>,
connection_store: Entity<AgentConnectionStore>,
context_server_registry: Entity<ContextServerRegistry>,
configuration: Option<Entity<AgentConfiguration>>,
configuration_subscription: Option<Subscription>,
focus_handle: FocusHandle,
base_view: BaseView,
last_created_entry_kind: AgentPanelEntryKind,
overlay_view: Option<OverlayView>,
draft_thread: Option<Entity<ConversationView>>,
retained_threads: HashMap<ThreadId, Entity<ConversationView>>,
terminals: HashMap<TerminalId, AgentTerminal>,
@@ -1568,15 +1548,12 @@ impl AgentPanel {
workspace_id,
base_view,
last_created_entry_kind: AgentPanelEntryKind::Thread,
overlay_view: None,
workspace,
user_store,
project: project.clone(),
fs: fs.clone(),
language_registry,
connection_store,
configuration: None,
configuration_subscription: None,
focus_handle: cx.focus_handle(),
context_server_registry,
draft_thread: None,
@@ -1752,7 +1729,6 @@ impl AgentPanel {
pub fn clear_base_view(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let old_view = std::mem::replace(&mut self.base_view, BaseView::Uninitialized);
self.retain_running_thread(old_view, cx);
self.clear_overlay_state();
self.activate_draft(false, AgentThreadSource::AgentPanel, window, cx);
self.serialize(cx);
cx.emit(AgentPanelEvent::ActiveViewChanged);
@@ -2928,15 +2904,7 @@ impl AgentPanel {
let draft = self.ensure_draft(source, window, cx);
if let BaseView::AgentThread { conversation_view } = &self.base_view {
if conversation_view.entity_id() == draft.entity_id() {
// If we're already viewing the draft as the base view but an
// overlay (e.g. Settings) is covering it, clear the overlay
// so the user actually sees the draft they asked for.
// Otherwise pressing "New Thread" from the Settings panel is
// a silent no-op because the early return below would leave
// the overlay on top of the draft.
if self.overlay_view.is_some() {
self.clear_overlay(focus, window, cx);
} else if focus {
if focus {
self.focus_handle(cx).focus(window, cx);
}
return;
@@ -3302,7 +3270,6 @@ impl AgentPanel {
}
if self.active_thread_id(cx) == Some(id) {
self.clear_overlay_state();
if activate_draft_after_remove {
self.activate_draft(false, AgentThreadSource::AgentPanel, window, cx);
} else {
@@ -3547,13 +3514,6 @@ impl AgentPanel {
})
}
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
if self.overlay_view.is_some() {
self.clear_overlay(true, window, cx);
cx.notify();
}
}
pub fn toggle_options_menu(
&mut self,
_: &ToggleOptionsMenu,
@@ -3671,57 +3631,13 @@ impl AgentPanel {
}
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// When the agent settings have been moved into the settings UI, the
// panel no longer shows its own configuration overlay. Instead, route to
// the settings UI at the LLM providers page (where the model selector's
// "Configure" button expects to land).
if cx.has_flag::<AgentSettingsUiFeatureFlag>() {
window.dispatch_action(
Box::new(zed_actions::OpenSettingsAt {
path: "llm_providers".to_string(),
target: None,
}),
cx,
);
return;
}
if matches!(self.overlay_view, Some(OverlayView::Configuration)) {
self.clear_overlay(true, window, cx);
return;
}
let agent_server_store = self.project.read(cx).agent_server_store().clone();
let context_server_store = self.project.read(cx).context_server_store();
let fs = self.fs.clone();
self.configuration = Some(cx.new(|cx| {
AgentConfiguration::new(
fs,
agent_server_store,
self.connection_store.clone(),
context_server_store,
self.context_server_registry.clone(),
self.language_registry.clone(),
self.workspace.clone(),
window,
cx,
)
}));
if let Some(configuration) = self.configuration.as_ref() {
self.configuration_subscription = Some(cx.subscribe_in(
configuration,
window,
Self::handle_agent_configuration_event,
));
}
self.set_overlay(OverlayView::Configuration, true, window, cx);
if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx).focus(window, cx);
}
window.dispatch_action(
Box::new(zed_actions::OpenSettingsPage {
page: "AI".to_string(),
target: None,
}),
cx,
);
}
pub(crate) fn open_active_thread_as_markdown(
@@ -4004,53 +3920,6 @@ impl AgentPanel {
.detach_and_log_err(cx);
}
fn handle_agent_configuration_event(
&mut self,
_entity: &Entity<AgentConfiguration>,
event: &AssistantConfigurationEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
AssistantConfigurationEvent::NewThread(provider) => {
if LanguageModelRegistry::read_global(cx)
.default_model()
.is_none_or(|model| model.provider.id() != provider.id())
&& let Some(model) = provider.default_model(cx)
{
update_settings_file(self.fs.clone(), cx, move |settings, _| {
let provider = model.provider_id().0.to_string();
let enable_thinking = model.supports_thinking();
let effort = model
.default_effort_level()
.map(|effort| effort.value.to_string());
let model = model.id().0.to_string();
settings
.agent
.get_or_insert_default()
.set_model(LanguageModelSelection {
provider: LanguageModelProviderSetting(provider),
model,
enable_thinking,
effort,
speed: None,
})
});
}
self.activate_new_thread(true, AgentThreadSource::AgentPanel, window, cx);
if let Some((thread, model)) = self
.active_native_agent_thread(cx)
.zip(provider.default_model(cx))
{
thread.update(cx, |thread, cx| {
thread.set_model(model, cx);
});
}
}
}
}
pub fn workspace_id(&self) -> Option<WorkspaceId> {
self.workspace_id
}
@@ -4283,8 +4152,6 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.clear_overlay_state();
let old_view = std::mem::replace(&mut self.base_view, new_view);
self.retain_running_thread(old_view, cx);
@@ -4305,35 +4172,6 @@ impl AgentPanel {
cx.emit(AgentPanelEvent::ActiveViewChanged);
}
fn set_overlay(
&mut self,
overlay: OverlayView,
focus: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.overlay_view = Some(overlay);
if focus {
self.focus_handle(cx).focus(window, cx);
}
cx.emit(AgentPanelEvent::ActiveViewChanged);
}
fn clear_overlay(&mut self, focus: bool, window: &mut Window, cx: &mut Context<Self>) {
self.clear_overlay_state();
if focus {
self.focus_handle(cx).focus(window, cx);
}
cx.emit(AgentPanelEvent::ActiveViewChanged);
}
fn clear_overlay_state(&mut self) {
self.overlay_view = None;
self.configuration_subscription = None;
self.configuration = None;
}
fn refresh_base_view_subscriptions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self._base_view_observation = match &self.base_view {
BaseView::AgentThread { conversation_view } => {
@@ -4386,14 +4224,6 @@ impl AgentPanel {
}
fn visible_surface(&self) -> VisibleSurface<'_> {
if let Some(overlay_view) = &self.overlay_view {
return match overlay_view {
OverlayView::Configuration => {
VisibleSurface::Configuration(self.configuration.as_ref())
}
};
}
match &self.base_view {
BaseView::Uninitialized => VisibleSurface::Uninitialized,
BaseView::AgentThread { conversation_view } => {
@@ -4407,15 +4237,8 @@ impl AgentPanel {
}
}
fn is_overlay_open(&self) -> bool {
self.overlay_view.is_some()
}
fn visible_font_size(&self) -> WhichFontSize {
self.overlay_view.as_ref().map_or_else(
|| self.base_view.which_font_size_used(),
OverlayView::which_font_size_used,
)
self.base_view.which_font_size_used()
}
fn subscribe_to_active_thread_view(
@@ -4499,7 +4322,6 @@ impl AgentPanel {
// Check if the active view already holds this thread.
if let BaseView::AgentThread { conversation_view } = &self.base_view {
if conversation_view.read(cx).thread_id == thread_id {
self.clear_overlay_state();
cx.emit(AgentPanelEvent::ActiveViewChanged);
return;
}
@@ -5052,13 +4874,6 @@ impl Focusable for AgentPanel {
VisibleSurface::Uninitialized => self.focus_handle.clone(),
VisibleSurface::AgentThread(conversation_view) => conversation_view.focus_handle(cx),
VisibleSurface::Terminal(terminal_view) => terminal_view.focus_handle(cx),
VisibleSurface::Configuration(configuration) => {
if let Some(configuration) = configuration {
configuration.focus_handle(cx)
} else {
self.focus_handle.clone()
}
}
}
}
}
@@ -5294,10 +5109,7 @@ impl AgentPanel {
}
fn destination_has_meaningful_state(&self, cx: &App) -> bool {
if self.overlay_view.is_some()
|| !self.retained_threads.is_empty()
|| !self.terminals.is_empty()
{
if !self.retained_threads.is_empty() || !self.terminals.is_empty() {
return true;
}
@@ -5578,9 +5390,7 @@ impl AgentPanel {
Label::new("Terminal").into_any_element()
}
}
VisibleSurface::Configuration(_) => {
Label::new("Settings").truncate().into_any_element()
}
VisibleSurface::Uninitialized => Label::new("Agent").truncate().into_any_element(),
};
@@ -5750,8 +5560,7 @@ impl AgentPanel {
if !showing_terminal {
menu = menu
.header("MCP Servers")
.action("Add Custom Server…", Box::new(AddContextServer::local()))
.action("Add Remote Server…", Box::new(AddContextServer::remote()))
.action("Add Server…", Box::new(AddContextServer::local()))
.action(
"Install New Servers…",
Box::new(zed_actions::Extensions {
@@ -5844,21 +5653,6 @@ impl AgentPanel {
})
}
fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
IconButton::new("go-back", IconName::ArrowLeft)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
this.go_back(&workspace::GoBack, window, cx);
}))
.tooltip({
move |_window, cx| {
Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
}
})
}
fn render_no_project_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
@@ -6139,15 +5933,12 @@ impl AgentPanel {
};
enum ToolbarMode {
Overlay,
Terminal,
EmptyThread,
ActiveThread,
}
let mode = if self.is_overlay_open() {
ToolbarMode::Overlay
} else if matches!(self.base_view, BaseView::Terminal { .. }) {
let mode = if matches!(self.base_view, BaseView::Terminal { .. }) {
ToolbarMode::Terminal
} else if self.active_thread_has_messages(cx) {
ToolbarMode::ActiveThread
@@ -6231,11 +6022,7 @@ impl AgentPanel {
.overflow_hidden()
.gap(DynamicSpacing::Base04.rems(cx))
.pl(DynamicSpacing::Base04.rems(cx))
.child(if matches!(mode, ToolbarMode::Overlay) {
self.render_toolbar_back_button(cx).into_any_element()
} else {
selected_agent.into_any_element()
})
.child(selected_agent.into_any_element())
.child(match empty_thread_title {
Some(title) => title,
None => self.render_title_view(window, cx),
@@ -6574,7 +6361,6 @@ impl Render for AgentPanel {
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown))
.on_action(cx.listener(Self::manage_skills))
.on_action(cx.listener(Self::go_back))
.on_action(cx.listener(Self::toggle_options_menu))
.on_action(cx.listener(Self::increase_font_size))
.on_action(cx.listener(Self::decrease_font_size))
@@ -6607,9 +6393,6 @@ impl Render for AgentPanel {
VisibleSurface::Terminal(terminal_view) => parent
.child(terminal_view.clone())
.child(self.render_drag_target(cx)),
VisibleSurface::Configuration(configuration) => {
parent.children(configuration.cloned())
}
})
.children(self.render_trial_end_upsell(window, cx));
@@ -9537,63 +9320,6 @@ mod tests {
});
}
#[gpui::test]
async fn test_new_thread_dismisses_settings_overlay(cx: &mut TestAppContext) {
let (panel, mut cx) = setup_panel(cx).await;
// Put the panel on its ephemeral new-draft view so the base view
// already contains the draft that `NewThread` would activate.
panel.update_in(&mut cx, |panel, window, cx| {
panel.activate_new_thread(true, AgentThreadSource::AgentPanel, window, cx);
});
cx.run_until_parked();
panel.read_with(&cx, |panel, cx| {
assert!(
panel.active_view_is_new_draft(cx),
"precondition: base view should be the ephemeral draft"
);
assert!(!panel.is_overlay_open());
});
// Simulate the Settings overlay being open on top of the draft.
// We don't go through `open_configuration` here because it would
// build provider configuration views, which call into
// `LanguageModelProvider::configuration_view` — unimplemented for
// the fake provider used in tests. The bug being exercised lives
// entirely in the overlay/base-view bookkeeping, so toggling the
// overlay flag directly is sufficient.
panel.update_in(&mut cx, |panel, window, cx| {
panel.set_overlay(OverlayView::Configuration, true, window, cx);
});
cx.run_until_parked();
panel.read_with(&cx, |panel, _cx| {
assert!(
panel.is_overlay_open(),
"precondition: Settings overlay should be open"
);
});
// Dispatching `NewThread` while Settings is open must dismiss the
// overlay so the user actually sees the new thread. Previously
// this was a silent no-op: `activate_draft` early-returned without
// clearing the overlay because the base view already held the
// draft.
panel.update_in(&mut cx, |panel, window, cx| {
panel.new_thread(&NewThread, window, cx);
});
cx.run_until_parked();
panel.read_with(&cx, |panel, cx| {
assert!(
!panel.is_overlay_open(),
"Settings overlay should be dismissed when invoking NewThread"
);
assert!(panel.active_view_is_new_draft(cx));
});
}
#[gpui::test]
async fn test_terminal_title_omits_placeholder_title(cx: &mut TestAppContext) {
let (panel, mut cx) = setup_panel(cx).await;
@@ -10273,62 +9999,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_terminal_bell_notifies_when_configuration_overlay_covers_terminal(
cx: &mut TestAppContext,
) {
let (panel, mut cx) = setup_visible_panel(cx).await;
let terminal_id = panel
.update_in(&mut cx, |panel, window, cx| {
panel.insert_test_terminal("Claude", true, window, cx)
})
.expect("test terminal should be inserted");
cx.run_until_parked();
panel.update_in(&mut cx, |panel, window, cx| {
panel.set_overlay(OverlayView::Configuration, true, window, cx);
});
panel.update(&mut cx, |panel, cx| {
panel.emit_test_terminal_bell(terminal_id, cx);
});
cx.run_until_parked();
panel.read_with(&cx, |panel, cx| {
let terminal = panel
.terminals(cx)
.into_iter()
.find(|terminal| terminal.id == terminal_id)
.expect("terminal should remain in the panel");
assert!(terminal.has_notification);
});
cx.windows()
.iter()
.find_map(|window| window.downcast::<AgentNotification>())
.expect("covered terminal bell should show a notification");
}
#[gpui::test]
async fn test_thread_notification_shows_when_configuration_overlay_covers_thread(
cx: &mut TestAppContext,
) {
let (panel, mut cx) = setup_visible_panel(cx).await;
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Default response".into()),
)]);
open_thread_with_connection(&panel, connection, &mut cx);
panel.update_in(&mut cx, |panel, window, cx| {
panel.set_overlay(OverlayView::Configuration, true, window, cx);
});
send_message(&panel, &mut cx);
cx.windows()
.iter()
.find_map(|window| window.downcast::<AgentNotification>())
.expect("covered thread should show a notification");
}
#[gpui::test]
async fn test_terminal_bell_marks_without_popup_when_sidebar_open(cx: &mut TestAppContext) {
let (panel, mut cx) = setup_visible_panel(cx).await;
+54 -38
View File
@@ -466,6 +466,10 @@ pub struct ConfigurationView {
copilot_status: Option<Status>,
is_authenticated: Box<dyn Fn(&mut App) -> bool + 'static>,
edit_prediction: bool,
/// When `true`, renders a compact control suitable for an inline settings
/// row: no explanatory labels (those live in the row's left column) and
/// content-sized buttons instead of full-width ones.
compact: bool,
_subscription: Option<Subscription>,
}
@@ -487,6 +491,7 @@ impl ConfigurationView {
copilot_status: copilot.as_ref().map(|copilot| copilot.0.read(cx).status()),
is_authenticated: Box::new(is_authenticated),
edit_prediction: matches!(mode, ConfigurationMode::EditPrediction),
compact: false,
_subscription: copilot.as_ref().map(|copilot| {
cx.observe(&copilot.0, |this, model, cx| {
this.copilot_status = Some(model.read(cx).status());
@@ -495,6 +500,14 @@ impl ConfigurationView {
}),
}
}
/// Renders the view compactly for an inline settings row (no labels,
/// content-sized buttons). The explanatory copy is expected to be shown
/// elsewhere (e.g. the row's left column).
pub fn compact(mut self) -> Self {
self.compact = true;
self
}
}
impl ConfigurationView {
@@ -536,34 +549,34 @@ impl ConfigurationView {
edit_prediction: bool,
) -> impl IntoElement {
Button::new("loading_button", label)
.full_width()
.map(|this| {
if edit_prediction || self.compact {
this.size(ButtonSize::Medium)
} else {
this.full_width()
}
})
.disabled(true)
.loading(true)
.style(ButtonStyle::Outlined)
.when(edit_prediction, |this| this.size(ButtonSize::Medium))
}
fn render_sign_in_button(&self, edit_prediction: bool) -> impl IntoElement {
let label = if edit_prediction {
"Sign in to GitHub"
} else {
"Sign in to use GitHub Copilot"
"Sign In"
};
Button::new("sign_in", label)
.map(|this| {
if edit_prediction {
if edit_prediction || self.compact {
this.size(ButtonSize::Medium)
} else {
this.full_width()
}
})
.style(ButtonStyle::Outlined)
.start_icon(
Icon::new(IconName::Github)
.size(IconSize::Small)
.color(Color::Muted),
)
.when(edit_prediction, |this| this.tab_index(0isize))
.on_click(|_, window, cx| {
let app_state = AppState::global(cx);
@@ -582,7 +595,7 @@ impl ConfigurationView {
Button::new("reinstall_and_sign_in", label)
.map(|this| {
if edit_prediction {
if edit_prediction || self.compact {
this.size(ButtonSize::Medium)
} else {
this.full_width()
@@ -656,31 +669,32 @@ impl ConfigurationView {
let start_label = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
let no_status_label = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different LLM provider.";
if let Some(msg) = self.loading_message() {
v_flex()
.gap_2()
.child(Label::new(start_label))
.child(self.render_loading_button(msg, false))
.into_any_element()
let (label, button) = if let Some(msg) = self.loading_message() {
(
start_label,
self.render_loading_button(msg, false).into_any_element(),
)
} else if self.is_error() {
v_flex()
.gap_2()
.child(Label::new(ERROR_LABEL))
.child(self.render_reinstall_button(false))
.into_any_element()
(
ERROR_LABEL,
self.render_reinstall_button(false).into_any_element(),
)
} else if self.has_no_status() {
v_flex()
.gap_2()
.child(Label::new(no_status_label))
.child(self.render_sign_in_button(false))
.into_any_element()
(
no_status_label,
self.render_sign_in_button(false).into_any_element(),
)
} else {
v_flex()
.gap_2()
.child(Label::new(start_label))
.child(self.render_sign_in_button(false))
.into_any_element()
}
(
start_label,
self.render_sign_in_button(false).into_any_element(),
)
};
v_flex()
.gap_2()
.when(!self.compact, |this| this.child(Label::new(label)))
.child(button)
}
}
@@ -689,13 +703,15 @@ impl Render for ConfigurationView {
let is_authenticated = &self.is_authenticated;
if is_authenticated(cx) {
return ConfiguredApiCard::new("Authorized")
let sign_out = |_: &gpui::ClickEvent, window: &mut Window, cx: &mut App| {
if let Some(auth) = GlobalCopilotAuth::try_global(cx) {
initiate_sign_out(auth.0.clone(), window, cx);
}
};
return ConfiguredApiCard::new("copilot-authorized", "Authorized")
.button_label("Sign Out")
.on_click(|_, window, cx| {
if let Some(auth) = GlobalCopilotAuth::try_global(cx) {
initiate_sign_out(auth.0.clone(), window, cx);
}
})
.on_click(sign_out)
.into_any_element();
}
+4 -8
View File
@@ -111,17 +111,13 @@ impl FeatureFlag for AgentThreadWorktreeLabelFlag {
}
register_feature_flag!(AgentThreadWorktreeLabelFlag);
/// Moves LLM provider and MCP server configuration out of the dedicated agent
/// panel page and into the settings UI. When enabled, the agent panel no longer
/// shows its configuration overlay and the settings UI exposes the "LLM
/// Providers" and "MCP Servers" sub-pages instead.
pub struct AgentSettingsUiFeatureFlag;
pub struct AutoWatchFeatureFlag;
impl FeatureFlag for AgentSettingsUiFeatureFlag {
const NAME: &'static str = "agent-settings-ui";
impl FeatureFlag for AutoWatchFeatureFlag {
const NAME: &'static str = "auto-watch-screens";
type Value = PresenceFlag;
}
register_feature_flag!(AgentSettingsUiFeatureFlag);
register_feature_flag!(AutoWatchFeatureFlag);
/// Wraps agent-run terminal commands in an OS-level sandbox where supported
/// (currently macOS Seatbelt only). When off, terminal commands run with the
+38 -4
View File
@@ -371,6 +371,22 @@ pub trait LanguageModelProvider: 'static {
ProviderConfigurationView::SubPage(self.configuration_view(target_agent, window, cx))
}
fn inline_title(&self, _cx: &App) -> Option<SharedString> {
None
}
fn inline_description(&self, _cx: &App) -> Option<InlineDescription> {
None
}
fn api_key_configuration(&self, _cx: &App) -> Option<ApiKeyConfiguration> {
None
}
fn set_api_key(&self, _key: String, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
/// Copy shown the first time a user enables fast mode for a model from
/// this provider. Returning `None` skips the confirmation prompt and lets
/// the toggle apply silently.
@@ -382,13 +398,31 @@ pub trait LanguageModelProvider: 'static {
/// How a provider's configuration UI prefers to be presented by the settings UI.
#[derive(Clone)]
pub enum ProviderConfigurationView {
/// A compact control suitable for rendering inline in a list row, such as a
/// single API-key field.
Inline(AnyView),
/// A richer view that should be shown on its own dedicated sub-page.
Inline { view: AnyView },
SubPage(AnyView),
}
/// A live snapshot of a single-API-key provider's credential state, used by the
/// settings UI to render the provider's "API Key" section.
#[derive(Clone)]
pub struct ApiKeyConfiguration {
pub has_key: bool,
pub is_from_env_var: bool,
pub env_var_name: SharedString,
pub api_key_url: SharedString,
}
/// The subtitle rendered beneath a provider's name when its configuration is
/// shown inline.
#[derive(Clone)]
pub enum InlineDescription {
/// A clickable "Where to find key" link pointing at the given URL, for
/// API-key based providers.
ApiKeyUrl(SharedString),
/// Plain descriptive text, e.g. explaining a sign-in based provider.
Text(SharedString),
}
/// Provider-specific copy shown the first time a user enables fast mode.
#[derive(Debug, Clone)]
pub struct FastModeConfirmation {
@@ -1,155 +0,0 @@
use std::rc::Rc;
use anyhow::Result;
use gpui::{App, Context, Entity, Subscription, Task, Window};
use language_model::ApiKeyState;
use ui::{Tooltip, prelude::*};
use ui_input::InputField;
/// The current credential state of a single-API-key provider, as reported by the
/// provider when constructing an [`ApiKeyEditor`].
pub enum ApiKeyStatus {
/// No key is configured; show the input field.
Unset,
/// A key is configured via the UI; show a "configured" row with a reset.
Configured,
/// The key comes from an environment variable and can't be edited here.
FromEnvVar(SharedString),
}
/// Maps a provider's [`ApiKeyState`] to the [`ApiKeyStatus`] the editor renders.
/// Shared so the API-key providers don't each duplicate this mapping.
pub fn api_key_status(state: &ApiKeyState) -> ApiKeyStatus {
if state.is_from_env_var() {
ApiKeyStatus::FromEnvVar(state.env_var_name().clone())
} else if state.has_key() {
ApiKeyStatus::Configured
} else {
ApiKeyStatus::Unset
}
}
/// A compact, reusable control for editing a provider's single API key, intended
/// to be returned from `LanguageModelProvider::configuration_view_v2` as an
/// inline control.
///
/// It is deliberately provider-agnostic: the provider supplies closures that
/// read the current [`ApiKeyStatus`] and store/clear the key against its own
/// state, so all credential knowledge stays in the provider.
pub struct ApiKeyEditor {
input: Entity<InputField>,
api_key_url: SharedString,
status: Rc<dyn Fn(&App) -> ApiKeyStatus>,
set_key: Rc<dyn Fn(String, &mut App) -> Task<Result<()>>>,
reset_key: Rc<dyn Fn(&mut App) -> Task<Result<()>>>,
_subscription: Subscription,
}
impl ApiKeyEditor {
pub fn new<S: 'static>(
state: Entity<S>,
api_key_url: impl Into<SharedString>,
placeholder: &str,
status: impl Fn(&S, &App) -> ApiKeyStatus + 'static,
set_key: impl Fn(&Entity<S>, String, &mut App) -> Task<Result<()>> + 'static,
reset_key: impl Fn(&Entity<S>, &mut App) -> Task<Result<()>> + 'static,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let input = cx.new(|cx| {
InputField::new(window, cx, placeholder)
.masked(true)
.tab_index(0)
});
let subscription = cx.observe(&state, |_, _, cx| cx.notify());
let status_state = state.clone();
let set_state = state.clone();
Self {
input,
api_key_url: api_key_url.into(),
status: Rc::new(move |cx| status(status_state.read(cx), cx)),
set_key: Rc::new(move |key, cx| set_key(&set_state, key, cx)),
reset_key: Rc::new(move |cx| reset_key(&state, cx)),
_subscription: subscription,
}
}
fn save(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let key = self.input.read(cx).text(cx).trim().to_string();
if key.is_empty() {
return;
}
self.input
.update(cx, |input, cx| input.set_text("", window, cx));
(self.set_key.clone())(key, cx).detach_and_log_err(cx);
}
fn reset(&mut self, cx: &mut Context<Self>) {
(self.reset_key.clone())(cx).detach_and_log_err(cx);
}
fn render_where_to_find_key(&self) -> impl IntoElement {
let url = self.api_key_url.clone();
let click_url = url.to_string();
h_flex()
.id("where-to-find-key")
.gap_0p5()
.cursor_pointer()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new("Where to find key")
.size(LabelSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(format!("Create an API key at {url}")))
.on_click(move |_, _window, cx| cx.open_url(&click_url))
}
}
impl Render for ApiKeyEditor {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
match (self.status)(cx) {
ApiKeyStatus::FromEnvVar(env_var_name) => Label::new(format!("Set via {env_var_name}"))
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
ApiKeyStatus::Configured => h_flex()
.gap_2()
.items_center()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(
Label::new("Configured")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Button::new("reset-api-key", "Reset")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.tab_index(0isize)
.on_click(cx.listener(|this, _, _window, cx| this.reset(cx))),
)
.into_any_element(),
ApiKeyStatus::Unset => v_flex()
.w_full()
.gap_1()
.child(self.render_where_to_find_key())
.child(
div()
.w_full()
.on_action(cx.listener(Self::save))
.child(self.input.clone()),
)
.into_any_element(),
}
}
}
@@ -10,12 +10,10 @@ use language_model::{
};
use provider::deepseek::DeepSeekLanguageModelProvider;
mod api_key_editor;
pub mod extension;
pub mod provider;
mod settings;
pub use crate::api_key_editor::{ApiKeyEditor, ApiKeyStatus, api_key_status};
pub use crate::extension::init_proxy as init_extension_proxy;
use crate::provider::anthropic::AnthropicLanguageModelProvider;
@@ -8,12 +8,12 @@ use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ApiKeyState, AuthenticateError,
ConfigurationViewTargetAgent, EnvVar, FastModeConfirmation, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
ProviderConfigurationView, RateLimiter, env_var,
ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ApiKeyConfiguration, ApiKeyState,
AuthenticateError, ConfigurationViewTargetAgent, EnvVar, FastModeConfirmation, IconOrSvg,
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, RateLimiter,
env_var,
};
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
@@ -290,28 +290,19 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://console.anthropic.com/settings/keys",
"sk-ant-...",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
fn api_key_configuration(&self, cx: &App) -> Option<ApiKeyConfiguration> {
let state = self.state.read(cx);
Some(ApiKeyConfiguration {
has_key: state.api_key_state.has_key(),
is_from_env_var: state.api_key_state.is_from_env_var(),
env_var_name: state.api_key_state.env_var_name().clone(),
api_key_url: "https://console.anthropic.com/settings/keys".into(),
})
}
fn set_api_key(&self, key: String, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(Some(key), cx))
}
fn fast_mode_confirmation(&self, _cx: &App) -> Option<FastModeConfirmation> {
@@ -702,7 +693,7 @@ impl Render for ConfigurationView {
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("anthropic-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
+79 -43
View File
@@ -32,11 +32,12 @@ use gpui::{
use gpui_tokio::Tokio;
use http_client::HttpClient;
use language_model::{
AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
LanguageModelToolUse, MessageContent, RateLimiter, Role, TokenUsage, env_var,
AuthenticateError, EnvVar, IconOrSvg, InlineDescription, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, RateLimiter, Role,
TokenUsage, env_var,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -460,6 +461,12 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
IconOrSvg::Icon(IconName::AiBedrock)
}
fn inline_description(&self, _cx: &App) -> Option<InlineDescription> {
Some(InlineDescription::Text(
"To use Zed's agent with Bedrock, set a custom authentication strategy in your settings or use static credentials.".into(),
))
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_language_model(bedrock::Model::default()))
}
@@ -1492,10 +1499,6 @@ impl ConfigurationView {
.detach_and_log_err(cx);
}
fn should_render_editor(&self, cx: &Context<Self>) -> bool {
self.state.read(cx).is_authenticated()
}
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
window.focus_next(cx);
}
@@ -1577,13 +1580,15 @@ impl Render for ConfigurationView {
None
};
if self.should_render_editor(cx) {
return ConfiguredApiCard::new(configured_label)
let credentials_control = if self.state.read(cx).is_authenticated() {
ConfiguredApiCard::new("bedrock-reset", configured_label)
.disabled(env_var_set || is_settings_derived)
.on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx)))
.when_some(tooltip_label, |this, label| this.tooltip_label(label))
.into_any_element();
}
.into_any_element()
} else {
self.render_static_credentials_ui().into_any_element()
};
v_flex()
.min_w_0()
@@ -1592,15 +1597,29 @@ impl Render for ConfigurationView {
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.on_action(cx.listener(ConfigurationView::save_credentials))
.child(Label::new("To use Zed's agent with Bedrock, you can set a custom authentication strategy through your settings file or use static credentials."))
.child(Label::new("But first, to access models on AWS, you need to:").mt_1())
.gap_1()
.child(Headline::new("Amazon Bedrock").size(HeadlineSize::Small))
.child(
Label::new(
"To use Zed's agent with Bedrock, you can set a custom authentication strategy through your settings file or use static credentials.",
)
.color(Color::Muted),
)
.child(
Label::new("But first, to access models on AWS, you need to:")
.mt_1()
.color(Color::Muted),
)
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new(
"Grant permissions to the strategy you'll use according to the:",
))
.child(
Label::new(
"Grant permissions to the strategy you'll use according to the:",
)
.color(Color::Muted),
)
.child(ButtonLink::new(
"Prerequisites",
"https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html",
@@ -1608,33 +1627,32 @@ impl Render for ConfigurationView {
)
.child(
ListBulletItem::new("")
.child(Label::new("Select the models you would like access to:"))
.child(
Label::new("Select the models you would like access to:")
.color(Color::Muted),
)
.child(ButtonLink::new(
"Bedrock Model Catalog",
"https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/model-catalog",
)),
),
)
.child(self.render_static_credentials_ui())
.child(credentials_control)
.into_any()
}
}
impl ConfigurationView {
fn render_static_credentials_ui(&self) -> impl IntoElement {
let section_header = |title: SharedString| {
h_flex()
.gap_2()
.child(Label::new(title).size(LabelSize::Default))
.child(Divider::horizontal())
};
let list_item = List::new()
.child(
ListBulletItem::new("")
.child(Label::new(
"For access keys: Create an IAM user in the AWS console with programmatic access",
))
.child(
Label::new(
"For access keys: Create an IAM user in the AWS console with programmatic access",
)
.color(Color::Muted),
)
.child(ButtonLink::new(
"IAM Console",
"https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users",
@@ -1642,7 +1660,10 @@ impl ConfigurationView {
)
.child(
ListBulletItem::new("")
.child(Label::new("For Bedrock API Keys: Generate an API key from the"))
.child(
Label::new("For Bedrock API Keys: Generate an API key from the")
.color(Color::Muted),
)
.child(ButtonLink::new(
"Bedrock Console",
"https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-use.html",
@@ -1650,28 +1671,42 @@ impl ConfigurationView {
)
.child(
ListBulletItem::new("")
.child(Label::new("Attach the necessary Bedrock permissions to"))
.child(
Label::new("Attach the necessary Bedrock permissions to")
.color(Color::Muted),
)
.child(ButtonLink::new(
"this user",
"https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html",
)),
)
.child(ListBulletItem::new(
"Enter either access keys OR a Bedrock API Key below (not both)",
));
.child(
ListBulletItem::new(
"Enter either access keys OR a Bedrock API Key below (not both)",
)
.label_color(Color::Muted),
);
v_flex()
.my_2()
.tab_group()
.gap_1p5()
.child(section_header("Static Credentials".into()))
.child(Label::new(
"This method uses your AWS access key ID and secret access key, or a Bedrock API Key.",
))
.child(Divider::horizontal())
.child(Label::new("Static Credentials").mt_2())
.child(
Label::new(
"This method uses your AWS access key ID and secret access key, or a Bedrock API Key.",
)
.color(Color::Muted),
)
.child(list_item)
.child(self.access_key_id_editor.clone())
.child(self.secret_access_key_editor.clone())
.child(self.session_token_editor.clone())
.child(
v_flex()
.gap_1()
.child(self.access_key_id_editor.clone())
.child(self.secret_access_key_editor.clone())
.child(self.session_token_editor.clone()),
)
.child(
Label::new(format!(
"You can also set the {}, {} and {} environment variables (or {} for Bedrock API Key authentication) and restart Zed.",
@@ -1695,7 +1730,8 @@ impl ConfigurationView {
.mt_1()
.mb_2p5(),
)
.child(section_header("Using the an API key".into()))
.child(Divider::horizontal())
.child(Label::new("Using the API key").mt_2().mb_1())
.child(self.bearer_token_editor.clone())
.child(
Label::new(format!(
+137 -66
View File
@@ -11,9 +11,10 @@ use futures::StreamExt;
use futures::future::BoxFuture;
use gpui::{AnyElement, AnyView, App, AppContext, Context, Entity, Subscription, Task, TaskExt};
use language_model::{
AuthenticateError, FastModeConfirmation, IconOrSvg, LanguageModel, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
ProviderConfigurationView, ZED_CLOUD_PROVIDER_ID, ZED_CLOUD_PROVIDER_NAME,
AuthenticateError, FastModeConfirmation, IconOrSvg, InlineDescription, LanguageModel,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, ProviderConfigurationView, ZED_CLOUD_PROVIDER_ID,
ZED_CLOUD_PROVIDER_NAME,
};
use language_models_cloud::{CloudLlmTokenProvider, CloudModelProvider};
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
@@ -365,20 +366,55 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
_: &mut Window,
cx: &mut App,
) -> AnyView {
cx.new(|_| ConfigurationView::new(self.state.clone()))
cx.new(|_| ConfigurationView::new(self.state.clone(), false))
.into()
}
fn configuration_view_v2(
&self,
target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
_target_agent: language_model::ConfigurationViewTargetAgent,
_window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
// The Zed sign-in/plan control is small enough that sending users to a
// dedicated sub-page just to reach it would be annoying, so render it
// inline even though it isn't an API-key field.
ProviderConfigurationView::Inline(self.configuration_view(target_agent, window, cx))
ProviderConfigurationView::Inline {
view: cx
.new(|_| ConfigurationView::new(self.state.clone(), true))
.into(),
}
}
fn inline_description(&self, cx: &App) -> Option<InlineDescription> {
let state = self.state.read(cx);
let user_store = state.user_store.read(cx);
let is_zed_model_provider_enabled = user_store
.current_organization_configuration()
.map_or(true, |config| config.is_zed_model_provider_enabled);
Some(InlineDescription::Text(
zed_ai_description(
!state.is_signed_out(cx),
user_store.plan(),
is_zed_model_provider_enabled,
user_store.trial_started_at().is_none(),
)
.into(),
))
}
fn inline_title(&self, cx: &App) -> Option<SharedString> {
let state = self.state.read(cx);
if state.is_signed_out(cx) {
return None;
}
let plan_name = match state.user_store.read(cx).plan()? {
Plan::ZedPro => "Pro",
Plan::ZedProTrial => "Pro Trial",
Plan::ZedStudent => "Student",
Plan::ZedBusiness => "Business",
Plan::ZedVip => "VIP",
Plan::ZedFree => return None,
};
Some(format!("Subscribed to {plan_name}").into())
}
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
@@ -413,63 +449,86 @@ struct ZedAiConfiguration {
is_zed_model_provider_enabled: bool,
eligible_for_trial: bool,
account_too_young: bool,
compact: bool,
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
}
fn zed_ai_description(
is_connected: bool,
plan: Option<Plan>,
is_zed_model_provider_enabled: bool,
eligible_for_trial: bool,
) -> &'static str {
if !is_connected {
return "Sign in to have access to Zed's complete agentic experience with hosted models.";
}
match plan {
Some(Plan::ZedPro) => {
"You have access to Zed's hosted models through your Pro subscription."
}
Some(Plan::ZedProTrial) => "You have access to Zed's hosted models through your Pro trial.",
Some(Plan::ZedStudent) => {
"You have access to Zed's hosted models through your Student subscription."
}
Some(Plan::ZedBusiness) => {
if is_zed_model_provider_enabled {
"You have access to Zed's hosted models through your organization."
} else {
"Zed's hosted models are disabled by your organization's configuration."
}
}
Some(Plan::ZedVip) => {
"You have access to Zed's hosted models through your VIP subscription."
}
Some(Plan::ZedFree) | None => {
if eligible_for_trial {
"Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
} else {
"Subscribe for access to Zed's hosted models."
}
}
}
}
impl RenderOnce for ZedAiConfiguration {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let (subscription_text, has_paid_plan) = match self.plan {
Some(Plan::ZedPro) => (
"You have access to Zed's hosted models through your Pro subscription.",
true,
),
Some(Plan::ZedProTrial) => (
"You have access to Zed's hosted models through your Pro trial.",
false,
),
Some(Plan::ZedStudent) => (
"You have access to Zed's hosted models through your Student subscription.",
true,
),
Some(Plan::ZedBusiness) => (
if self.is_zed_model_provider_enabled {
"You have access to Zed's hosted models through your organization."
} else {
"Zed's hosted models are disabled by your organization's configuration."
},
true,
),
Some(Plan::ZedVip) => (
"You have access to Zed's hosted models through your VIP subscription.",
true,
),
let has_paid_plan = matches!(
self.plan,
Some(Plan::ZedPro | Plan::ZedStudent | Plan::ZedBusiness | Plan::ZedVip)
);
Some(Plan::ZedFree) | None => (
if self.eligible_for_trial {
"Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
} else {
"Subscribe for access to Zed's hosted models."
},
false,
),
};
let description = zed_ai_description(
self.is_connected,
self.plan,
self.is_zed_model_provider_enabled,
self.eligible_for_trial,
);
let manage_subscription_buttons = if has_paid_plan {
Button::new("manage_settings", "Manage Subscription")
.full_width()
.label_size(LabelSize::Small)
.when(!self.compact, |this| {
this.full_width().label_size(LabelSize::Small)
})
.when(self.compact, |this| this.size(ButtonSize::Medium))
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
.into_any_element()
} else if self.plan.is_none() || self.eligible_for_trial {
Button::new("start_trial", "Start 14-day Free Pro Trial")
.full_width()
.when(!self.compact, |this| {
this.full_width().label_size(LabelSize::Small)
})
.when(self.compact, |this| this.size(ButtonSize::Medium))
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
.into_any_element()
} else {
Button::new("upgrade", "Upgrade to Pro")
.full_width()
.when(!self.compact, |this| {
this.full_width().label_size(LabelSize::Small)
})
.when(self.compact, |this| this.size(ButtonSize::Medium))
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
.into_any_element()
@@ -478,11 +537,15 @@ impl RenderOnce for ZedAiConfiguration {
if !self.is_connected {
return v_flex()
.gap_2()
.child(Label::new("Sign in to have access to Zed's complete agentic experience with hosted models."))
.when(!self.compact, |this| this.child(Label::new(description)))
.child(
Button::new("sign_in", "Sign In to use Zed AI")
.start_icon(Icon::new(IconName::Github).size(IconSize::Small).color(Color::Muted))
.full_width()
.start_icon(
Icon::new(IconName::Github)
.size(IconSize::Small)
.color(Color::Muted),
)
.when(!self.compact, |this| this.full_width())
.on_click({
let callback = self.sign_in_callback.clone();
move |_, window, cx| (callback)(window, cx)
@@ -490,30 +553,35 @@ impl RenderOnce for ZedAiConfiguration {
);
}
v_flex().gap_2().w_full().map(|this| {
if self.account_too_young {
this.child(YoungAccountBanner).child(
Button::new("upgrade", "Upgrade to Pro")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
)
} else {
this.text_sm()
.child(subscription_text)
.child(manage_subscription_buttons)
}
})
v_flex()
.gap_2()
.when(!self.compact, |this| this.w_full())
.map(|this| {
if self.account_too_young {
this.child(YoungAccountBanner).child(
Button::new("upgrade", "Upgrade to Pro")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.when(!self.compact, |this| this.full_width())
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
}),
)
} else {
this.when(!self.compact, |this| this.text_sm().child(description))
.child(manage_subscription_buttons)
}
})
}
}
struct ConfigurationView {
state: Entity<State>,
compact: bool,
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
}
impl ConfigurationView {
fn new(state: Entity<State>) -> Self {
fn new(state: Entity<State>, compact: bool) -> Self {
let sign_in_callback = Arc::new({
let state = state.clone();
move |_window: &mut Window, cx: &mut App| {
@@ -525,6 +593,7 @@ impl ConfigurationView {
Self {
state,
compact,
sign_in_callback,
}
}
@@ -545,6 +614,7 @@ impl Render for ConfigurationView {
is_zed_model_provider_enabled,
eligible_for_trial: user_store.trial_started_at().is_none(),
account_too_young: user_store.account_too_young(),
compact: self.compact,
sign_in_callback: self.sign_in_callback.clone(),
}
}
@@ -904,6 +974,7 @@ impl Component for ZedAiConfiguration {
is_zed_model_provider_enabled: config.is_zed_model_provider_enabled,
eligible_for_trial: config.eligible_for_trial,
account_too_young: false,
compact: false,
sign_in_callback: Arc::new(|_, _| {}),
}
.into_any_element()
@@ -199,13 +199,48 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
fn configuration_view_v2(
&self,
target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
_target_agent: language_model::ConfigurationViewTargetAgent,
_window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
// GitHub Copilot's control is just a sign-in button, so render it inline
// rather than behind a sub-page.
ProviderConfigurationView::Inline(self.configuration_view(target_agent, window, cx))
// GitHub Copilot's control is just a sign-in/out button, so render it
// inline rather than behind a sub-page. The explanatory copy is surfaced
// via `inline_description` (the row's left column), so the view itself
// renders compactly.
ProviderConfigurationView::Inline {
view: cx
.new(|cx| {
copilot_ui::ConfigurationView::new(
|cx| {
CopilotChat::global(cx)
.map(|m| m.read(cx).is_authenticated())
.unwrap_or(false)
},
copilot_ui::ConfigurationMode::Chat,
cx,
)
.compact()
})
.into(),
}
}
fn inline_title(&self, cx: &App) -> Option<SharedString> {
if self.state.read(cx).is_authenticated(cx) {
None
} else {
Some("Configure Copilot".into())
}
}
fn inline_description(&self, cx: &App) -> Option<language_model::InlineDescription> {
if self.state.read(cx).is_authenticated(cx) {
None
} else {
Some(language_model::InlineDescription::Text(
"Requires an active GitHub Copilot subscription.".into(),
))
}
}
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
+20 -29
View File
@@ -8,12 +8,12 @@ use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
ProviderConfigurationView, RateLimiter, Role, StopReason, TokenUsage, env_var,
ApiKeyConfiguration, ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelEffortLevel,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
RateLimiter, Role, StopReason, TokenUsage, env_var,
};
pub use settings::DeepseekAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
@@ -212,28 +212,19 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://platform.deepseek.com/api_keys",
"Paste your DeepSeek API key",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
fn api_key_configuration(&self, cx: &App) -> Option<ApiKeyConfiguration> {
let state = self.state.read(cx);
Some(ApiKeyConfiguration {
has_key: state.api_key_state.has_key(),
is_from_env_var: state.api_key_state.is_from_env_var(),
env_var_name: state.api_key_state.env_var_name().clone(),
api_key_url: "https://platform.deepseek.com/api_keys".into(),
})
}
fn set_api_key(&self, key: String, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(Some(key), cx))
}
}
@@ -780,7 +771,7 @@ impl Render for ConfigurationView {
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("deepseek-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.into_any_element()
+17 -26
View File
@@ -7,9 +7,9 @@ pub use google_ai::completion::{GoogleEventMapper, into_google};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat,
ProviderConfigurationView,
ApiKeyConfiguration, AuthenticateError, ConfigurationViewTargetAgent, EnvVar,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelToolChoice,
LanguageModelToolSchemaFormat,
};
use language_model::{
GOOGLE_PROVIDER_ID, GOOGLE_PROVIDER_NAME, IconOrSvg, LanguageModel, LanguageModelEffortLevel,
@@ -237,28 +237,19 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://aistudio.google.com/app/apikey",
"AIza...",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
fn api_key_configuration(&self, cx: &App) -> Option<ApiKeyConfiguration> {
let state = self.state.read(cx);
Some(ApiKeyConfiguration {
has_key: state.api_key_state.has_key(),
is_from_env_var: state.api_key_state.is_from_env_var(),
env_var_name: state.api_key_state.env_var_name().clone(),
api_key_url: "https://aistudio.google.com/app/apikey".into(),
})
}
fn set_api_key(&self, key: String, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(Some(key), cx))
}
}
@@ -526,7 +517,7 @@ impl Render for ConfigurationView {
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("google-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
@@ -4,15 +4,16 @@ use credentials_provider::CredentialsProvider;
use fs::Fs;
use futures::Stream;
use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task, TaskExt};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt};
use http_client::{CustomHeaders, HttpClient};
use language_model::util::parse_tool_arguments;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, InlineDescription, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, RateLimiter, Role,
StopReason, TokenUsage, env_var,
};
use llama_cpp::{
LLAMA_CPP_API_URL, ModelEntry, Props, get_models, get_props, stream_chat_completion,
@@ -25,8 +26,7 @@ use std::sync::LazyLock;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::time::Duration;
use ui::{
ButtonLike, ButtonLink, ConfiguredApiCard, ElevationIndex, List, ListBulletItem, Tooltip,
prelude::*,
ButtonLike, ButtonLink, ConfiguredApiCard, Divider, List, ListBulletItem, Tooltip, prelude::*,
};
use ui_input::InputField;
use util::ResultExt;
@@ -562,6 +562,12 @@ impl LanguageModelProvider for LlamaCppLanguageModelProvider {
None
}
fn inline_description(&self, _cx: &App) -> Option<InlineDescription> {
Some(InlineDescription::Text(
"Run local models on your machine with LlamaCpp.".into(),
))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let settings = LlamaCppLanguageModelProvider::settings(cx);
let effective = compute_effective_models(&self.state.read(cx).fetched_models, settings);
@@ -1327,31 +1333,42 @@ impl ConfigurationView {
fn render_instructions(cx: &App) -> Div {
v_flex()
.gap_2()
.child(Label::new(
"Run open models locally with llama.cpp's built-in server, or connect to a \
.child(
Label::new(
"Run open models locally with llama.cpp's built-in server, or connect to a \
remote llama.cpp server.",
))
.child(Label::new("To use a local llama.cpp server:"))
)
.color(Color::Muted),
)
.child(Label::new("To use a local llama.cpp server:").color(Color::Muted))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Install llama.cpp from"))
.child(Label::new("Install llama.cpp from").color(Color::Muted))
.child(ButtonLink::new("llama.app", LLAMA_CPP_DOWNLOAD_URL)),
)
.child(
ListBulletItem::new("")
.child(Label::new("Start the server in router mode:"))
.child(
Label::new("Start the server in router mode:").color(Color::Muted),
)
.child(Label::new("llama serve").inline_code(cx)),
)
.child(ListBulletItem::new(
"Click 'Connect' below to start using llama.cpp in Zed",
)),
.child(
ListBulletItem::new(
"Click 'Connect' below to start using llama.cpp in Zed",
)
.label_color(Color::Muted),
),
)
.child(Label::new(
"Alternatively, you can connect to a remote llama.cpp server by specifying its \
.child(
Label::new(
"Alternatively, you can connect to a remote llama.cpp server by specifying its \
URL and API key (set with --api-key, may not be required):",
))
)
.color(Color::Muted),
)
}
fn render_api_key_editor(&self, cx: &Context<Self>) -> impl IntoElement {
@@ -1363,20 +1380,10 @@ impl ConfigurationView {
"API key configured".to_string()
};
if !state.api_key_state.has_key() {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(self.api_key_editor.clone())
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
let api_key_control = if !state.api_key_state.has_key() {
self.api_key_editor.clone().into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("llama-cpp-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
@@ -1385,7 +1392,20 @@ impl ConfigurationView {
))
})
.into_any_element()
}
};
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(api_key_control)
.gap_1p5()
.mb_2()
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
}
fn render_context_window_editor(&self, cx: &Context<Self>) -> Div {
@@ -1394,26 +1414,26 @@ impl ConfigurationView {
if custom_context_window_set {
h_flex()
.p_3()
.p_1()
.justify_between()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().elevated_surface_background)
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().background.opacity(0.5))
.child(
h_flex()
.gap_2()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(v_flex().gap_1().child(Label::new(format!(
.child(Label::new(format!(
"Context Window: {}",
settings.context_window.unwrap_or_default()
)))),
))),
)
.child(
Button::new("reset-context-window", "Reset")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
.layer(ElevationIndex::ModalSurface)
.on_click(
cx.listener(|this, _, window, cx| {
this.reset_context_window(window, cx)
@@ -1428,8 +1448,9 @@ impl ConfigurationView {
}),
)
.child(self.context_window_editor.clone())
.gap_1p5()
.child(
Label::new("Default: discovered from the server")
Label::new("Default: Discovered from the server")
.size(LabelSize::Small)
.color(Color::Muted),
)
@@ -1442,23 +1463,23 @@ impl ConfigurationView {
if custom_api_url_set {
h_flex()
.p_3()
.p_1()
.justify_between()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().elevated_surface_background)
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().background.opacity(0.5))
.child(
h_flex()
.gap_2()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(v_flex().gap_1().child(Label::new(api_url))),
.child(Label::new(api_url)),
)
.child(
Button::new("reset-api-url", "Reset API URL")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
.layer(ElevationIndex::ModalSurface)
.on_click(
cx.listener(|this, _, window, cx| this.reset_api_url(window, cx)),
),
@@ -1469,7 +1490,7 @@ impl ConfigurationView {
this.save_api_url(cx);
cx.notify();
}))
.gap_2()
.gap_1p5()
.child(self.api_url_editor.clone())
}
}
@@ -1481,12 +1502,15 @@ impl Render for ConfigurationView {
v_flex()
.gap_2()
.child(Headline::new("llama.cpp").size(HeadlineSize::Small))
.child(Self::render_instructions(cx))
.child(self.render_api_url_editor(cx))
.child(self.render_context_window_editor(cx))
.child(self.render_api_key_editor(cx))
.child(Divider::horizontal())
.child(
h_flex()
.pt_2()
.w_full()
.justify_between()
.gap_2()
@@ -1498,7 +1522,8 @@ impl Render for ConfigurationView {
if is_authenticated {
this.child(
Button::new("llama-cpp-webui", "Open WebUI")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
@@ -1513,7 +1538,8 @@ impl Render for ConfigurationView {
)
.child(
Button::new("llama-cpp-site", "llama.cpp")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
@@ -1527,7 +1553,8 @@ impl Render for ConfigurationView {
} else {
this.child(
Button::new("download_llama_cpp_button", "Get llama.cpp")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
@@ -1542,7 +1569,8 @@ impl Render for ConfigurationView {
})
.child(
Button::new("view-models", "Browse GGUF Models")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
@@ -1555,17 +1583,16 @@ impl Render for ConfigurationView {
if is_authenticated {
this.child(
ButtonLike::new("connected")
.disabled(true)
.cursor_style(CursorStyle::Arrow)
.size(ButtonSize::Medium)
.child(
h_flex()
.gap_2()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new("Connected"))
.into_any_element(),
.child(Label::new("Connected")),
)
.child(
IconButton::new("refresh-models", IconName::RotateCcw)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Refresh Models"))
.on_click(cx.listener(|this, _, window, cx| {
this.state.update(cx, |state, _| {
@@ -1578,6 +1605,8 @@ impl Render for ConfigurationView {
} else {
this.child(
Button::new("retry_llama_cpp_models", "Connect")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.start_icon(
Icon::new(IconName::PlayOutlined).size(IconSize::XSmall),
)
+53 -57
View File
@@ -3,7 +3,7 @@ use credentials_provider::CredentialsProvider;
use fs::Fs;
use futures::Stream;
use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Subscription, Task, TaskExt};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Subscription, Task, TaskExt};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
@@ -11,8 +11,9 @@ use language_model::{
LanguageModelToolUse, MessageContent, StopReason, TokenUsage, env_var,
};
use language_model::{
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
InlineDescription, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter, Role,
};
use lmstudio::{LMSTUDIO_API_URL, ModelType, get_models};
@@ -24,9 +25,7 @@ use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use ui::{
ButtonLike, ConfiguredApiCard, ElevationIndex, List, ListBulletItem, Tooltip, prelude::*,
};
use ui::{ButtonLike, ConfiguredApiCard, Divider, List, ListBulletItem, Tooltip, prelude::*};
use ui_input::InputField;
use crate::AllLanguageModelSettings;
@@ -253,6 +252,12 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
IconOrSvg::Icon(IconName::AiLmStudio)
}
fn inline_description(&self, _cx: &App) -> Option<InlineDescription> {
Some(InlineDescription::Text(
"Run local LLMs like Llama, Phi, and Qwen with LM Studio.".into(),
))
}
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
// We shouldn't try to select default model, because it might lead to a load call for an unloaded model.
// In a constrained environment where user might not have enough resources it'll be a bad UX to select something
@@ -833,28 +838,8 @@ impl ConfigurationView {
let custom_api_url_set = api_url != LMSTUDIO_API_URL;
if custom_api_url_set {
h_flex()
.p_3()
.justify_between()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().elevated_surface_background)
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(v_flex().gap_1().child(Label::new(api_url))),
)
.child(
Button::new("reset-api-url", "Reset API URL")
.label_size(LabelSize::Small)
.start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
.layer(ElevationIndex::ModalSurface)
.on_click(
cx.listener(|this, _, _window, cx| this.reset_api_url(_window, cx)),
),
)
ConfiguredApiCard::new("reset-api-url", api_url)
.on_click(cx.listener(|this, _, _window, cx| this.reset_api_url(_window, cx)))
.into_any_element()
} else {
v_flex()
@@ -862,7 +847,6 @@ impl ConfigurationView {
this.save_api_url(cx);
cx.notify();
}))
.gap_2()
.child(self.api_url_editor.clone())
.into_any_element()
}
@@ -877,20 +861,10 @@ impl ConfigurationView {
"API key configured".to_string()
};
if !state.api_key_state.has_key() {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(self.api_key_editor.clone())
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
let api_key_control = if !state.api_key_state.has_key() {
self.api_key_editor.clone().into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("lmstudio-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, _window, cx| this.reset_api_key(_window, cx)))
.when(env_var_set, |this| {
@@ -899,7 +873,20 @@ impl ConfigurationView {
))
})
.into_any_element()
}
};
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(api_key_control)
.gap_1p5()
.mb_2()
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
}
}
@@ -912,39 +899,45 @@ impl Render for ConfigurationView {
.child(
v_flex()
.gap_1()
.child(Label::new("Run local LLMs like Llama, Phi, and Qwen."))
.child(Headline::new("LM Studio").size(HeadlineSize::Small))
.child(
Label::new("Run local LLMs like Llama, Phi, and Qwen.").color(Color::Muted),
)
.child(
List::new()
.child(ListBulletItem::new(
"LM Studio needs to be running with at least one model downloaded.",
))
).label_color(Color::Muted))
.child(
ListBulletItem::new("")
.child(Label::new("To get your first model, try running"))
.child(Label::new("lms get qwen2.5-coder-7b").inline_code(cx)),
.child(Label::new("To get your first model, try running").color(Color::Muted))
.child(Label::new("lms get qwen2.5-coder-7b").inline_code(cx).color(Color::Muted).ml_1()),
),
)
.child(Label::new(
"Alternatively, you can connect to an LM Studio server by specifying its \
URL and API key (may not be required):",
)),
).color(Color::Muted)),
)
.child(self.render_api_url_editor(cx))
.child(self.render_api_key_editor(cx))
.child(Divider::horizontal())
.child(
h_flex()
.pt_2()
.w_full()
.justify_between()
.gap_2()
.gap_1()
.child(
h_flex()
.w_full()
.gap_2()
.gap_1()
.map(|this| {
if is_authenticated {
this.child(
Button::new("lmstudio-site", "LM Studio")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::Small)
@@ -961,7 +954,8 @@ impl Render for ConfigurationView {
"download_lmstudio_button",
"Download LM Studio",
)
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::Small)
@@ -976,7 +970,8 @@ impl Render for ConfigurationView {
})
.child(
Button::new("view-models", "Model Catalog")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::Small)
@@ -991,18 +986,17 @@ impl Render for ConfigurationView {
if is_authenticated {
this.child(
ButtonLike::new("connected")
.disabled(true)
.cursor_style(CursorStyle::Arrow)
.size(ButtonSize::Medium)
.child(
h_flex()
.gap_2()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new("Connected"))
.into_any_element(),
)
.child(
IconButton::new("refresh-models", IconName::RotateCcw)
.tooltip(Tooltip::text("Refresh Models"))
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.state.update(cx, |state, _| {
state.available_models.clear();
@@ -1014,6 +1008,8 @@ impl Render for ConfigurationView {
} else {
this.child(
Button::new("retry_lmstudio_models", "Connect")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.start_icon(
Icon::new(IconName::PlayFilled).size(IconSize::XSmall),
)
+20 -29
View File
@@ -6,12 +6,12 @@ use futures::{FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream
use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, TaskExt, Window};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
LanguageModelToolUse, MessageContent, ProviderConfigurationView, RateLimiter, Role, StopReason,
TokenUsage, env_var,
ApiKeyConfiguration, ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, RateLimiter, Role,
StopReason, TokenUsage, env_var,
};
pub use mistral::{MISTRAL_API_URL, StreamResponse};
pub use settings::MistralAvailableModel as AvailableModel;
@@ -237,28 +237,19 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://console.mistral.ai/api-keys",
"Paste your Mistral API key",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
fn api_key_configuration(&self, cx: &App) -> Option<ApiKeyConfiguration> {
let state = self.state.read(cx);
Some(ApiKeyConfiguration {
has_key: state.api_key_state.has_key(),
is_from_env_var: state.api_key_state.is_from_env_var(),
env_var_name: state.api_key_state.env_var_name().clone(),
api_key_url: "https://console.mistral.ai/api-keys".into(),
})
}
fn set_api_key(&self, key: String, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(Some(key), cx))
}
}
@@ -898,7 +889,7 @@ impl Render for ConfigurationView {
.size_full()
.gap_1()
.child(
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("mistral-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
+82 -54
View File
@@ -4,12 +4,12 @@ use credentials_provider::CredentialsProvider;
use fs::Fs;
use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use futures::{Stream, TryFutureExt, stream};
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task, TaskExt};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
ApiKeyState, AuthenticateError, DisabledReason, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
ApiKeyState, AuthenticateError, DisabledReason, EnvVar, IconOrSvg, InlineDescription,
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelRequestTool,
LanguageModelToolChoice, LanguageModelToolUse, LanguageModelToolUseId, MessageContent,
RateLimiter, Role, StopReason, TokenUsage, env_var,
@@ -25,8 +25,7 @@ use std::pin::Pin;
use std::sync::Arc;
use std::sync::LazyLock;
use ui::{
ButtonLike, ButtonLink, ConfiguredApiCard, ElevationIndex, List, ListBulletItem, Tooltip,
prelude::*,
ButtonLike, ButtonLink, ConfiguredApiCard, Divider, List, ListBulletItem, Tooltip, prelude::*,
};
use ui_input::InputField;
@@ -277,6 +276,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
IconOrSvg::Icon(IconName::AiOllama)
}
fn inline_description(&self, _cx: &App) -> Option<InlineDescription> {
Some(InlineDescription::Text(
"Run local models on your machine with Ollama.".into(),
))
}
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
// We shouldn't try to select default model, because it might lead to a load call for an unloaded model.
// In a constrained environment where user might not have enough resources it'll be a bad UX to select something
@@ -832,31 +837,43 @@ impl ConfigurationView {
fn render_instructions(cx: &App) -> Div {
v_flex()
.gap_2()
.child(Label::new(
"Run LLMs locally on your machine with Ollama, or connect to an Ollama server. \
.child(
Label::new(
"Run LLMs locally on your machine with Ollama, or connect to an Ollama server. \
Can provide access to Llama, Mistral, Gemma, and hundreds of other models.",
))
.child(Label::new("To use local Ollama:"))
)
.color(Color::Muted),
)
.child(Label::new("To use local Ollama:").color(Color::Muted))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Download and install Ollama from"))
.child(
Label::new("Download and install Ollama from").color(Color::Muted),
)
.child(ButtonLink::new("ollama.com", "https://ollama.com/download")),
)
.child(
ListBulletItem::new("")
.child(Label::new("Start Ollama and download a model:"))
.child(
Label::new("Start Ollama and download a model:")
.color(Color::Muted),
)
.child(Label::new("ollama run gpt-oss:20b").inline_code(cx)),
)
.child(ListBulletItem::new(
"Click 'Connect' below to start using Ollama in Zed",
)),
.child(
ListBulletItem::new("Click 'Connect' below to start using Ollama in Zed")
.label_color(Color::Muted),
),
)
.child(Label::new(
"Alternatively, you can connect to an Ollama server by specifying its \
.child(
Label::new(
"Alternatively, you can connect to an Ollama server by specifying its \
URL and API key (may not be required):",
))
)
.color(Color::Muted),
)
}
fn render_api_key_editor(&self, cx: &Context<Self>) -> impl IntoElement {
@@ -868,27 +885,30 @@ impl ConfigurationView {
"API key configured".to_string()
};
if !state.api_key_state.has_key() {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(self.api_key_editor.clone())
.child(
Label::new(
format!("You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed.")
)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
let api_key_control = if !state.api_key_state.has_key() {
self.api_key_editor.clone().into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("ollama-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
})
.into_any_element()
}
};
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(api_key_control)
.gap_1p5()
.mb_2()
.child(
Label::new(
format!("You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed.")
)
.size(LabelSize::Small)
.color(Color::Muted),
)
}
fn render_context_window_editor(&self, cx: &Context<Self>) -> Div {
@@ -897,26 +917,26 @@ impl ConfigurationView {
if custom_context_window_set {
h_flex()
.p_3()
.p_1()
.justify_between()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().elevated_surface_background)
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().background.opacity(0.5))
.child(
h_flex()
.gap_2()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(v_flex().gap_1().child(Label::new(format!(
.child(Label::new(format!(
"Context Window: {}",
settings.context_window.unwrap()
)))),
))),
)
.child(
Button::new("reset-context-window", "Reset")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
.layer(ElevationIndex::ModalSurface)
.on_click(
cx.listener(|this, _, window, cx| {
this.reset_context_window(window, cx)
@@ -931,6 +951,7 @@ impl ConfigurationView {
}),
)
.child(self.context_window_editor.clone())
.gap_1p5()
.child(
Label::new("Default: Model specific")
.size(LabelSize::Small)
@@ -945,23 +966,23 @@ impl ConfigurationView {
if custom_api_url_set {
h_flex()
.p_3()
.p_1()
.justify_between()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().elevated_surface_background)
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().background.opacity(0.5))
.child(
h_flex()
.gap_2()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(v_flex().gap_1().child(Label::new(api_url))),
.child(Label::new(api_url)),
)
.child(
Button::new("reset-api-url", "Reset API URL")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.start_icon(Icon::new(IconName::Undo).size(IconSize::Small))
.layer(ElevationIndex::ModalSurface)
.on_click(
cx.listener(|this, _, window, cx| this.reset_api_url(window, cx)),
),
@@ -984,15 +1005,18 @@ impl Render for ConfigurationView {
v_flex()
.gap_2()
.child(Headline::new("Ollama").size(HeadlineSize::Small))
.child(Self::render_instructions(cx))
.child(self.render_api_url_editor(cx))
.child(self.render_context_window_editor(cx))
.child(self.render_api_key_editor(cx))
.child(Divider::horizontal())
.child(
h_flex()
.pt_2()
.w_full()
.justify_between()
.gap_2()
.gap_1()
.child(
h_flex()
.w_full()
@@ -1001,7 +1025,8 @@ impl Render for ConfigurationView {
if is_authenticated {
this.child(
Button::new("ollama-site", "Ollama")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
@@ -1013,7 +1038,8 @@ impl Render for ConfigurationView {
} else {
this.child(
Button::new("download_ollama_button", "Download Ollama")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
@@ -1028,7 +1054,8 @@ impl Render for ConfigurationView {
})
.child(
Button::new("view-models", "View All Models")
.style(ButtonStyle::Subtle)
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
@@ -1041,17 +1068,16 @@ impl Render for ConfigurationView {
if is_authenticated {
this.child(
ButtonLike::new("connected")
.disabled(true)
.cursor_style(CursorStyle::Arrow)
.size(ButtonSize::Medium)
.child(
h_flex()
.gap_2()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new("Connected"))
.into_any_element(),
.child(Label::new("Connected")),
)
.child(
IconButton::new("refresh-models", IconName::RotateCcw)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Refresh Models"))
.on_click(cx.listener(|this, _, window, cx| {
this.state.update(cx, |state, _| {
@@ -1064,6 +1090,8 @@ impl Render for ConfigurationView {
} else {
this.child(
Button::new("retry_ollama_models", "Connect")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.start_icon(
Icon::new(IconName::PlayOutlined).size(IconSize::XSmall),
)
+19 -28
View File
@@ -5,11 +5,11 @@ use futures::{FutureExt, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, FastModeConfirmation, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelEffortLevel,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelToolChoice, OPEN_AI_PROVIDER_ID, OPEN_AI_PROVIDER_NAME, ProviderConfigurationView,
ApiKeyConfiguration, ApiKeyState, AuthenticateError, EnvVar, FastModeConfirmation, IconOrSvg,
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelEffortLevel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, OPEN_AI_PROVIDER_ID, OPEN_AI_PROVIDER_NAME,
RateLimiter, env_var,
};
use menu;
@@ -219,28 +219,19 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://platform.openai.com/api-keys",
"sk-...",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
fn api_key_configuration(&self, cx: &App) -> Option<ApiKeyConfiguration> {
let state = self.state.read(cx);
Some(ApiKeyConfiguration {
has_key: state.api_key_state.has_key(),
is_from_env_var: state.api_key_state.is_from_env_var(),
env_var_name: state.api_key_state.env_var_name().clone(),
api_key_url: "https://platform.openai.com/api-keys".into(),
})
}
fn set_api_key(&self, key: String, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(Some(key), cx))
}
fn fast_mode_confirmation(&self, _cx: &App) -> Option<FastModeConfirmation> {
@@ -728,7 +719,7 @@ impl Render for ConfigurationView {
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("openai-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
@@ -5,12 +5,12 @@ use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, ProviderConfigurationView,
RateLimiter, Role, StopReason, TokenUsage, env_var,
ApiKeyConfiguration, ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
};
use open_router::{
Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, ResponseStreamEvent, list_models,
@@ -274,28 +274,19 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://openrouter.ai/keys",
"sk-or-...",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
fn api_key_configuration(&self, cx: &App) -> Option<ApiKeyConfiguration> {
let state = self.state.read(cx);
Some(ApiKeyConfiguration {
has_key: state.api_key_state.has_key(),
is_from_env_var: state.api_key_state.is_from_env_var(),
env_var_name: state.api_key_state.env_var_name().clone(),
api_key_url: "https://openrouter.ai/keys".into(),
})
}
fn set_api_key(&self, key: String, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(Some(key), cx))
}
}
@@ -1019,7 +1010,7 @@ impl Render for ConfigurationView {
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("openrouter-reset-key", configured_card_label)
.disabled(env_var_set)
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
.when(env_var_set, |this| {
@@ -9,11 +9,11 @@ use http_client::{
http::{HeaderName, HeaderValue},
};
use language_model::{
AuthenticateError, FastModeConfirmation, IconOrSvg, LanguageModel,
AuthenticateError, FastModeConfirmation, IconOrSvg, InlineDescription, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelEffortLevel,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelToolChoice, RateLimiter,
LanguageModelToolChoice, ProviderConfigurationView, RateLimiter,
};
use open_ai::{ReasoningEffort, responses::stream_response};
use rand::RngCore as _;
@@ -31,6 +31,9 @@ const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("opena
const PROVIDER_NAME: LanguageModelProviderName =
LanguageModelProviderName::new("ChatGPT Subscription");
const SUBSCRIPTION_DESCRIPTION: &str =
"Sign in with your ChatGPT Plus or Pro subscription to use OpenAI models in Zed's agent.";
const CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api/codex";
const OPENAI_TOKEN_URL: &str = "https://auth.openai.com/oauth/token";
const OPENAI_AUTHORIZE_URL: &str = "https://auth.openai.com/oauth/authorize";
@@ -248,8 +251,48 @@ impl LanguageModelProvider for OpenAiSubscribedProvider {
) -> AnyView {
let state = self.state.clone();
let http_client = self.http_client.clone();
cx.new(|_cx| ConfigurationView { state, http_client })
.into()
cx.new(|_cx| ConfigurationView {
state,
http_client,
compact: false,
})
.into()
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
_window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
let http_client = self.http_client.clone();
ProviderConfigurationView::Inline {
view: cx
.new(|_cx| ConfigurationView {
state,
http_client,
compact: true,
})
.into(),
}
}
fn inline_title(&self, cx: &App) -> Option<SharedString> {
if self.state.read(cx).is_authenticated() {
None
} else {
Some("Configure ChatGPT".into())
}
}
fn inline_description(&self, cx: &App) -> Option<InlineDescription> {
if self.state.read(cx).is_authenticated() {
None
} else {
Some(InlineDescription::Text(SUBSCRIPTION_DESCRIPTION.into()))
}
}
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
@@ -1043,6 +1086,9 @@ fn do_sign_out(state: &gpui::WeakEntity<State>, cx: &mut App) -> Task<Result<()>
struct ConfigurationView {
state: Entity<State>,
http_client: Arc<dyn HttpClient>,
/// When `true`, the description is rendered elsewhere (the settings row's
/// left column), so it's omitted here to avoid duplication.
compact: bool,
}
impl Render for ConfigurationView {
@@ -1059,7 +1105,7 @@ impl Render for ConfigurationView {
return v_flex()
.child(
ConfiguredApiCard::new(SharedString::from(label))
ConfiguredApiCard::new("openai-subscribed-sign-out", SharedString::from(label))
.button_label("Sign Out")
.on_click(cx.listener(move |_this, _, _window, cx| {
do_sign_out(&weak_state, cx).detach_and_log_err(cx);
@@ -1076,27 +1122,21 @@ impl Render for ConfigurationView {
let button_label = if is_signing_in {
"Signing in…"
} else {
"Sign in to use ChatGPT Subscription"
"Sign In"
};
v_flex()
.gap_2()
.child(Label::new(
"Sign in with your ChatGPT Plus or Pro subscription to use OpenAI models in Zed's agent.",
))
.when(!self.compact, |this| {
this.child(Label::new(SUBSCRIPTION_DESCRIPTION))
})
.child(
Button::new("sign-in", button_label)
.full_width()
.when(!self.compact, |this| this.full_width())
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.loading(is_signing_in)
.disabled(is_signing_in)
.when(!is_signing_in, |this| {
this.start_icon(
Icon::new(IconName::AiOpenAi)
.size(IconSize::Small)
.color(Color::Muted),
)
})
.on_click(move |_, _window, cx| {
do_sign_in(&provider_state, &http_client, cx);
}),
+63 -44
View File
@@ -6,11 +6,11 @@ use futures::{FutureExt, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, TaskExt, Window};
use http_client::{AsyncBody, CustomHeaders, HttpClient, http};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, RateLimiter,
ReasoningEffort, env_var,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, InlineDescription, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelEffortLevel,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelToolChoice, RateLimiter, ReasoningEffort, env_var,
};
use opencode::{ApiProtocol, OPENCODE_API_URL, OpenCodeSubscription};
pub use settings::OpenCodeAvailableModel as AvailableModel;
@@ -18,7 +18,7 @@ use settings::{Settings, SettingsStore, update_settings_file};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
use ui::{
Banner, ButtonLink, ConfiguredApiCard, List, ListBulletItem, Severity, Switch,
Banner, ButtonLink, ConfiguredApiCard, Divider, List, ListBulletItem, Severity, Switch,
SwitchLabelPosition, ToggleState, prelude::*,
};
use ui_input::InputField;
@@ -200,6 +200,12 @@ impl LanguageModelProvider for OpenCodeLanguageModelProvider {
IconOrSvg::Icon(IconName::AiOpenCode)
}
fn inline_description(&self, _cx: &App) -> Option<InlineDescription> {
Some(InlineDescription::Text(
"To use OpenCode models in Zed, you need an API key.".into(),
))
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
if Self::subscription_enabled(OpenCodeSubscription::Go, cx) {
// If both Go and Zen are enabled, prefer Go since it's not pay-as-you-go
@@ -859,37 +865,12 @@ impl Render for ConfigurationView {
}
};
let api_key_section = if self.should_render_editor(cx) {
v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new(
"To use OpenCode models in Zed, you need an API key:",
))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Sign in and get your key at"))
.child(ButtonLink::new(
"OpenCode Console",
"https://opencode.ai/auth",
)),
)
.child(ListBulletItem::new(
"Paste your API key below and hit enter to start using OpenCode",
)),
)
.child(self.api_key_editor.clone())
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
let is_editing = self.should_render_editor(cx);
let api_key_control = if is_editing {
self.api_key_editor.clone().into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("opencode-reset-key", configured_card_label)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip_label(format!(
@@ -900,8 +881,39 @@ impl Render for ConfigurationView {
.into_any_element()
};
let api_key_section = v_flex()
.on_action(cx.listener(Self::save_api_key))
.child(Label::new(
"To use OpenCode models in Zed, you need an API key:",
).color(Color::Muted))
.child(
List::new()
.child(
ListBulletItem::new("")
.child(Label::new("Sign in and get your key at").color(Color::Muted))
.child(ButtonLink::new(
"OpenCode Console",
"https://opencode.ai/auth",
)),
)
.when(is_editing, |this| {
this.child(ListBulletItem::new(
"Paste your API key below and hit enter to start using OpenCode",
).label_color(Color::Muted))
}),
)
.child(api_key_control)
.child(
Label::new(format!(
"You can also set the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."
))
.size(LabelSize::Small)
.color(Color::Muted).mt_1p5(),
)
.into_any_element();
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any()
Label::new("Loading Credentials…").into_any_element()
} else {
let settings = OpenCodeLanguageModelProvider::settings(cx);
let show_zen = settings.show_zen_models;
@@ -909,12 +921,13 @@ impl Render for ConfigurationView {
let show_free = settings.show_free_models;
let subscription_toggles = v_flex()
.gap_1()
.child(Label::new("Subscriptions:").color(Color::Muted))
.gap_2()
.child(Label::new("Subscriptions"))
.child(
Switch::new("opencode-show-zen-models", show_zen.into())
.label("Show Zen models")
.label_position(SwitchLabelPosition::End)
.full_width(true)
.label("Show Zen Models")
.label_position(SwitchLabelPosition::Start)
.on_click(cx.listener(|this, state, window, cx| {
this.set_subscription_enabled(
OpenCodeSubscription::Zen,
@@ -924,10 +937,12 @@ impl Render for ConfigurationView {
);
})),
)
.child(Divider::horizontal_dashed())
.child(
Switch::new("opencode-show-go-models", show_go.into())
.full_width(true)
.label("Show Go models")
.label_position(SwitchLabelPosition::End)
.label_position(SwitchLabelPosition::Start)
.on_click(cx.listener(|this, state, window, cx| {
this.set_subscription_enabled(
OpenCodeSubscription::Go,
@@ -937,10 +952,12 @@ impl Render for ConfigurationView {
);
})),
)
.child(Divider::horizontal_dashed())
.child(
Switch::new("opencode-show-free-models", show_free.into())
.full_width(true)
.label("Show Free models")
.label_position(SwitchLabelPosition::End)
.label_position(SwitchLabelPosition::Start)
.on_click(cx.listener(|this, state, window, cx| {
this.set_subscription_enabled(
OpenCodeSubscription::Free,
@@ -961,8 +978,10 @@ impl Render for ConfigurationView {
v_flex()
.size_full()
.gap_2()
.gap_2p5()
.child(Headline::new("OpenCode").size(HeadlineSize::Small))
.child(api_key_section)
.child(Divider::horizontal())
.child(subscription_toggles)
.children(no_subscriptions_warning)
.into_any()
@@ -7,11 +7,11 @@ use http_client::{
AsyncBody, CustomHeaders, HttpClient, Method, Request as HttpRequest, RequestBuilderExt, http,
};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat,
ProviderConfigurationView, RateLimiter, env_var,
ApiKeyConfiguration, ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolSchemaFormat, RateLimiter, env_var,
};
use open_ai::ResponseStreamEvent;
use serde::Deserialize;
@@ -261,28 +261,21 @@ impl LanguageModelProvider for VercelAiGatewayLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys&title=Go+to+AI+Gateway",
"Paste your Vercel AI Gateway API key",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
fn api_key_configuration(&self, cx: &App) -> Option<ApiKeyConfiguration> {
let state = self.state.read(cx);
Some(ApiKeyConfiguration {
has_key: state.api_key_state.has_key(),
is_from_env_var: state.api_key_state.is_from_env_var(),
env_var_name: state.api_key_state.env_var_name().clone(),
api_key_url:
"https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys&title=Go+to+AI+Gateway"
.into(),
})
}
fn set_api_key(&self, key: String, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(Some(key), cx))
}
}
@@ -743,7 +736,7 @@ impl Render for ConfigurationView {
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("vercel-reset-key", configured_card_label)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
+19 -28
View File
@@ -5,11 +5,11 @@ use futures::{FutureExt, StreamExt, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt, Window};
use http_client::{CustomHeaders, HttpClient};
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolSchemaFormat, ProviderConfigurationView, RateLimiter, env_var,
ApiKeyConfiguration, ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelEffortLevel,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, env_var,
};
use open_ai::ResponseStreamEvent;
pub use settings::XaiAvailableModel as AvailableModel;
@@ -208,28 +208,19 @@ impl LanguageModelProvider for XAiLanguageModelProvider {
.update(cx, |state, cx| state.set_api_key(None, cx))
}
fn configuration_view_v2(
&self,
_target_agent: language_model::ConfigurationViewTargetAgent,
window: &mut Window,
cx: &mut App,
) -> ProviderConfigurationView {
let state = self.state.clone();
ProviderConfigurationView::Inline(
cx.new(|cx| {
crate::ApiKeyEditor::new(
state,
"https://console.x.ai/team/default/api-keys",
"xai-...",
|state, _cx| crate::api_key_status(&state.api_key_state),
|state, key, cx| state.update(cx, |state, cx| state.set_api_key(Some(key), cx)),
|state, cx| state.update(cx, |state, cx| state.set_api_key(None, cx)),
window,
cx,
)
})
.into(),
)
fn api_key_configuration(&self, cx: &App) -> Option<ApiKeyConfiguration> {
let state = self.state.read(cx);
Some(ApiKeyConfiguration {
has_key: state.api_key_state.has_key(),
is_from_env_var: state.api_key_state.is_from_env_var(),
env_var_name: state.api_key_state.env_var_name().clone(),
api_key_url: "https://console.x.ai/team/default/api-keys".into(),
})
}
fn set_api_key(&self, key: String, cx: &mut App) -> Task<Result<()>> {
self.state
.update(cx, |state, cx| state.set_api_key(Some(key), cx))
}
}
@@ -632,7 +623,7 @@ impl Render for ConfigurationView {
)
.into_any_element()
} else {
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new("xai-reset-key", configured_card_label)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip_label(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable."))
+57 -105
View File
@@ -7943,7 +7943,7 @@ fn collaboration_page() -> SettingsPage {
}
fn ai_page(cx: &App) -> SettingsPage {
fn general_section() -> [SettingsPageItem; 3] {
fn general_section() -> [SettingsPageItem; 6] {
[
SettingsPageItem::SectionHeader("General"),
SettingsPageItem::SettingItem(SettingItem {
@@ -7974,24 +7974,11 @@ fn ai_page(cx: &App) -> SettingsPage {
metadata: None,
files: USER,
}),
]
}
fn agent_configuration_section(cx: &App) -> Box<[SettingsPageItem]> {
use feature_flags::FeatureFlagAppExt as _;
// The LLM provider and MCP server pages are gated behind a feature flag
// while their configuration is being moved out of the agent panel.
let agent_settings_ui_enabled = cx.has_flag::<feature_flags::AgentSettingsUiFeatureFlag>();
let mut items = vec![SettingsPageItem::SectionHeader("Agent Configuration")];
if agent_settings_ui_enabled {
items.push(SettingsPageItem::SubPageLink(SubPageLink {
SettingsPageItem::SubPageLink(SubPageLink {
title: "LLM Providers".into(),
r#type: Default::default(),
json_path: Some("llm_providers"),
description: Some("Configure API keys and settings for LLM providers.".into()),
description: Some("Configure natively-included model providers.".into()),
search_aliases: &[
"ai",
"amazon",
@@ -8021,8 +8008,52 @@ fn ai_page(cx: &App) -> SettingsPage {
in_json: false,
files: USER,
render: render_llm_providers_page,
}));
}
}),
SettingsPageItem::SubPageLink(SubPageLink {
title: "External Agents".into(),
r#type: Default::default(),
json_path: Some("agent_servers"),
description: Some(
"View, add, and remove agents connected through the Agent Client Protocol."
.into(),
),
search_aliases: &[
"acp",
"agent client protocol",
"amp",
"claude agent",
"claude code",
"codex",
"copilot cli",
"cursor",
"external agent",
"factory droid",
"github copilot",
"grok build",
"junie",
"opencode",
],
in_json: false,
files: USER,
render: render_external_agents_page,
}),
SettingsPageItem::SubPageLink(SubPageLink {
title: "MCP Servers".into(),
r#type: Default::default(),
json_path: Some("context_servers"),
description: Some(
"View, add, configure, and remove Model Context Protocol servers.".into(),
),
search_aliases: &["context server", "mcp", "model context protocol"],
in_json: false,
files: USER,
render: render_mcp_servers_page,
}),
]
}
fn agent_configuration_section(_cx: &App) -> Box<[SettingsPageItem]> {
let mut items = vec![SettingsPageItem::SectionHeader("Agent Configuration")];
items.extend([
SettingsPageItem::SubPageLink(SubPageLink {
@@ -8068,49 +8099,6 @@ fn ai_page(cx: &App) -> SettingsPage {
}),
]);
if agent_settings_ui_enabled {
items.push(SettingsPageItem::SubPageLink(SubPageLink {
title: "MCP Servers".into(),
r#type: Default::default(),
json_path: Some("context_servers"),
description: Some(
"View, add, configure, and remove Model Context Protocol servers.".into(),
),
search_aliases: &["context server", "mcp", "model context protocol"],
in_json: false,
files: USER,
render: render_mcp_servers_page,
}));
items.push(SettingsPageItem::SubPageLink(SubPageLink {
title: "External Agents".into(),
r#type: Default::default(),
json_path: Some("agent_servers"),
description: Some(
"View, add, and remove agents connected through the Agent Client Protocol."
.into(),
),
search_aliases: &[
"acp",
"agent client protocol",
"amp",
"claude agent",
"claude code",
"codex",
"copilot cli",
"cursor",
"external agent",
"factory droid",
"github copilot",
"grok build",
"junie",
"opencode",
],
in_json: false,
files: USER,
render: render_external_agents_page,
}));
}
items.extend([
SettingsPageItem::SettingItem(SettingItem {
title: "Single File Review",
@@ -8467,28 +8455,6 @@ fn ai_page(cx: &App) -> SettingsPage {
items.into_boxed_slice()
}
fn context_servers_section() -> [SettingsPageItem; 2] {
[
SettingsPageItem::SectionHeader("Context Servers"),
SettingsPageItem::SettingItem(SettingItem {
title: "Context Server Timeout",
description: "Default timeout in seconds for context server tool calls. Can be overridden per-server in context_servers configuration.",
field: Box::new(SettingField {
organization_override: None,
json_path: Some("context_server_timeout"),
pick: |settings_content| {
settings_content.project.context_server_timeout.as_ref()
},
write: |settings_content, value, _| {
settings_content.project.context_server_timeout = value;
},
}),
metadata: None,
files: USER | PROJECT,
}),
]
}
fn edit_prediction_display_sub_section() -> [SettingsPageItem; 1] {
[SettingsPageItem::SettingItem(SettingItem {
title: "Display Mode",
@@ -8519,30 +8485,16 @@ fn ai_page(cx: &App) -> SettingsPage {
})]
}
use feature_flags::FeatureFlagAppExt as _;
// When the agent settings UI is enabled, the context server timeout is shown
// inside the MCP Servers sub-page. Otherwise it remains a standalone section
// here so it stays reachable.
let agent_settings_ui_enabled = cx.has_flag::<feature_flags::AgentSettingsUiFeatureFlag>();
let mut items = concat_sections!(
@vec,
general_section(),
agent_configuration_section(cx),
);
if !agent_settings_ui_enabled {
items.extend(context_servers_section());
}
items.extend(concat_sections!(
@vec,
edit_prediction_language_settings_section(),
edit_prediction_display_sub_section(),
));
SettingsPage {
title: "AI",
items: items.into(),
items: concat_sections!(
@vec,
general_section(),
agent_configuration_section(cx),
edit_prediction_language_settings_section(),
edit_prediction_display_sub_section(),
)
.into(),
}
}
+6 -2
View File
@@ -15,10 +15,14 @@ pub(crate) use audio_input_output_setup::{
};
pub(crate) use audio_test_window::open_audio_test_window;
pub(crate) use edit_prediction_provider_setup::render_edit_prediction_setup_page;
pub(crate) use external_agents_page::{CustomAgentForm, render_external_agents_page};
pub(crate) use external_agents_page::{
CustomAgentForm, render_add_agent_popover, render_external_agents_page,
};
pub(crate) use feature_flags::render_feature_flags_page;
pub(crate) use llm_providers_page::render_llm_providers_page;
pub(crate) use mcp_servers_page::{McpServerForm, render_mcp_servers_page};
pub(crate) use mcp_servers_page::{
McpServerForm, render_add_server_popover, render_mcp_servers_page,
};
pub(crate) use sandbox_settings::render_sandbox_settings_page;
pub use skill_creator::SkillCreatorOpenMode;
pub(crate) use skill_creator::{
@@ -230,10 +230,11 @@ fn render_api_key_provider(
};
let base_container = v_flex().id(title).min_w_0().pt_8().gap_1p5();
let header = SettingsSectionHeader::new(title)
.icon(icon)
.no_padding(true);
let button_link_label = format!("{} dashboard", title);
let description = match docs {
ApiKeyDocs::Custom { message } => div().min_w_0().w_full().child(
Label::new(message)
@@ -251,7 +252,7 @@ fn render_api_key_provider(
.color(Color::Muted),
)
.child(
ButtonLink::new(button_link_label, dashboard_url)
ButtonLink::new(format!("{title} dashboard"), dashboard_url)
.no_icon(true)
.label_size(LabelSize::Small)
.label_color(Color::Muted),
@@ -262,6 +263,7 @@ fn render_api_key_provider(
.color(Color::Muted),
),
};
let configured_card_label = if is_from_env_var {
"API Key Set in Environment Variable"
} else {
@@ -270,7 +272,7 @@ fn render_api_key_provider(
let container = if has_key {
base_container.child(header).child(
ConfiguredApiCard::new(configured_card_label)
ConfiguredApiCard::new(format!("{title}-reset-key"), configured_card_label)
.button_label("Reset Key")
.button_tab_index(0)
.disabled(is_from_env_var)
@@ -298,6 +300,7 @@ fn render_api_key_provider(
.w_full()
.min_w_0()
.max_w_1_2()
.gap_0p5()
.child(Label::new("API Key"))
.child(description)
.when_some(env_var_name, |this, env_var_name| {
@@ -14,7 +14,7 @@ use settings::{
};
use ui::{
AiSettingItem, AiSettingItemSource, AiSettingItemStatus, ContextMenu, ContextMenuEntry,
Divider, DividerColor, PopoverMenu, Tooltip, prelude::*,
Divider, PopoverMenu, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{MultiWorkspace, Workspace, create_and_open_local_file};
@@ -25,7 +25,7 @@ use crate::SettingsWindow;
pub(crate) fn render_external_agents_page(
settings_window: &SettingsWindow,
scroll_handle: &ScrollHandle,
window: &mut Window,
_window: &mut Window,
cx: &mut Context<SettingsWindow>,
) -> AnyElement {
let agent_server_store = get_agent_server_store(settings_window, cx);
@@ -41,8 +41,6 @@ pub(crate) fn render_external_agents_page(
render_no_project_state(cx)
};
let add_agent_popover = render_add_agent_popover(settings_window, window, cx);
v_flex()
.id("external-agents-page")
.size_full()
@@ -51,22 +49,11 @@ pub(crate) fn render_external_agents_page(
.pb_16()
.track_scroll(scroll_handle)
.overflow_y_scroll()
.child(Label::new("External Agents"))
.child(
h_flex()
.w_full()
.justify_between()
.items_center()
.mb_4()
.child(
v_flex()
.child(Label::new("External Agents").size(LabelSize::Large))
.child(
Label::new("All agents connected through the Agent Client Protocol.")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(add_agent_popover),
Label::new("Agents connected through the Agent Client Protocol.")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(agent_list)
.into_any_element()
@@ -165,11 +152,7 @@ fn render_agent_list(agents: Vec<AgentRow>, cx: &mut Context<SettingsWindow>) ->
agents.into_iter().map(|(id, icon, display_name, source)| {
render_agent(id, icon, display_name, source, cx).into_any_element()
}),
|| {
Divider::horizontal()
.color(DividerColor::BorderFaded)
.into_any_element()
},
|| Divider::horizontal().into_any_element(),
))
.into_any_element()
}
@@ -198,22 +181,20 @@ fn render_agent(
// Only custom agents are editable here; registry agents are managed via the
// ACP registry and only support removal.
let configure_button = (source == ExternalAgentSource::Custom).then(|| {
IconButton::new(
SharedString::from(format!("configure-{}", id_string)),
IconName::Settings,
)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tab_index(0isize)
.tooltip(Tooltip::text("Configure Agent"))
.on_click(cx.listener({
let id = id.clone();
move |this, _event, window, cx| {
let existing =
custom_agent_settings(&id, cx).map(|settings| (id.clone(), settings));
open_custom_agent_form(this, existing, window, cx);
}
}))
IconButton::new(format!("configure-{}", id_string), IconName::Settings)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.size(ButtonSize::Medium)
.tab_index(0isize)
.tooltip(Tooltip::text("Configure Agent"))
.on_click(cx.listener({
let id = id.clone();
move |this, _event, window, cx| {
let existing =
custom_agent_settings(&id, cx).map(|settings| (id.clone(), settings));
open_custom_agent_form(this, existing, window, cx);
}
}))
});
let remove_tooltip = match source {
@@ -221,17 +202,15 @@ fn render_agent(
ExternalAgentSource::Custom => "Remove Custom Agent",
};
let remove_button = IconButton::new(
SharedString::from(format!("uninstall-{}", id_string)),
IconName::Trash,
)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tab_index(0isize)
.tooltip(Tooltip::text(remove_tooltip))
.on_click(move |_event, _window, cx| {
remove_agent(&id, source, cx);
});
let remove_button = IconButton::new(format!("uninstall-{}", id_string), IconName::Trash)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.size(ButtonSize::Medium)
.tab_index(0isize)
.tooltip(Tooltip::text(remove_tooltip))
.on_click(move |_event, _window, cx| {
remove_agent(&id, source, cx);
});
// The connection status of an external agent is tracked per agent-panel
// session (via the agent panel's `AgentConnectionStore`), which isn't
@@ -273,7 +252,7 @@ fn remove_agent(id: &AgentId, source: ExternalAgentSource, cx: &mut App) {
});
}
fn render_add_agent_popover(
pub(crate) fn render_add_agent_popover(
settings_window: &SettingsWindow,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
@@ -1,13 +1,15 @@
use std::sync::Arc;
use gpui::{ScrollHandle, prelude::*};
use gpui::{AnyView, ScrollHandle, prelude::*};
use language_model::{
ConfigurationViewTargetAgent, IconOrSvg, LanguageModelProvider, LanguageModelProviderId,
LanguageModelRegistry, ProviderConfigurationView,
ApiKeyConfiguration, ConfigurationViewTargetAgent, IconOrSvg, InlineDescription,
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
ProviderConfigurationView,
};
use ui::{Divider, DividerColor, prelude::*};
use ui::{ButtonLink, ConfiguredApiCard, Divider, DividerColor, prelude::*};
use crate::SettingsWindow;
use crate::components::SettingsInputField;
pub(crate) fn render_llm_providers_page(
settings_window: &SettingsWindow,
@@ -20,7 +22,6 @@ pub(crate) fn render_llm_providers_page(
v_flex()
.id("llm-providers-page")
.size_full()
.pt_2p5()
.px_8()
.pb_16()
.track_scroll(scroll_handle)
@@ -28,94 +29,272 @@ pub(crate) fn render_llm_providers_page(
.children(
providers
.iter()
.map(|provider| render_provider_row(settings_window, provider, window, cx))
.enumerate()
.map(|(index, provider)| {
render_provider_section(settings_window, provider, index == 0, window, cx)
})
.collect::<Vec<_>>(),
)
.into_any_element()
}
fn render_provider_row(
fn render_provider_section(
settings_window: &SettingsWindow,
provider: &Arc<dyn LanguageModelProvider>,
is_first: bool,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
) -> AnyElement {
let provider_id = provider.id();
let provider_name = provider.name().0;
let is_authenticated = provider.is_authenticated(cx);
let icon = match provider.icon() {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.size(IconSize::Small)
.color(Color::Muted);
let left = h_flex()
.flex_none()
.gap_1p5()
.child(icon)
.child(Label::new(provider_name))
.when(is_authenticated, |this| {
this.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
});
// The provider tells us how it wants to be presented: a compact inline
// control, or a richer view that belongs on its own sub-page.
let control =
let body = if let Some(config) = provider.api_key_configuration(cx) {
render_api_key_providers_item(provider, provider_name.clone(), config)
} else {
match get_or_create_configuration_view(settings_window, &provider_id, provider, window, cx)
{
ProviderConfigurationView::Inline(view) => v_flex()
.min_w_0()
.w_full()
.max_w(rems(24.))
.child(view)
.into_any_element(),
ProviderConfigurationView::SubPage(_) => render_configure_button(&provider_id, cx),
};
ProviderConfigurationView::Inline { view } => render_inline_body(provider, view, cx),
ProviderConfigurationView::SubPage(_) => render_subpage_item(provider, cx),
}
};
v_flex()
.min_w_0()
.map(|s| if is_first { s.pt_4() } else { s.pt_8() })
.gap_1p5()
.child(render_provider_header(provider_name, provider.icon(), cx))
.child(body)
.into_any_element()
}
/// An icon + name header with a faded divider, mirroring `SettingsSectionHeader`
/// but able to render providers' external SVG icons.
fn render_provider_header(
provider_name: SharedString,
icon: IconOrSvg,
cx: &mut Context<SettingsWindow>,
) -> impl IntoElement {
let icon = match icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(Color::Muted);
v_flex()
.w_full()
.gap_1p5()
.child(
div()
.px_2()
.child(Divider::horizontal().color(DividerColor::BorderFaded)),
h_flex().gap_1p5().child(icon).child(
Label::new(provider_name)
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
),
)
.child(Divider::horizontal().color(DividerColor::BorderFaded))
}
fn render_api_key_providers_item(
provider: &Arc<dyn LanguageModelProvider>,
provider_name: SharedString,
config: ApiKeyConfiguration,
) -> AnyElement {
let ApiKeyConfiguration {
has_key,
is_from_env_var,
env_var_name,
api_key_url,
} = config;
if has_key {
let configured_label = if is_from_env_var {
"API Key Set in Environment Variable"
} else {
"API Key Configured"
};
let button_id = format!("reset-api-key-{}", provider.id().0);
let provider = provider.clone();
let env_var_name_for_tooltip = env_var_name;
return ConfiguredApiCard::new(button_id, configured_label)
.button_label("Reset Key")
.button_tab_index(0)
.disabled(is_from_env_var)
.when(is_from_env_var, |this| {
this.tooltip_label(format!(
"To reset your API key, unset the {env_var_name_for_tooltip} environment variable."
))
})
.on_click(move |_, _, cx| {
provider.reset_credentials(cx).detach_and_log_err(cx);
})
.into_any_element();
}
let input_id = format!("{}-api-key-input", provider.id().0);
let aria_label = format!("{provider_name} API Key");
let provider = provider.clone();
h_flex()
.pt_2p5()
.w_full()
.min_w_0()
.gap_4()
.justify_between()
.child(
v_flex()
.w_full()
.min_w_0()
.max_w_1_2()
.gap_0p5()
.child(Label::new("API Key"))
.child(
h_flex()
.w_full()
.min_w_0()
.flex_wrap()
.gap_0p5()
.child(
Label::new("Visit the")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
ButtonLink::new(format!("{provider_name} dashboard"), api_key_url)
.no_icon(true)
.label_size(LabelSize::Small)
.label_color(Color::Muted),
)
.child(
Label::new("to generate an API key.")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
Label::new(format!(
"Or set the {env_var_name} env var and restart Zed for it to take effect."
))
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
.child(
h_flex()
.w_full()
.py_2()
.px_2()
.gap_6()
.justify_between()
.items_start()
.child(left)
.child(control),
SettingsInputField::new(input_id)
.tab_index(0)
.with_placeholder("xxxxxxxxxxxxxxxxxxxx")
.aria_label(aria_label)
.on_confirm(move |api_key, _window, cx| {
if let Some(key) = api_key.filter(|key| !key.is_empty()) {
provider.set_api_key(key, cx).detach_and_log_err(cx);
}
}),
)
.into_any_element()
}
fn render_configure_button(
provider_id: &LanguageModelProviderId,
fn render_inline_body(
provider: &Arc<dyn LanguageModelProvider>,
view: AnyView,
cx: &mut Context<SettingsWindow>,
) -> AnyElement {
let provider_id = provider_id.clone();
Button::new(
SharedString::from(format!("configure-{}", provider_id.0)),
"Configure",
)
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.tab_index(0isize)
.on_click(cx.listener(move |this, _, window, cx| {
open_provider_configuration(this, provider_id.clone(), window, cx);
}))
.into_any_element()
let provider_name = provider.name().0;
let title = provider.inline_title(cx);
let description = provider.inline_description(cx);
if title.is_none() && description.is_none() {
return v_flex()
.pt_1()
.w_full()
.min_w_0()
.child(view)
.into_any_element();
}
h_flex()
.pt_2p5()
.w_full()
.min_w_0()
.gap_4()
.justify_between()
.child(
v_flex()
.w_full()
.min_w_0()
.max_w_1_2()
.when_some(title, |this, title| this.child(Label::new(title)))
.when_some(description, |this, description| {
this.child(render_inline_description(provider_name, description))
}),
)
.child(h_flex().flex_none().child(view))
.into_any_element()
}
fn render_subpage_item(
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<SettingsWindow>,
) -> AnyElement {
let provider_id = provider.id();
let provider_name = provider.name().0;
let description = provider.inline_description(cx);
h_flex()
.pt_2p5()
.w_full()
.min_w_0()
.gap_4()
.justify_between()
.child(
v_flex()
.w_full()
.min_w_0()
.max_w_1_2()
.gap_0p5()
.child(Label::new("Configure Provider"))
.when_some(description, |this, description| {
this.child(render_inline_description(provider_name, description))
}),
)
.child(
Button::new(format!("configure-{}", provider_id.0), "Configure")
.style(ButtonStyle::OutlinedGhost)
.size(ButtonSize::Medium)
.end_icon(
Icon::new(IconName::ChevronRight)
.size(IconSize::Small)
.color(Color::Muted),
)
.tab_index(0isize)
.on_click(cx.listener(move |this, _, window, cx| {
open_provider_configuration(this, provider_id.clone(), window, cx);
})),
)
.into_any_element()
}
fn render_inline_description(
provider_name: SharedString,
description: InlineDescription,
) -> AnyElement {
match description {
InlineDescription::ApiKeyUrl(url) => h_flex()
.gap_0p5()
.child(
Label::new("To find an API key, visit the")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
ButtonLink::new(format!("{provider_name} dashboard."), url)
.label_size(LabelSize::Small),
)
.into_any_element(),
InlineDescription::Text(text) => Label::new(text)
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}
}
fn open_provider_configuration(
@@ -164,7 +343,8 @@ fn render_provider_config_sub_page(
window,
cx,
) {
ProviderConfigurationView::Inline(view) | ProviderConfigurationView::SubPage(view) => view,
ProviderConfigurationView::Inline { view, .. }
| ProviderConfigurationView::SubPage(view) => view,
};
v_flex()
+82 -111
View File
@@ -11,8 +11,8 @@ use project::context_server_store::{
use project::project_settings::ContextServerSettings;
use settings::{ContextServerCommand, ContextServerSettingsContent, OAuthClientSettings};
use ui::{
AiSettingItem, AiSettingItemSource, AiSettingItemStatus, ContextMenu, Divider, DividerColor,
PopoverMenu, Switch, ToggleState, Tooltip, prelude::*,
AiSettingItem, AiSettingItemSource, AiSettingItemStatus, ContextMenu, Divider, PopoverMenu,
Switch, ToggleState, Tooltip, prelude::*,
};
use util::ResultExt as _;
@@ -41,34 +41,29 @@ pub(crate) fn render_mcp_servers_page(
};
let timeout_setting = render_context_server_timeout(settings_window, window, cx);
let add_server_popover = render_add_server_popover(settings_window, window, cx);
v_flex()
.id("mcp-servers-page")
.track_scroll(scroll_handle)
.size_full()
.pt_2p5()
.pb_16()
.track_scroll(scroll_handle)
.overflow_y_scroll()
.child(
h_flex()
v_flex()
.w_full()
.px_8()
.justify_between()
.items_center()
.mb_4()
.gap_2()
.child(
v_flex()
.child(Label::new("MCP Servers").size(LabelSize::Large))
.child(
Label::new("Manage Model Context Protocol servers connected directly or via extensions.")
.size(LabelSize::Small)
.color(Color::Muted),
),
v_flex().child(Label::new("Configured Servers")).child(
Label::new("Manage servers connected directly or via extensions.")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(add_server_popover),
.child(server_list)
.child(Divider::horizontal()),
)
.child(div().px_8().child(server_list))
.child(timeout_setting)
.into_any_element()
}
@@ -151,11 +146,7 @@ fn render_server_list(
server_ids
.iter()
.map(|server_id| render_context_server(server_id, store, cx).into_any_element()),
|| {
Divider::horizontal()
.color(DividerColor::BorderFaded)
.into_any_element()
},
|| Divider::horizontal_dashed().into_any_element(),
))
.into_any_element()
}
@@ -213,22 +204,18 @@ fn render_context_server(
None
};
// Build gear menu. Pre-fill "Configure Server" from the raw configured
// settings (not the resolved runtime configuration) so the form is editable
// even when the settings contain invalid data (e.g. an unparsable URL) or
// the server is disabled / not yet started.
let server_settings = store
.read(cx)
.settings_for_server(context_server_id)
.cloned();
let gear_menu = render_gear_menu(
context_server_id,
store,
cx.entity().downgrade(),
server_settings.clone(),
provided_by_extension,
should_show_logout,
);
let configure_button = (!provided_by_extension).then(|| {
render_configure_button(
context_server_id,
cx.entity().downgrade(),
server_settings.clone(),
)
});
let uninstall_button = render_uninstall_button(context_server_id, provided_by_extension);
// Build toggle switch
let toggle_switch = render_toggle_switch(context_server_id, store, is_running);
@@ -241,7 +228,8 @@ fn render_context_server(
};
AiSettingItem::new(item_id, display_name, status, source)
.action(gear_menu)
.when_some(configure_button, |this, button| this.action(button))
.action(uninstall_button)
.action(toggle_switch)
.when_some(tool_label, |this, label| this.detail_label(label))
.when_some(details, |this, details| this.details(details))
@@ -278,85 +266,51 @@ fn resolve_extension_display_name(id: &ContextServerId, cx: &App) -> Option<Shar
})
}
fn render_gear_menu(
fn render_configure_button(
context_server_id: &ContextServerId,
store: &Entity<ContextServerStore>,
settings_window: WeakEntity<SettingsWindow>,
server_settings: Option<ContextServerSettings>,
provided_by_extension: bool,
should_show_logout: bool,
) -> impl IntoElement {
let context_server_id = context_server_id.clone();
let store = store.clone();
PopoverMenu::new(SharedString::from(format!(
"mcp-gear-{}",
context_server_id.0
)))
.trigger_with_tooltip(
IconButton::new(
SharedString::from(format!("mcp-gear-btn-{}", context_server_id.0)),
IconName::Settings,
)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tab_index(0isize),
Tooltip::text("Configure MCP Server"),
IconButton::new(
format!("mcp-configure-btn-{}", context_server_id.0),
IconName::Settings,
)
.anchor(gpui::Anchor::TopRight)
.menu({
move |window, cx| {
let context_server_id = context_server_id.clone();
let store = store.clone();
let settings_window = settings_window.clone();
let server_settings = server_settings.clone();
.icon_size(IconSize::Small)
.tab_index(0isize)
.tooltip(Tooltip::text("Configure MCP Server"))
.on_click(move |_event, window, cx| {
let transport = match &server_settings {
Some(ContextServerSettings::Http { .. }) => McpTransport::Http,
_ => McpTransport::Stdio,
};
let existing = server_settings
.clone()
.map(|settings| (context_server_id.clone(), settings));
settings_window
.update(cx, |this, cx| {
open_mcp_server_form(this, transport, existing, window, cx);
})
.log_err();
})
}
Some(ContextMenu::build(window, cx, move |menu, _window, _cx| {
menu.when(!provided_by_extension, |this| {
this.entry("Configure Server", None, {
let settings_window = settings_window.clone();
let context_server_id = context_server_id.clone();
let server_settings = server_settings.clone();
move |window, cx| {
let transport = match &server_settings {
Some(ContextServerSettings::Http { .. }) => McpTransport::Http,
_ => McpTransport::Stdio,
};
let existing = server_settings
.clone()
.map(|settings| (context_server_id.clone(), settings));
settings_window
.update(cx, |this, cx| {
open_mcp_server_form(this, transport, existing, window, cx);
})
.log_err();
}
})
})
.when(should_show_logout, |this| {
this.entry("Log Out", None, {
let store = store.clone();
let context_server_id = context_server_id.clone();
move |_window, cx| {
store.update(cx, |s, cx| {
s.logout_server(&context_server_id, cx).log_err();
});
}
})
})
// Only show a divider when there is an entry above "Uninstall".
// Extension servers have neither "Configure Server" nor "Log Out".
.when(!provided_by_extension || should_show_logout, |this| {
this.separator()
})
.entry("Uninstall", None, {
let context_server_id = context_server_id.clone();
move |_, cx| {
uninstall_server(&context_server_id, provided_by_extension, cx);
}
})
}))
}
fn render_uninstall_button(
context_server_id: &ContextServerId,
provided_by_extension: bool,
) -> impl IntoElement {
let context_server_id = context_server_id.clone();
IconButton::new(
format!("mcp-uninstall-btn-{}", context_server_id.0),
IconName::Trash,
)
.icon_size(IconSize::Small)
.tab_index(0isize)
.tooltip(Tooltip::text("Uninstall MCP Server"))
.on_click(move |_event, _window, cx| {
uninstall_server(&context_server_id, provided_by_extension, cx);
})
}
@@ -432,7 +386,6 @@ fn render_status_details(
feedback_base()
.child(
h_flex()
.pr_4()
.min_w_0()
.w_full()
.gap_2()
@@ -475,7 +428,6 @@ fn render_status_details(
feedback_base()
.child(
h_flex()
.pr_4()
.min_w_0()
.w_full()
.gap_2()
@@ -509,7 +461,6 @@ fn render_status_details(
feedback_base()
.child(
h_flex()
.pr_4()
.min_w_0()
.w_full()
.gap_2()
@@ -529,7 +480,6 @@ fn render_status_details(
ContextServerStatus::Authenticating => Some(
h_flex()
.mt_1()
.pr_4()
.min_w_0()
.w_full()
.gap_2()
@@ -541,11 +491,32 @@ fn render_status_details(
)
.into_any_element(),
),
ContextServerStatus::Running if should_show_logout => {
let store = store.clone();
let context_server_id = context_server_id.clone();
Some(
h_flex()
.py_1()
.w_full()
.justify_end()
.child(
Button::new("running-logout", "Log Out")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.on_click(move |_event, _window, cx| {
store.update(cx, |s, cx| {
s.logout_server(&context_server_id, cx).log_err();
});
}),
)
.into_any_element(),
)
}
_ => None,
}
}
fn render_add_server_popover(
pub(crate) fn render_add_server_popover(
settings_window: &SettingsWindow,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
@@ -10,8 +10,6 @@ use util::ResultExt as _;
use crate::SettingsWindow;
use crate::components::{SettingsInputField, SettingsSectionHeader};
const SANDBOX_DISCLAIMER: &str = "Customize how the sandbox for the agents tool should behave.";
const DOMAINS_DESCRIPTION: &str = "Each entry is an exact domain (github.com) or a leading-*. subdomain wildcard (*.npmjs.org). IP addresses and local domains are not allowed.";
const WRITE_PATHS_DESCRIPTION: &str =
@@ -58,17 +56,9 @@ pub(crate) fn render_sandbox_settings_page(
.pt_2p5()
.px_8()
.pb_16()
.gap_4()
.gap_6()
.overflow_y_scroll()
.track_scroll(scroll_handle)
.child(
Banner::new().child(
Label::new(SANDBOX_DISCLAIMER)
.size(LabelSize::Small)
.color(Color::Muted)
.mt_0p5(),
),
)
.child(
SwitchField::new(
"sandbox-enabled",
@@ -102,7 +92,7 @@ pub(crate) fn render_sandbox_settings_page(
})
.child(
v_flex()
.gap_3()
.gap_4()
.child(SettingsSectionHeader::new("Network").no_padding(true))
.child(
SwitchField::new(
@@ -130,7 +120,7 @@ pub(crate) fn render_sandbox_settings_page(
.child(Divider::horizontal())
.child(
v_flex()
.gap_3()
.gap_4()
.child(SettingsSectionHeader::new("Git").no_padding(true))
.child(
SwitchField::new(
@@ -151,14 +141,14 @@ pub(crate) fn render_sandbox_settings_page(
.child(Divider::horizontal())
.child(
v_flex()
.gap_3()
.child(SettingsSectionHeader::new("Filesystem").no_padding(true))
.gap_4()
.child(SettingsSectionHeader::new("File System").no_padding(true))
.child(
SwitchField::new(
"sandbox-allow-fs-write-all",
Some("Allow All Filesystem Writes"),
Some("Allow All File System Writes"),
Some(
"Let sandboxed commands write anywhere on the filesystem without prompting."
"Let sandboxed commands write anywhere on the file system without prompting."
.into(),
),
permissions.allow_fs_write_all,
+86 -20
View File
@@ -51,7 +51,7 @@ use workspace::{
};
use zed_actions::{
AGENT_SKILLS_SETTINGS_PATH, OpenProjectSettings, OpenSettings, OpenSettingsAt,
OpenSettingsAtTarget,
OpenSettingsAtTarget, OpenSettingsPage,
};
use crate::components::{
@@ -450,6 +450,15 @@ pub fn init(cx: &mut App) {
cx,
);
})
.register_action(|_, action: &OpenSettingsPage, window, cx| {
let window_handle = window.window_handle().downcast::<MultiWorkspace>();
open_settings_editor_to_page(
&action.page,
action.target.as_ref().map(SettingsFileTarget::from),
window_handle,
cx,
);
})
.register_action(|_, _: &OpenSettings, window, cx| {
let window_handle = window.window_handle().downcast::<MultiWorkspace>();
open_settings_editor(None, None, window_handle, cx);
@@ -669,30 +678,71 @@ pub fn open_settings_editor(
);
}
fn select_settings_file_target(
target_file: SettingsFileTarget,
settings_window: &mut SettingsWindow,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
) {
let file_index = settings_window
.files
.iter()
.position(|(file, _)| match target_file {
SettingsFileTarget::User => matches!(file, SettingsUiFile::User),
SettingsFileTarget::Project(worktree_id) => file.worktree_id() == Some(worktree_id),
});
if let Some(file_index) = file_index {
settings_window.change_file(file_index, window, cx);
}
}
fn open_settings_editor_to_page(
page: &str,
target_file: Option<SettingsFileTarget>,
workspace_handle: Option<WindowHandle<MultiWorkspace>>,
cx: &mut App,
) {
let page = page.to_string();
open_settings_editor_with(workspace_handle, cx, move |settings_window, window, cx| {
if let Some(target_file) = target_file {
select_settings_file_target(target_file, settings_window, window, cx);
}
settings_window.opening_link = false;
settings_window.search_bar.update(cx, |editor, cx| {
editor.set_text(String::new(), window, cx);
});
for page_filter in &mut settings_window.filter_table {
page_filter.fill(true);
}
settings_window.has_query = false;
settings_window.filter_matches_to_file();
let Some(navbar_entry_index) = settings_window
.navbar_entries
.iter()
.position(|entry| entry.is_root && entry.title.eq_ignore_ascii_case(&page))
else {
log::error!("settings page not found: {page}");
return;
};
settings_window.open_and_scroll_to_navbar_entry(
navbar_entry_index,
None,
false,
window,
cx,
);
});
}
fn open_settings_editor_at_target(
path: Option<&str>,
target_file: Option<SettingsFileTarget>,
workspace_handle: Option<WindowHandle<MultiWorkspace>>,
cx: &mut App,
) {
fn select_target_file(
target_file: SettingsFileTarget,
settings_window: &mut SettingsWindow,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
) {
let file_index = settings_window
.files
.iter()
.position(|(file, _)| match target_file {
SettingsFileTarget::User => matches!(file, SettingsUiFile::User),
SettingsFileTarget::Project(worktree_id) => file.worktree_id() == Some(worktree_id),
});
if let Some(file_index) = file_index {
settings_window.change_file(file_index, window, cx);
}
}
/// Assumes a settings GUI window is already open
fn open_path(
path: &str,
@@ -740,7 +790,7 @@ fn open_settings_editor_at_target(
let path = path.map(ToOwned::to_owned);
open_settings_editor_with(workspace_handle, cx, move |settings_window, window, cx| {
if let Some(target_file) = target_file {
select_target_file(target_file, settings_window, window, cx);
select_settings_file_target(target_file, settings_window, window, cx);
}
if let Some(path) = path {
open_path(&path, settings_window, window, cx);
@@ -1772,6 +1822,12 @@ impl SettingsWindow {
})
.detach();
let language_model_registry = language_model::LanguageModelRegistry::global(cx);
cx.subscribe(&language_model_registry, |_, _, _event, cx| {
cx.notify();
})
.detach();
cx.on_window_closed(|cx, _window_id| {
if let Some(existing_window) = cx
.windows()
@@ -3651,6 +3707,9 @@ impl SettingsWindow {
if let Some(current_sub_page) = self.sub_page_stack.last() {
let is_skills_page =
current_sub_page.link.json_path == Some(AGENT_SKILLS_SETTINGS_PATH);
let is_external_agents_page = current_sub_page.link.json_path == Some("agent_servers");
let is_mcp_servers_page = current_sub_page.link.json_path == Some("context_servers");
page_header = h_flex()
.w_full()
.min_w_0()
@@ -3701,6 +3760,12 @@ impl SettingsWindow {
);
})),
)
})
.when(is_external_agents_page, |this| {
this.child(pages::render_add_agent_popover(self, window, cx))
})
.when(is_mcp_servers_page, |this| {
this.child(pages::render_add_server_popover(self, window, cx))
}),
)
.into_any_element();
@@ -5227,6 +5292,7 @@ pub mod test {
theme_settings::init(theme::LoadThemes::JustBase, cx);
editor::init(cx);
menu::init();
language_model::init(cx);
}
fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow {
@@ -21,9 +21,9 @@ impl AiSettingItemStatus {
Self::Starting => "Server is starting.",
Self::Running => "Server is active.",
Self::Error => "Server has an error.",
Self::AuthRequired => "Authentication required.",
Self::ClientSecretRequired => "Client secret required.",
Self::Authenticating => "Waiting for authorization…",
Self::AuthRequired => "Authentication Required.",
Self::ClientSecretRequired => "Client Secret Required.",
Self::Authenticating => "Waiting for Authorization…",
}
}
@@ -198,6 +198,7 @@ impl RenderOnce for AiSettingItem {
v_flex()
.id(id)
.min_w_0()
.py_2()
.child(
h_flex()
.min_w_0()
@@ -1,8 +1,9 @@
use crate::{Tooltip, prelude::*};
use gpui::{ClickEvent, IntoElement, ParentElement, SharedString};
use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, SharedString};
#[derive(IntoElement, RegisterComponent)]
pub struct ConfiguredApiCard {
id: ElementId,
label: SharedString,
button_label: Option<SharedString>,
button_tab_index: Option<isize>,
@@ -12,8 +13,9 @@ pub struct ConfiguredApiCard {
}
impl ConfiguredApiCard {
pub fn new(label: impl Into<SharedString>) -> Self {
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
label: label.into(),
button_label: None,
button_tab_index: None,
@@ -52,79 +54,21 @@ impl ConfiguredApiCard {
}
}
impl Component for ConfiguredApiCard {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn description() -> &'static str {
"A card used in AI provider settings to indicate that an API has been \
configured, with an optional action button to manage or reconfigure it."
}
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
let container = || {
v_flex()
.w_72()
.p_2()
.gap_2()
.border_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().panel_background)
};
let examples = vec![
single_example(
"Default",
container()
.child(ConfiguredApiCard::new("API key is configured"))
.into_any_element(),
),
single_example(
"Custom Button Label",
container()
.child(
ConfiguredApiCard::new("OpenAI API key configured")
.button_label("Remove Key"),
)
.into_any_element(),
),
single_example(
"With Tooltip",
container()
.child(
ConfiguredApiCard::new("Anthropic API key configured")
.tooltip_label("Click to reset your API key"),
)
.into_any_element(),
),
single_example(
"Disabled",
container()
.child(ConfiguredApiCard::new("API key is configured").disabled(true))
.into_any_element(),
),
];
example_group(examples).into_any_element()
}
}
impl RenderOnce for ConfiguredApiCard {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let button_label = self.button_label.unwrap_or("Reset Key".into());
let button_id = SharedString::new(format!("id-{}", button_label));
let button_id = self.id;
h_flex()
.min_w_0()
.mt_0p5()
.p_1()
.flex_wrap()
.justify_between()
.rounded_md()
.flex_wrap()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().background)
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().background.opacity(0.5))
.child(
h_flex()
.min_w_0()
@@ -154,3 +98,63 @@ impl RenderOnce for ConfiguredApiCard {
)
}
}
impl Component for ConfiguredApiCard {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn description() -> &'static str {
"A card used in AI provider settings to indicate that an API has been \
configured, with an optional action button to manage or reconfigure it."
}
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
let container = || {
v_flex()
.w_72()
.p_2()
.gap_2()
.border_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().panel_background)
};
let examples = vec![
single_example(
"Default",
container()
.child(ConfiguredApiCard::new("default", "API key is configured"))
.into_any_element(),
),
single_example(
"Custom Button Label",
container()
.child(
ConfiguredApiCard::new("custom-button-label", "OpenAI API key configured")
.button_label("Remove Key"),
)
.into_any_element(),
),
single_example(
"With Tooltip",
container()
.child(
ConfiguredApiCard::new("with-tooltip", "Anthropic API key configured")
.tooltip_label("Click to reset your API key"),
)
.into_any_element(),
),
single_example(
"Disabled",
container()
.child(
ConfiguredApiCard::new("disabled", "API key is configured").disabled(true),
)
.into_any_element(),
),
];
example_group(examples).into_any_element()
}
}
+1 -5
View File
@@ -177,11 +177,7 @@ impl Render for InputField {
.w_full()
.gap_1()
.when_some(self.label.clone(), |this, label| {
this.child(
Label::new(label)
.size(self.label_size)
.color(Color::Default),
)
this.child(Label::new(label).size(self.label_size))
})
.child(
h_flex()
+12
View File
@@ -149,6 +149,18 @@ pub struct OpenSettingsAt {
pub target: Option<OpenSettingsAtTarget>,
}
#[derive(PartialEq, Clone, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
#[serde(deny_unknown_fields)]
pub struct OpenSettingsPage {
/// A settings page title (e.g. `AI`).
pub page: String,
/// The settings file to select before opening `page`. When omitted, the
/// existing settings file selection is preserved.
#[serde(default)]
pub target: Option<OpenSettingsAtTarget>,
}
/// `OpenSettingsAt` path of the agent skills page in the settings UI.
pub const AGENT_SKILLS_SETTINGS_PATH: &str = "agent.skills";
+18 -15
View File
@@ -1,19 +1,20 @@
---
title: Agent Settings - Zed
description: Map the Agent Settings panel to Zed AI setup pages for LLM providers, External Agents, MCP servers, and related settings.
description: Map the AI settings pages to Zed AI setup for LLM providers, External Agents, MCP servers, and related settings.
---
# Agent Settings
Agent Settings is the in-panel configuration view for model providers, External Agents, and MCP servers. Open it with {#action agent::OpenSettings} or from the top-right menu in the [Agent Panel](./agent-panel.md).
Agent Settings live in the **AI** section of the Settings Editor, which configures model providers, External Agents, and MCP servers.
Open it with {#action agent::OpenSettings} (also available from the top-right menu in the [Agent Panel](./agent-panel.md)), which takes you straight to the AI page.
You can also reach the same page with {#action zed::OpenSettings} and selecting **AI** in the sidebar.
Agent Settings is different from the Settings Editor.
Within the AI page, LLM Providers, External Agents, and MCP Servers each open as their own sub-page under the **General** section.
| Surface | Opens with | Use it for |
| -------------------- | ------------------------------- | ------------------------------------------------------------------------------------ |
| Agent Settings panel | {#action agent::OpenSettings} | LLM providers, External Agents, MCP servers |
| Settings Editor | {#action zed::OpenSettings} | General Zed settings, `disable_ai`, tool permissions, edit prediction provider setup |
| Settings file | {#action zed::OpenSettingsFile} | Direct JSON edits and settings not exposed in UI |
| Surface | Opens with | Use it for |
| --------------- | ------------------------------- | --------------------------------------------------------------------------- |
| Settings Editor | {#action agent::OpenSettings} | AI settings: LLM providers, External Agents, MCP servers, and related pages |
| Settings file | {#action zed::OpenSettingsFile} | Direct JSON edits and settings not exposed in UI |
For general settings mechanics, see [Configuring Zed](../configuring-zed.md).
@@ -95,23 +96,25 @@ For setup details and support boundaries, see [External Agents](./external-agent
## MCP Servers {#mcp-servers}
The `Model Context Protocol (MCP) Servers` section configures MCP servers connected to Zed.
The `MCP Servers` page configures Model Context Protocol servers connected to Zed.
Use `Add Server` to:
Use `Add Server` (in the page header) to:
- `Add Custom Server`
- `Add Local Server`
- `Add Remote Server`
- `Install from Extensions`
Each configured server can expose actions such as:
Each configured server row exposes:
- `Configure Server`
- `View Tools`
- a `Configure` (gear) button to edit the server
- an `Uninstall` (trash) button to remove it
- a toggle to enable or disable it
For MCP setup, auth, server status, and agent-path boundaries, see [MCP](./mcp.md).
## Related Configuration {#related-configuration}
Some AI settings are not configured in the Agent Settings panel:
Some AI settings are not configured on the AI settings pages:
| Task | Go to |
| ------------------------------------------------------------ | ---------------------------------------------- |
+2 -2
View File
@@ -17,7 +17,7 @@ For Zed-hosted models and Zed-managed AI features, see [AI Privacy](./privacy-an
The ACP Registry is the primary way to install common External Agents in Zed.
Open the registry with {#action zed::AcpRegistry}, or open [Agent Settings](./agent-settings.md) with {#action agent::OpenSettings}, click `Add Agent`, and choose `Install from Registry`.
Open the registry with {#action zed::AcpRegistry}, or open [Agent Settings](./agent-settings.md) with {#action agent::OpenSettings}, go to the **External Agents** page, click `Add Agent`, and choose `Install from Registry`.
After installation, the agent appears in the new-thread menu in the Agent Panel and Threads Sidebar.
@@ -130,7 +130,7 @@ Zed LLM provider API keys saved in the local keychain are not automatically the
Use custom agents when you are developing an ACP-compatible agent or need to run an agent that is not in the registry.
Open [Agent Settings](./agent-settings.md), click `Add Agent`, and choose `Add Custom Agent`. Zed opens your settings file with an `agent_servers` entry.
Open [Agent Settings](./agent-settings.md), go to the **External Agents** page, click `Add Agent`, and choose `Add Custom Agent`. Zed opens your settings file with an `agent_servers` entry.
```json [settings]
{
+3 -3
View File
@@ -35,7 +35,7 @@ Many MCP servers are available as extensions. Find them via:
1. [the Zed website](https://zed.dev/extensions?filter=context-servers)
2. in the app, open the Command Palette and run the {#action zed::Extensions} action
3. in the app, go to the Agent Panel's top-right menu and look for the "Install New Servers…" menu item under the "MCP Servers" section
3. in the app, open **Settings → AI → MCP Servers**, click `Add Server`, and choose `Install from Extensions`
Popular servers available as an extension include:
@@ -72,7 +72,7 @@ You can connect both local and remote MCP servers easily utilizing the MCP serve
}
```
Alternatively, you can also open the modal by accessing the Agent Panel's Settings view (also accessible via the {#action agent::OpenSettings} action) and clicking the "Add Custom Server" button there.
Alternatively, you can open the modal from **Settings → AI → MCP Servers** (also accessible via the {#action agent::OpenSettings} action, then selecting `MCP Servers`) and clicking the `Add Server` button in the page header, then choosing `Add Local Server` or `Add Remote Server`.
> Note: When a remote MCP server has no configured `"Authorization"` header, Zed will prompt you to authenticate yourself against the MCP server using the standard MCP OAuth flow.
@@ -87,7 +87,7 @@ For example, the GitHub MCP extension requires you to add a [Personal Access Tok
In the case of custom servers, make sure you check the provider documentation to determine what type of command, arguments, and environment variables need to be added to the JSON.
To check if your MCP server is properly configured, go to the Agent Panel's settings view and watch the indicator dot next to its name.
To check if your MCP server is properly configured, open **Settings → AI → MCP Servers** and watch the indicator dot next to its name.
If it's running correctly, the indicator will be green and its tooltip will say "Server is active".
If not, other colors and tooltip messages will indicate what is happening.
+2 -2
View File
@@ -20,7 +20,7 @@ Use OpenRouter when you want to route Zed AI features through OpenRouter.
1. Visit [OpenRouter](https://openrouter.ai) and create an account.
2. Generate an API key from your [OpenRouter keys page](https://openrouter.ai/keys).
3. Open Agent Settings with {#action agent::OpenSettings} and go to the OpenRouter section.
3. Open **Settings → AI → LLM Providers** with {#action agent::OpenSettings} and find the OpenRouter row.
4. Enter your OpenRouter API key.
Zed also reads `OPENROUTER_API_KEY` from the local Zed process environment.
@@ -104,7 +104,7 @@ Supported fields include `order`, `allow_fallbacks`, `require_parameters`, `data
Use Vercel AI Gateway when you want to route Zed AI features through Vercel.
1. Create an API key from your Vercel AI Gateway keys page.
2. Open Agent Settings with {#action agent::OpenSettings} and go to the Vercel AI Gateway section.
2. Open **Settings → AI → LLM Providers** with {#action agent::OpenSettings} and find the Vercel AI Gateway row.
3. Enter your Vercel AI Gateway API key.
Zed also reads `VERCEL_AI_GATEWAY_API_KEY` from the local Zed process environment.
+1 -1
View File
@@ -41,7 +41,7 @@ agent paths and model access paths.
## API Keys and Environment Variables {#api-keys}
Most API-access providers can be configured in Zed's Agent Settings panel with {#action agent::OpenSettings}. Keys saved through Zed are stored in the system keychain, not in `settings.json`.
Most API-access providers can be configured on the **Settings → AI → LLM Providers** page with {#action agent::OpenSettings}. Keys saved through Zed are stored in the system keychain, not in `settings.json`.
Zed also reads provider-specific environment variables. Non-empty environment variables take precedence over keychain values. If a key comes from an environment variable, unset the variable and restart Zed to stop using it.