diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25a1ed8670..23f0b3915a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -800,7 +800,8 @@ jobs: - name: Upload Artifacts to release uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 - if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview + # Re-enable when we are ready to publish windows preview releases + if: false && ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview with: draft: true prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} diff --git a/Cargo.lock b/Cargo.lock index 38bb7819ca..7da1b6064b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3043,6 +3043,7 @@ dependencies = [ "context_server", "ctor", "dap", + "dap-types", "dap_adapters", "dashmap 6.1.0", "debugger_ui", @@ -9000,6 +9001,7 @@ dependencies = [ "util", "vercel", "workspace-hack", + "x_ai", "zed_llm_client", ] @@ -19731,6 +19733,17 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "x_ai" +version = "0.1.0" +dependencies = [ + "anyhow", + "schemars", + "serde", + "strum 0.27.1", + "workspace-hack", +] + [[package]] name = "xattr" version = "0.2.3" @@ -19972,7 +19985,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.195.0" +version = "0.195.2" dependencies = [ "activity_indicator", "agent", diff --git a/Cargo.toml b/Cargo.toml index a4d8b3cb95..d1042c499c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,6 +177,7 @@ members = [ "crates/welcome", "crates/workspace", "crates/worktree", + "crates/x_ai", "crates/zed", "crates/zed_actions", "crates/zeta", @@ -390,6 +391,7 @@ web_search_providers = { path = "crates/web_search_providers" } welcome = { path = "crates/welcome" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } +x_ai = { path = "crates/x_ai" } zed = { path = "crates/zed" } zed_actions = { path = "crates/zed_actions" } zeta = { path = "crates/zeta" } diff --git a/assets/icons/ai_x_ai.svg b/assets/icons/ai_x_ai.svg new file mode 100644 index 0000000000..289525c8ef --- /dev/null +++ b/assets/icons/ai_x_ai.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 8bfdd50761..579331c9ac 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -491,6 +491,7 @@ impl AgentConfiguration { category_filter: Some( ExtensionCategoryFilter::ContextServers, ), + id: None, } .boxed_clone(), cx, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 5f58e0bd8d..47adcd859e 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1778,6 +1778,7 @@ impl AgentPanel { category_filter: Some( zed_actions::ExtensionCategoryFilter::ContextServers, ), + id: None, }), ) .action("Add Custom Server…", Box::new(AddContextServer)) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 55c15cac5a..7b536a2d24 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -94,6 +94,7 @@ context_server.workspace = true ctor.workspace = true dap = { workspace = true, features = ["test-support"] } dap_adapters = { workspace = true, features = ["test-support"] } +dap-types.workspace = true debugger_ui = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } extension.workspace = true diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 7aeb381c02..8ab6e6910c 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -2,6 +2,7 @@ use crate::tests::TestServer; use call::ActiveCall; use collections::{HashMap, HashSet}; +use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling}; use debugger_ui::debugger_panel::DebugPanel; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs as _, RemoveOptions}; @@ -22,6 +23,7 @@ use language::{ use node_runtime::NodeRuntime; use project::{ ProjectPath, + debugger::session::ThreadId, lsp_store::{FormatTrigger, LspFormatTarget}, }; use remote::SshRemoteClient; @@ -29,7 +31,11 @@ use remote_server::{HeadlessAppState, HeadlessProject}; use rpc::proto; use serde_json::json; use settings::SettingsStore; -use std::{path::Path, sync::Arc}; +use std::{ + path::Path, + sync::{Arc, atomic::AtomicUsize}, +}; +use task::TcpArgumentsTemplate; use util::path; #[gpui::test(iterations = 10)] @@ -688,3 +694,162 @@ async fn test_remote_server_debugger( shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_slow_adapter_startup_retries( + cx_a: &mut TestAppContext, + server_cx: &mut TestAppContext, + executor: BackgroundExecutor, +) { + cx_a.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + command_palette_hooks::init(cx); + zlog::init_test(); + dap_adapters::init(cx); + }); + server_cx.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + dap_adapters::init(cx); + }); + let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let remote_fs = FakeFs::new(server_cx.executor()); + remote_fs + .insert_tree( + path!("/code"), + json!({ + "lib.rs": "fn one() -> usize { 1 }" + }), + ) + .await; + + // User A connects to the remote project via SSH. + server_cx.update(HeadlessProject::init); + let remote_http_client = Arc::new(BlockedHttpClient); + let node = NodeRuntime::unavailable(); + let languages = Arc::new(LanguageRegistry::new(server_cx.executor())); + let _headless_project = server_cx.new(|cx| { + client::init_settings(cx); + HeadlessProject::new( + HeadlessAppState { + session: server_ssh, + fs: remote_fs.clone(), + http_client: remote_http_client, + node_runtime: node, + languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), + }, + cx, + ) + }); + + let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let mut server = TestServer::start(server_cx.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + cx_a.update(|cx| { + debugger_ui::init(cx); + command_palette_hooks::init(cx); + }); + let (project_a, _) = client_a + .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a) + .await; + + let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a); + + let debugger_panel = workspace + .update_in(cx_a, |_workspace, window, cx| { + cx.spawn_in(window, DebugPanel::load) + }) + .await + .unwrap(); + + workspace.update_in(cx_a, |workspace, window, cx| { + workspace.add_panel(debugger_panel, window, cx); + }); + + cx_a.run_until_parked(); + let debug_panel = workspace + .update(cx_a, |workspace, cx| workspace.panel::(cx)) + .unwrap(); + + let workspace_window = cx_a + .window_handle() + .downcast::() + .unwrap(); + + let count = Arc::new(AtomicUsize::new(0)); + let session = debugger_ui::tests::start_debug_session_with( + &workspace_window, + cx_a, + DebugTaskDefinition { + adapter: "fake-adapter".into(), + label: "test".into(), + config: json!({ + "request": "launch" + }), + tcp_connection: Some(TcpArgumentsTemplate { + port: None, + host: None, + timeout: None, + }), + }, + move |client| { + let count = count.clone(); + client.on_request_ext::(move |_seq, _request| { + if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 { + return RequestHandling::Exit; + } + RequestHandling::Respond(Ok(Capabilities::default())) + }); + }, + ) + .unwrap(); + cx_a.run_until_parked(); + + let client = session.update(cx_a, |session, _| session.adapter_client().unwrap()); + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + + let active_session = debug_panel + .update(cx_a, |this, _| this.active_session()) + .unwrap(); + + let running_state = active_session.update(cx_a, |active_session, _| { + active_session.running_state().clone() + }); + + assert_eq!( + client.id(), + running_state.read_with(cx_a, |running_state, _| running_state.session_id()) + ); + assert_eq!( + ThreadId(1), + running_state.read_with(cx_a, |running_state, _| running_state + .selected_thread_id() + .unwrap()) + ); + + let shutdown_session = workspace.update(cx_a, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(session.read(cx).session_id(), cx) + }) + }) + }); + + client_ssh.update(cx_a, |a, _| { + a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index d9f26b3b34..bd36b07387 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -442,10 +442,18 @@ impl DebugAdapter for FakeAdapter { _: Option>, _: &mut AsyncApp, ) -> Result { + let connection = task_definition + .tcp_connection + .as_ref() + .map(|connection| TcpArguments { + host: connection.host(), + port: connection.port.unwrap_or(17), + timeout: connection.timeout, + }); Ok(DebugAdapterBinary { command: Some("command".into()), arguments: vec![], - connection: None, + connection, envs: HashMap::default(), cwd: None, request_args: StartDebuggingRequestArguments { diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index ff082e3b76..86a15b2d8a 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -2,7 +2,7 @@ use crate::{ adapters::DebugAdapterBinary, transport::{IoKind, LogKind, TransportDelegate}, }; -use anyhow::{Context as _, Result}; +use anyhow::Result; use dap_types::{ messages::{Message, Response}, requests::Request, @@ -110,9 +110,7 @@ impl DebugAdapterClient { self.transport_delegate .pending_requests .lock() - .as_mut() - .context("client is closed")? - .insert(sequence_id, callback_tx); + .insert(sequence_id, callback_tx)?; log::debug!( "Client {} send `{}` request with sequence_id: {}", @@ -170,6 +168,7 @@ impl DebugAdapterClient { pub fn kill(&self) { log::debug!("Killing DAP process"); self.transport_delegate.transport.lock().kill(); + self.transport_delegate.pending_requests.lock().shutdown(); } pub fn has_adapter_logs(&self) -> bool { @@ -184,11 +183,34 @@ impl DebugAdapterClient { } #[cfg(any(test, feature = "test-support"))] - pub fn on_request(&self, handler: F) + pub fn on_request(&self, mut handler: F) where F: 'static + Send + FnMut(u64, R::Arguments) -> Result, + { + use crate::transport::RequestHandling; + + self.transport_delegate + .transport + .lock() + .as_fake() + .on_request::(move |seq, request| { + RequestHandling::Respond(handler(seq, request)) + }); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn on_request_ext(&self, handler: F) + where + F: 'static + + Send + + FnMut( + u64, + R::Arguments, + ) -> crate::transport::RequestHandling< + Result, + >, { self.transport_delegate .transport diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 14370f66e4..6dadf1cf35 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -49,6 +49,12 @@ pub enum IoKind { StdErr, } +#[cfg(any(test, feature = "test-support"))] +pub enum RequestHandling { + Respond(T), + Exit, +} + type LogHandlers = Arc>>; pub trait Transport: Send + Sync { @@ -76,7 +82,11 @@ async fn start( ) -> Result> { #[cfg(any(test, feature = "test-support"))] if cfg!(any(test, feature = "test-support")) { - return Ok(Box::new(FakeTransport::start(cx).await?)); + if let Some(connection) = binary.connection.clone() { + return Ok(Box::new(FakeTransport::start_tcp(connection, cx).await?)); + } else { + return Ok(Box::new(FakeTransport::start_stdio(cx).await?)); + } } if binary.connection.is_some() { @@ -90,11 +100,57 @@ async fn start( } } +pub(crate) struct PendingRequests { + inner: Option>>>, +} + +impl PendingRequests { + fn new() -> Self { + Self { + inner: Some(HashMap::default()), + } + } + + fn flush(&mut self, e: anyhow::Error) { + let Some(inner) = self.inner.as_mut() else { + return; + }; + for (_, sender) in inner.drain() { + sender.send(Err(e.cloned())).ok(); + } + } + + pub(crate) fn insert( + &mut self, + sequence_id: u64, + callback_tx: oneshot::Sender>, + ) -> anyhow::Result<()> { + let Some(inner) = self.inner.as_mut() else { + bail!("client is closed") + }; + inner.insert(sequence_id, callback_tx); + Ok(()) + } + + pub(crate) fn remove( + &mut self, + sequence_id: u64, + ) -> anyhow::Result>>> { + let Some(inner) = self.inner.as_mut() else { + bail!("client is closed"); + }; + Ok(inner.remove(&sequence_id)) + } + + pub(crate) fn shutdown(&mut self) { + self.flush(anyhow!("transport shutdown")); + self.inner = None; + } +} + pub(crate) struct TransportDelegate { log_handlers: LogHandlers, - // TODO this should really be some kind of associative channel - pub(crate) pending_requests: - Arc>>>>>, + pub(crate) pending_requests: Arc>, pub(crate) transport: Mutex>, pub(crate) server_tx: smol::lock::Mutex>>, tasks: Mutex>>, @@ -108,7 +164,7 @@ impl TransportDelegate { transport: Mutex::new(transport), log_handlers, server_tx: Default::default(), - pending_requests: Arc::new(Mutex::new(Some(HashMap::default()))), + pending_requests: Arc::new(Mutex::new(PendingRequests::new())), tasks: Default::default(), }) } @@ -151,24 +207,10 @@ impl TransportDelegate { Ok(()) => { pending_requests .lock() - .take() - .into_iter() - .flatten() - .for_each(|(_, request)| { - request - .send(Err(anyhow!("debugger shutdown unexpectedly"))) - .ok(); - }); + .flush(anyhow!("debugger shutdown unexpectedly")); } Err(e) => { - pending_requests - .lock() - .take() - .into_iter() - .flatten() - .for_each(|(_, request)| { - request.send(Err(e.cloned())).ok(); - }); + pending_requests.lock().flush(e); } } })); @@ -286,7 +328,7 @@ impl TransportDelegate { async fn recv_from_server( server_stdout: Stdout, mut message_handler: DapMessageHandler, - pending_requests: Arc>>>>>, + pending_requests: Arc>, log_handlers: Option, ) -> Result<()> where @@ -303,14 +345,10 @@ impl TransportDelegate { ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"), ConnectionResult::ConnectionReset => { log::info!("Debugger closed the connection"); - break Ok(()); + return Ok(()); } ConnectionResult::Result(Ok(Message::Response(res))) => { - let tx = pending_requests - .lock() - .as_mut() - .context("client is closed")? - .remove(&res.request_seq); + let tx = pending_requests.lock().remove(res.request_seq)?; if let Some(tx) = tx { if let Err(e) = tx.send(Self::process_response(res)) { log::trace!("Did not send response `{:?}` for a cancelled", e); @@ -704,8 +742,7 @@ impl Drop for StdioTransport { } #[cfg(any(test, feature = "test-support"))] -type RequestHandler = - Box dap_types::messages::Response>; +type RequestHandler = Box RequestHandling>; #[cfg(any(test, feature = "test-support"))] type ResponseHandler = Box; @@ -716,23 +753,38 @@ pub struct FakeTransport { request_handlers: Arc>>, // for reverse request responses response_handlers: Arc>>, - - stdin_writer: Option, - stdout_reader: Option, message_handler: Option>>, + kind: FakeTransportKind, +} + +#[cfg(any(test, feature = "test-support"))] +pub enum FakeTransportKind { + Stdio { + stdin_writer: Option, + stdout_reader: Option, + }, + Tcp { + connection: TcpArguments, + executor: BackgroundExecutor, + }, } #[cfg(any(test, feature = "test-support"))] impl FakeTransport { pub fn on_request(&self, mut handler: F) where - F: 'static + Send + FnMut(u64, R::Arguments) -> Result, + F: 'static + + Send + + FnMut(u64, R::Arguments) -> RequestHandling>, { self.request_handlers.lock().insert( R::COMMAND, Box::new(move |seq, args| { let result = handler(seq, serde_json::from_value(args).unwrap()); - let response = match result { + let RequestHandling::Respond(response) = result else { + return RequestHandling::Exit; + }; + let response = match response { Ok(response) => Response { seq: seq + 1, request_seq: seq, @@ -750,7 +802,7 @@ impl FakeTransport { message: None, }, }; - response + RequestHandling::Respond(response) }), ); } @@ -764,86 +816,75 @@ impl FakeTransport { .insert(R::COMMAND, Box::new(handler)); } - async fn start(cx: &mut AsyncApp) -> Result { + async fn start_tcp(connection: TcpArguments, cx: &mut AsyncApp) -> Result { + Ok(Self { + request_handlers: Arc::new(Mutex::new(HashMap::default())), + response_handlers: Arc::new(Mutex::new(HashMap::default())), + message_handler: None, + kind: FakeTransportKind::Tcp { + connection, + executor: cx.background_executor().clone(), + }, + }) + } + + async fn handle_messages( + request_handlers: Arc>>, + response_handlers: Arc>>, + stdin_reader: PipeReader, + stdout_writer: PipeWriter, + ) -> Result<()> { use dap_types::requests::{Request, RunInTerminal, StartDebugging}; use serde_json::json; - let (stdin_writer, stdin_reader) = async_pipe::pipe(); - let (stdout_writer, stdout_reader) = async_pipe::pipe(); - - let mut this = Self { - request_handlers: Arc::new(Mutex::new(HashMap::default())), - response_handlers: Arc::new(Mutex::new(HashMap::default())), - stdin_writer: Some(stdin_writer), - stdout_reader: Some(stdout_reader), - message_handler: None, - }; - - let request_handlers = this.request_handlers.clone(); - let response_handlers = this.response_handlers.clone(); + let mut reader = BufReader::new(stdin_reader); let stdout_writer = Arc::new(smol::lock::Mutex::new(stdout_writer)); + let mut buffer = String::new(); - this.message_handler = Some(cx.background_spawn(async move { - let mut reader = BufReader::new(stdin_reader); - let mut buffer = String::new(); - - loop { - match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None) - .await - { - ConnectionResult::Timeout => { - anyhow::bail!("Timed out when connecting to debugger"); - } - ConnectionResult::ConnectionReset => { - log::info!("Debugger closed the connection"); - break Ok(()); - } - ConnectionResult::Result(Err(e)) => break Err(e), - ConnectionResult::Result(Ok(message)) => { - match message { - Message::Request(request) => { - // redirect reverse requests to stdout writer/reader - if request.command == RunInTerminal::COMMAND - || request.command == StartDebugging::COMMAND - { - let message = - serde_json::to_string(&Message::Request(request)).unwrap(); - - let mut writer = stdout_writer.lock().await; - writer - .write_all( - TransportDelegate::build_rpc_message(message) - .as_bytes(), - ) - .await - .unwrap(); - writer.flush().await.unwrap(); - } else { - let response = if let Some(handle) = - request_handlers.lock().get_mut(request.command.as_str()) - { - handle(request.seq, request.arguments.unwrap_or(json!({}))) - } else { - panic!("No request handler for {}", request.command); - }; - let message = - serde_json::to_string(&Message::Response(response)) - .unwrap(); - - let mut writer = stdout_writer.lock().await; - writer - .write_all( - TransportDelegate::build_rpc_message(message) - .as_bytes(), - ) - .await - .unwrap(); - writer.flush().await.unwrap(); - } - } - Message::Event(event) => { + loop { + match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None).await { + ConnectionResult::Timeout => { + anyhow::bail!("Timed out when connecting to debugger"); + } + ConnectionResult::ConnectionReset => { + log::info!("Debugger closed the connection"); + break Ok(()); + } + ConnectionResult::Result(Err(e)) => break Err(e), + ConnectionResult::Result(Ok(message)) => { + match message { + Message::Request(request) => { + // redirect reverse requests to stdout writer/reader + if request.command == RunInTerminal::COMMAND + || request.command == StartDebugging::COMMAND + { let message = - serde_json::to_string(&Message::Event(event)).unwrap(); + serde_json::to_string(&Message::Request(request)).unwrap(); + + let mut writer = stdout_writer.lock().await; + writer + .write_all( + TransportDelegate::build_rpc_message(message).as_bytes(), + ) + .await + .unwrap(); + writer.flush().await.unwrap(); + } else { + let response = if let Some(handle) = + request_handlers.lock().get_mut(request.command.as_str()) + { + handle(request.seq, request.arguments.unwrap_or(json!({}))) + } else { + panic!("No request handler for {}", request.command); + }; + let response = match response { + RequestHandling::Respond(response) => response, + RequestHandling::Exit => { + break Err(anyhow!("exit in response to request")); + } + }; + let message = + serde_json::to_string(&Message::Response(response)).unwrap(); let mut writer = stdout_writer.lock().await; writer @@ -854,20 +895,56 @@ impl FakeTransport { .unwrap(); writer.flush().await.unwrap(); } - Message::Response(response) => { - if let Some(handle) = - response_handlers.lock().get(response.command.as_str()) - { - handle(response); - } else { - log::error!("No response handler for {}", response.command); - } + } + Message::Event(event) => { + let message = serde_json::to_string(&Message::Event(event)).unwrap(); + + let mut writer = stdout_writer.lock().await; + writer + .write_all(TransportDelegate::build_rpc_message(message).as_bytes()) + .await + .unwrap(); + writer.flush().await.unwrap(); + } + Message::Response(response) => { + if let Some(handle) = + response_handlers.lock().get(response.command.as_str()) + { + handle(response); + } else { + log::error!("No response handler for {}", response.command); } } } } } - })); + } + } + + async fn start_stdio(cx: &mut AsyncApp) -> Result { + let (stdin_writer, stdin_reader) = async_pipe::pipe(); + let (stdout_writer, stdout_reader) = async_pipe::pipe(); + let kind = FakeTransportKind::Stdio { + stdin_writer: Some(stdin_writer), + stdout_reader: Some(stdout_reader), + }; + + let mut this = Self { + request_handlers: Arc::new(Mutex::new(HashMap::default())), + response_handlers: Arc::new(Mutex::new(HashMap::default())), + message_handler: None, + kind, + }; + + let request_handlers = this.request_handlers.clone(); + let response_handlers = this.response_handlers.clone(); + + this.message_handler = Some(cx.background_spawn(Self::handle_messages( + request_handlers, + response_handlers, + stdin_reader, + stdout_writer, + ))); Ok(this) } @@ -876,7 +953,10 @@ impl FakeTransport { #[cfg(any(test, feature = "test-support"))] impl Transport for FakeTransport { fn tcp_arguments(&self) -> Option { - None + match &self.kind { + FakeTransportKind::Stdio { .. } => None, + FakeTransportKind::Tcp { connection, .. } => Some(connection.clone()), + } } fn connect( @@ -887,12 +967,33 @@ impl Transport for FakeTransport { Box, )>, > { - let result = util::maybe!({ - Ok(( - Box::new(self.stdin_writer.take().context("Cannot reconnect")?) as _, - Box::new(self.stdout_reader.take().context("Cannot reconnect")?) as _, - )) - }); + let result = match &mut self.kind { + FakeTransportKind::Stdio { + stdin_writer, + stdout_reader, + } => util::maybe!({ + Ok(( + Box::new(stdin_writer.take().context("Cannot reconnect")?) as _, + Box::new(stdout_reader.take().context("Cannot reconnect")?) as _, + )) + }), + FakeTransportKind::Tcp { executor, .. } => { + let (stdin_writer, stdin_reader) = async_pipe::pipe(); + let (stdout_writer, stdout_reader) = async_pipe::pipe(); + + let request_handlers = self.request_handlers.clone(); + let response_handlers = self.response_handlers.clone(); + + self.message_handler = Some(executor.spawn(Self::handle_messages( + request_handlers, + response_handlers, + stdin_reader, + stdout_writer, + ))); + + Ok((Box::new(stdin_writer) as _, Box::new(stdout_reader) as _)) + } + }; Task::ready(result) } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 988f6f4019..ffde772c36 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1694,6 +1694,7 @@ impl Render for DebugPanel { category_filter: Some( zed_actions::ExtensionCategoryFilter::DebugAdapters, ), + id: None, } .boxed_clone(), cx, diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 482297b136..4cc2602909 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -122,7 +122,7 @@ impl DebugSession { .to_owned() } - pub(crate) fn running_state(&self) -> &Entity { + pub fn running_state(&self) -> &Entity { &self.running_state } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index af8c14aef7..d308fc9bd2 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1459,7 +1459,7 @@ impl RunningState { } } - pub(crate) fn selected_thread_id(&self) -> Option { + pub fn selected_thread_id(&self) -> Option { self.thread_id } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c5fe0db74c..263544dba6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -482,9 +482,7 @@ pub enum SelectMode { #[derive(Clone, PartialEq, Eq, Debug)] pub enum EditorMode { - SingleLine { - auto_width: bool, - }, + SingleLine, AutoHeight { min_lines: usize, max_lines: Option, @@ -1662,13 +1660,7 @@ impl Editor { pub fn single_line(window: &mut Window, cx: &mut Context) -> Self { let buffer = cx.new(|cx| Buffer::local("", cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: false }, - buffer, - None, - window, - cx, - ) + Self::new(EditorMode::SingleLine, buffer, None, window, cx) } pub fn multi_line(window: &mut Window, cx: &mut Context) -> Self { @@ -1677,18 +1669,6 @@ impl Editor { Self::new(EditorMode::full(), buffer, None, window, cx) } - pub fn auto_width(window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: true }, - buffer, - None, - window, - cx, - ) - } - pub fn auto_height( min_lines: usize, max_lines: usize, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8a5bfb3bab..8db65189f8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7777,46 +7777,13 @@ impl Element for EditorElement { editor.set_style(self.style.clone(), window, cx); let layout_id = match editor.mode { - EditorMode::SingleLine { auto_width } => { + EditorMode::SingleLine => { let rem_size = window.rem_size(); - let height = self.style.text.line_height_in_pixels(rem_size); - if auto_width { - let editor_handle = cx.entity().clone(); - let style = self.style.clone(); - window.request_measured_layout( - Style::default(), - move |_, _, window, cx| { - let editor_snapshot = editor_handle - .update(cx, |editor, cx| editor.snapshot(window, cx)); - let line = Self::layout_lines( - DisplayRow(0)..DisplayRow(1), - &editor_snapshot, - &style, - px(f32::MAX), - |_| false, // Single lines never soft wrap - window, - cx, - ) - .pop() - .unwrap(); - - let font_id = - window.text_system().resolve_font(&style.text.font()); - let font_size = - style.text.font_size.to_pixels(window.rem_size()); - let em_width = - window.text_system().em_width(font_id, font_size).unwrap(); - - size(line.width + em_width, height) - }, - ) - } else { - let mut style = Style::default(); - style.size.height = height.into(); - style.size.width = relative(1.).into(); - window.request_layout(style, None, cx) - } + let mut style = Style::default(); + style.size.height = height.into(); + style.size.width = relative(1.).into(); + window.request_layout(style, None, cx) } EditorMode::AutoHeight { min_lines, @@ -10388,7 +10355,7 @@ mod tests { }); for editor_mode_without_invisibles in [ - EditorMode::SingleLine { auto_width: false }, + EditorMode::SingleLine, EditorMode::AutoHeight { min_lines: 1, max_lines: Some(100), diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 48cb41a006..f97470da22 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -6,6 +6,7 @@ use std::sync::OnceLock; use std::time::Duration; use std::{ops::Range, sync::Arc}; +use anyhow::Context as _; use client::{ExtensionMetadata, ExtensionProvides}; use collections::{BTreeMap, BTreeSet}; use editor::{Editor, EditorElement, EditorStyle}; @@ -80,16 +81,24 @@ pub fn init(cx: &mut App) { .find_map(|item| item.downcast::()); if let Some(existing) = existing { - if provides_filter.is_some() { - existing.update(cx, |extensions_page, cx| { + existing.update(cx, |extensions_page, cx| { + if provides_filter.is_some() { extensions_page.change_provides_filter(provides_filter, cx); - }); - } + } + if let Some(id) = action.id.as_ref() { + extensions_page.focus_extension(id, window, cx); + } + }); workspace.activate_item(&existing, true, true, window, cx); } else { - let extensions_page = - ExtensionsPage::new(workspace, provides_filter, window, cx); + let extensions_page = ExtensionsPage::new( + workspace, + provides_filter, + action.id.as_deref(), + window, + cx, + ); workspace.add_item_to_active_pane( Box::new(extensions_page), None, @@ -287,6 +296,7 @@ impl ExtensionsPage { pub fn new( workspace: &Workspace, provides_filter: Option, + focus_extension_id: Option<&str>, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -317,6 +327,9 @@ impl ExtensionsPage { let query_editor = cx.new(|cx| { let mut input = Editor::single_line(window, cx); input.set_placeholder_text("Search extensions...", cx); + if let Some(id) = focus_extension_id { + input.set_text(format!("id:{id}"), window, cx); + } input }); cx.subscribe(&query_editor, Self::on_query_change).detach(); @@ -340,7 +353,7 @@ impl ExtensionsPage { scrollbar_state: ScrollbarState::new(scroll_handle), }; this.fetch_extensions( - None, + this.search_query(cx), Some(BTreeSet::from_iter(this.provides_filter)), None, cx, @@ -464,9 +477,23 @@ impl ExtensionsPage { .cloned() .collect::>(); - let remote_extensions = extension_store.update(cx, |store, cx| { - store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx) - }); + let remote_extensions = + if let Some(id) = search.as_ref().and_then(|s| s.strip_prefix("id:")) { + let versions = + extension_store.update(cx, |store, cx| store.fetch_extension_versions(id, cx)); + cx.foreground_executor().spawn(async move { + let versions = versions.await?; + let latest = versions + .into_iter() + .max_by_key(|v| v.published_at) + .context("no extension found")?; + Ok(vec![latest]) + }) + } else { + extension_store.update(cx, |store, cx| { + store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx) + }) + }; cx.spawn(async move |this, cx| { let dev_extensions = if let Some(search) = search { @@ -1156,6 +1183,13 @@ impl ExtensionsPage { self.refresh_feature_upsells(cx); } + pub fn focus_extension(&mut self, id: &str, window: &mut Window, cx: &mut Context) { + self.query_editor.update(cx, |editor, cx| { + editor.set_text(format!("id:{id}"), window, cx) + }); + self.refresh_search(cx); + } + pub fn change_provides_filter( &mut self, provides_filter: Option, diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 332e38b038..332a8d5791 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -20,6 +20,7 @@ pub enum IconName { AiOpenAi, AiOpenRouter, AiVZero, + AiXAi, AiZed, ArrowCircle, ArrowDown, diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 514443ddec..e928df8a74 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } vercel = { workspace = true, features = ["schemars"] } +x_ai = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true proto.workspace = true release_channel.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index c7324732c9..192f5a5fae 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -20,6 +20,7 @@ use crate::provider::ollama::OllamaLanguageModelProvider; use crate::provider::open_ai::OpenAiLanguageModelProvider; use crate::provider::open_router::OpenRouterLanguageModelProvider; use crate::provider::vercel::VercelLanguageModelProvider; +use crate::provider::x_ai::XAiLanguageModelProvider; pub use crate::settings::*; pub fn init(user_store: Entity, client: Arc, cx: &mut App) { @@ -81,5 +82,6 @@ fn register_language_model_providers( VercelLanguageModelProvider::new(client.http_client(), cx), cx, ); + registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); } diff --git a/crates/language_models/src/provider.rs b/crates/language_models/src/provider.rs index 6bc93bd366..c717be7c90 100644 --- a/crates/language_models/src/provider.rs +++ b/crates/language_models/src/provider.rs @@ -10,3 +10,4 @@ pub mod ollama; pub mod open_ai; pub mod open_router; pub mod vercel; +pub mod x_ai; diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 9b7fee228a..518f386ebe 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -166,46 +166,9 @@ impl State { } let response = Self::fetch_models(client, llm_api_token, use_cloud).await?; - cx.update(|cx| { - this.update(cx, |this, cx| { - let mut models = Vec::new(); - - for model in response.models { - models.push(Arc::new(model.clone())); - - // Right now we represent thinking variants of models as separate models on the client, - // so we need to insert variants for any model that supports thinking. - if model.supports_thinking { - models.push(Arc::new(zed_llm_client::LanguageModel { - id: zed_llm_client::LanguageModelId( - format!("{}-thinking", model.id).into(), - ), - display_name: format!("{} Thinking", model.display_name), - ..model - })); - } - } - - this.default_model = models - .iter() - .find(|model| model.id == response.default_model) - .cloned(); - this.default_fast_model = models - .iter() - .find(|model| model.id == response.default_fast_model) - .cloned(); - this.recommended_models = response - .recommended_models - .iter() - .filter_map(|id| models.iter().find(|model| &model.id == id)) - .cloned() - .collect(); - this.models = models; - cx.notify(); - }) - })??; - - anyhow::Ok(()) + this.update(cx, |this, cx| { + this.update_models(response, cx); + }) }) .await .context("failed to fetch Zed models") @@ -216,12 +179,15 @@ impl State { }), _llm_token_subscription: cx.subscribe( &refresh_llm_token_listener, - |this, _listener, _event, cx| { + move |this, _listener, _event, cx| { let client = this.client.clone(); let llm_api_token = this.llm_api_token.clone(); - cx.spawn(async move |_this, _cx| { + cx.spawn(async move |this, cx| { llm_api_token.refresh(&client).await?; - anyhow::Ok(()) + let response = Self::fetch_models(client, llm_api_token, use_cloud).await?; + this.update(cx, |this, cx| { + this.update_models(response, cx); + }) }) .detach_and_log_err(cx); }, @@ -264,6 +230,41 @@ impl State { })); } + fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context) { + let mut models = Vec::new(); + + for model in response.models { + models.push(Arc::new(model.clone())); + + // Right now we represent thinking variants of models as separate models on the client, + // so we need to insert variants for any model that supports thinking. + if model.supports_thinking { + models.push(Arc::new(zed_llm_client::LanguageModel { + id: zed_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()), + display_name: format!("{} Thinking", model.display_name), + ..model + })); + } + } + + self.default_model = models + .iter() + .find(|model| model.id == response.default_model) + .cloned(); + self.default_fast_model = models + .iter() + .find(|model| model.id == response.default_fast_model) + .cloned(); + self.recommended_models = response + .recommended_models + .iter() + .filter_map(|id| models.iter().find(|model| &model.id == id)) + .cloned() + .collect(); + self.models = models; + cx.notify(); + } + async fn fetch_models( client: Arc, llm_api_token: LlmApiToken, diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 5883da1e2f..90d375a6b2 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -376,7 +376,7 @@ impl LanguageModel for OpenRouterLanguageModel { fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { let model_id = self.model.id().trim().to_lowercase(); - if model_id.contains("gemini") { + if model_id.contains("gemini") || model_id.contains("grok-4") { LanguageModelToolSchemaFormat::JsonSchemaSubset } else { LanguageModelToolSchemaFormat::JsonSchema diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs new file mode 100644 index 0000000000..5f6034571b --- /dev/null +++ b/crates/language_models/src/provider/x_ai.rs @@ -0,0 +1,571 @@ +use anyhow::{Context as _, Result, anyhow}; +use collections::BTreeMap; +use credentials_provider::CredentialsProvider; +use futures::{FutureExt, StreamExt, future::BoxFuture}; +use gpui::{AnyView, App, AsyncApp, Context, Entity, Subscription, Task, Window}; +use http_client::HttpClient; +use language_model::{ + AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, + LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, + LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, Role, +}; +use menu; +use open_ai::ResponseStreamEvent; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsStore}; +use std::sync::Arc; +use strum::IntoEnumIterator; +use x_ai::Model; + +use ui::{ElevationIndex, List, Tooltip, prelude::*}; +use ui_input::SingleLineInput; +use util::ResultExt; + +use crate::{AllLanguageModelSettings, ui::InstructionListItem}; + +const PROVIDER_ID: &str = "x_ai"; +const PROVIDER_NAME: &str = "xAI"; + +#[derive(Default, Clone, Debug, PartialEq)] +pub struct XAiSettings { + pub api_url: String, + pub available_models: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct AvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, + pub max_completion_tokens: Option, +} + +pub struct XAiLanguageModelProvider { + http_client: Arc, + state: gpui::Entity, +} + +pub struct State { + api_key: Option, + api_key_from_env: bool, + _subscription: Subscription, +} + +const XAI_API_KEY_VAR: &str = "XAI_API_KEY"; + +impl State { + fn is_authenticated(&self) -> bool { + self.api_key.is_some() + } + + fn reset_api_key(&self, cx: &mut Context) -> Task> { + let credentials_provider = ::global(cx); + let settings = &AllLanguageModelSettings::get_global(cx).x_ai; + let api_url = if settings.api_url.is_empty() { + x_ai::XAI_API_URL.to_string() + } else { + settings.api_url.clone() + }; + cx.spawn(async move |this, cx| { + credentials_provider + .delete_credentials(&api_url, &cx) + .await + .log_err(); + this.update(cx, |this, cx| { + this.api_key = None; + this.api_key_from_env = false; + cx.notify(); + }) + }) + } + + fn set_api_key(&mut self, api_key: String, cx: &mut Context) -> Task> { + let credentials_provider = ::global(cx); + let settings = &AllLanguageModelSettings::get_global(cx).x_ai; + let api_url = if settings.api_url.is_empty() { + x_ai::XAI_API_URL.to_string() + } else { + settings.api_url.clone() + }; + cx.spawn(async move |this, cx| { + credentials_provider + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .await + .log_err(); + this.update(cx, |this, cx| { + this.api_key = Some(api_key); + cx.notify(); + }) + }) + } + + fn authenticate(&self, cx: &mut Context) -> Task> { + if self.is_authenticated() { + return Task::ready(Ok(())); + } + + let credentials_provider = ::global(cx); + let settings = &AllLanguageModelSettings::get_global(cx).x_ai; + let api_url = if settings.api_url.is_empty() { + x_ai::XAI_API_URL.to_string() + } else { + settings.api_url.clone() + }; + cx.spawn(async move |this, cx| { + let (api_key, from_env) = if let Ok(api_key) = std::env::var(XAI_API_KEY_VAR) { + (api_key, true) + } else { + let (_, api_key) = credentials_provider + .read_credentials(&api_url, &cx) + .await? + .ok_or(AuthenticateError::CredentialsNotFound)?; + ( + String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?, + false, + ) + }; + this.update(cx, |this, cx| { + this.api_key = Some(api_key); + this.api_key_from_env = from_env; + cx.notify(); + })?; + + Ok(()) + }) + } +} + +impl XAiLanguageModelProvider { + pub fn new(http_client: Arc, cx: &mut App) -> Self { + let state = cx.new(|cx| State { + api_key: None, + api_key_from_env: false, + _subscription: cx.observe_global::(|_this: &mut State, cx| { + cx.notify(); + }), + }); + + Self { http_client, state } + } + + fn create_language_model(&self, model: x_ai::Model) -> Arc { + Arc::new(XAiLanguageModel { + id: LanguageModelId::from(model.id().to_string()), + model, + state: self.state.clone(), + http_client: self.http_client.clone(), + request_limiter: RateLimiter::new(4), + }) + } +} + +impl LanguageModelProviderState for XAiLanguageModelProvider { + type ObservableEntity = State; + + fn observable_entity(&self) -> Option> { + Some(self.state.clone()) + } +} + +impl LanguageModelProvider for XAiLanguageModelProvider { + fn id(&self) -> LanguageModelProviderId { + LanguageModelProviderId(PROVIDER_ID.into()) + } + + fn name(&self) -> LanguageModelProviderName { + LanguageModelProviderName(PROVIDER_NAME.into()) + } + + fn icon(&self) -> IconName { + IconName::AiXAi + } + + fn default_model(&self, _cx: &App) -> Option> { + Some(self.create_language_model(x_ai::Model::default())) + } + + fn default_fast_model(&self, _cx: &App) -> Option> { + Some(self.create_language_model(x_ai::Model::default_fast())) + } + + fn provided_models(&self, cx: &App) -> Vec> { + let mut models = BTreeMap::default(); + + for model in x_ai::Model::iter() { + if !matches!(model, x_ai::Model::Custom { .. }) { + models.insert(model.id().to_string(), model); + } + } + + for model in &AllLanguageModelSettings::get_global(cx) + .x_ai + .available_models + { + models.insert( + model.name.clone(), + x_ai::Model::Custom { + name: model.name.clone(), + display_name: model.display_name.clone(), + max_tokens: model.max_tokens, + max_output_tokens: model.max_output_tokens, + max_completion_tokens: model.max_completion_tokens, + }, + ); + } + + models + .into_values() + .map(|model| self.create_language_model(model)) + .collect() + } + + fn is_authenticated(&self, cx: &App) -> bool { + self.state.read(cx).is_authenticated() + } + + fn authenticate(&self, cx: &mut App) -> Task> { + self.state.update(cx, |state, cx| state.authenticate(cx)) + } + + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) + .into() + } + + fn reset_credentials(&self, cx: &mut App) -> Task> { + self.state.update(cx, |state, cx| state.reset_api_key(cx)) + } +} + +pub struct XAiLanguageModel { + id: LanguageModelId, + model: x_ai::Model, + state: gpui::Entity, + http_client: Arc, + request_limiter: RateLimiter, +} + +impl XAiLanguageModel { + fn stream_completion( + &self, + request: open_ai::Request, + cx: &AsyncApp, + ) -> BoxFuture<'static, Result>>> + { + let http_client = self.http_client.clone(); + let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| { + let settings = &AllLanguageModelSettings::get_global(cx).x_ai; + let api_url = if settings.api_url.is_empty() { + x_ai::XAI_API_URL.to_string() + } else { + settings.api_url.clone() + }; + (state.api_key.clone(), api_url) + }) else { + return futures::future::ready(Err(anyhow!("App state dropped"))).boxed(); + }; + + let future = self.request_limiter.stream(async move { + let api_key = api_key.context("Missing xAI API Key")?; + let request = + open_ai::stream_completion(http_client.as_ref(), &api_url, &api_key, request); + let response = request.await?; + Ok(response) + }); + + async move { Ok(future.await?.boxed()) }.boxed() + } +} + +impl LanguageModel for XAiLanguageModel { + fn id(&self) -> LanguageModelId { + self.id.clone() + } + + fn name(&self) -> LanguageModelName { + LanguageModelName::from(self.model.display_name().to_string()) + } + + fn provider_id(&self) -> LanguageModelProviderId { + LanguageModelProviderId(PROVIDER_ID.into()) + } + + fn provider_name(&self) -> LanguageModelProviderName { + LanguageModelProviderName(PROVIDER_NAME.into()) + } + + fn supports_tools(&self) -> bool { + self.model.supports_tool() + } + + fn supports_images(&self) -> bool { + self.model.supports_images() + } + + fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { + match choice { + LanguageModelToolChoice::Auto + | LanguageModelToolChoice::Any + | LanguageModelToolChoice::None => true, + } + } + fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { + let model_id = self.model.id().trim().to_lowercase(); + if model_id.eq(x_ai::Model::Grok4.id()) { + LanguageModelToolSchemaFormat::JsonSchemaSubset + } else { + LanguageModelToolSchemaFormat::JsonSchema + } + } + + fn telemetry_id(&self) -> String { + format!("x_ai/{}", self.model.id()) + } + + fn max_token_count(&self) -> u64 { + self.model.max_token_count() + } + + fn max_output_tokens(&self) -> Option { + self.model.max_output_tokens() + } + + fn count_tokens( + &self, + request: LanguageModelRequest, + cx: &App, + ) -> BoxFuture<'static, Result> { + count_xai_tokens(request, self.model.clone(), cx) + } + + fn stream_completion( + &self, + request: LanguageModelRequest, + cx: &AsyncApp, + ) -> BoxFuture< + 'static, + Result< + futures::stream::BoxStream< + 'static, + Result, + >, + LanguageModelCompletionError, + >, + > { + let request = crate::provider::open_ai::into_open_ai( + request, + self.model.id(), + self.model.supports_parallel_tool_calls(), + self.max_output_tokens(), + ); + let completions = self.stream_completion(request, cx); + async move { + let mapper = crate::provider::open_ai::OpenAiEventMapper::new(); + Ok(mapper.map_stream(completions.await?).boxed()) + } + .boxed() + } +} + +pub fn count_xai_tokens( + request: LanguageModelRequest, + model: Model, + cx: &App, +) -> BoxFuture<'static, Result> { + cx.background_spawn(async move { + let messages = request + .messages + .into_iter() + .map(|message| tiktoken_rs::ChatCompletionRequestMessage { + role: match message.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: Some(message.string_contents()), + name: None, + function_call: None, + }) + .collect::>(); + + let model_name = if model.max_token_count() >= 100_000 { + "gpt-4o" + } else { + "gpt-4" + }; + tiktoken_rs::num_tokens_from_messages(model_name, &messages).map(|tokens| tokens as u64) + }) + .boxed() +} + +struct ConfigurationView { + api_key_editor: Entity, + state: gpui::Entity, + load_credentials_task: Option>, +} + +impl ConfigurationView { + fn new(state: gpui::Entity, window: &mut Window, cx: &mut Context) -> Self { + let api_key_editor = cx.new(|cx| { + SingleLineInput::new( + window, + cx, + "xai-0000000000000000000000000000000000000000000000000", + ) + .label("API key") + }); + + cx.observe(&state, |_, _, cx| { + cx.notify(); + }) + .detach(); + + let load_credentials_task = Some(cx.spawn_in(window, { + let state = state.clone(); + async move |this, cx| { + if let Some(task) = state + .update(cx, |state, cx| state.authenticate(cx)) + .log_err() + { + // We don't log an error, because "not signed in" is also an error. + let _ = task.await; + } + this.update(cx, |this, cx| { + this.load_credentials_task = None; + cx.notify(); + }) + .log_err(); + } + })); + + Self { + api_key_editor, + state, + load_credentials_task, + } + } + + fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { + let api_key = self + .api_key_editor + .read(cx) + .editor() + .read(cx) + .text(cx) + .trim() + .to_string(); + + // Don't proceed if no API key is provided and we're not authenticated + if api_key.is_empty() && !self.state.read(cx).is_authenticated() { + return; + } + + let state = self.state.clone(); + cx.spawn_in(window, async move |_, cx| { + state + .update(cx, |state, cx| state.set_api_key(api_key, cx))? + .await + }) + .detach_and_log_err(cx); + + cx.notify(); + } + + fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context) { + self.api_key_editor.update(cx, |input, cx| { + input.editor.update(cx, |editor, cx| { + editor.set_text("", window, cx); + }); + }); + + let state = self.state.clone(); + cx.spawn_in(window, async move |_, cx| { + state.update(cx, |state, cx| state.reset_api_key(cx))?.await + }) + .detach_and_log_err(cx); + + cx.notify(); + } + + fn should_render_editor(&self, cx: &mut Context) -> bool { + !self.state.read(cx).is_authenticated() + } +} + +impl Render for ConfigurationView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let env_var_set = self.state.read(cx).api_key_from_env; + + 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 Zed's agent with xAI, you need to add an API key. Follow these steps:")) + .child( + List::new() + .child(InstructionListItem::new( + "Create one by visiting", + Some("xAI console"), + Some("https://console.x.ai/team/default/api-keys"), + )) + .child(InstructionListItem::text_only( + "Paste your API key below and hit enter to start using the agent", + )), + ) + .child(self.api_key_editor.clone()) + .child( + Label::new(format!( + "You can also assign the {XAI_API_KEY_VAR} environment variable and restart Zed." + )) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Label::new("Note that xAI is a custom OpenAI-compatible provider.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any() + } else { + h_flex() + .mt_1() + .p_1() + .justify_between() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().background) + .child( + h_flex() + .gap_1() + .child(Icon::new(IconName::Check).color(Color::Success)) + .child(Label::new(if env_var_set { + format!("API key set in {XAI_API_KEY_VAR} environment variable.") + } else { + "API key configured.".to_string() + })), + ) + .child( + Button::new("reset-api-key", "Reset API Key") + .label_size(LabelSize::Small) + .icon(IconName::Undo) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .layer(ElevationIndex::ModalSurface) + .when(env_var_set, |this| { + this.tooltip(Tooltip::text(format!("To reset your API key, unset the {XAI_API_KEY_VAR} environment variable."))) + }) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), + ) + .into_any() + }; + + if self.load_credentials_task.is_some() { + div().child(Label::new("Loading credentials…")).into_any() + } else { + v_flex().size_full().child(api_key_section).into_any() + } + } +} diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index f96a2c0a66..dafbb62910 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -17,6 +17,7 @@ use crate::provider::{ open_ai::OpenAiSettings, open_router::OpenRouterSettings, vercel::VercelSettings, + x_ai::XAiSettings, }; /// Initializes the language model settings. @@ -28,33 +29,33 @@ pub fn init(cx: &mut App) { pub struct AllLanguageModelSettings { pub anthropic: AnthropicSettings, pub bedrock: AmazonBedrockSettings, - pub ollama: OllamaSettings, - pub openai: OpenAiSettings, - pub open_router: OpenRouterSettings, - pub zed_dot_dev: ZedDotDevSettings, - pub google: GoogleSettings, - pub vercel: VercelSettings, - - pub lmstudio: LmStudioSettings, pub deepseek: DeepSeekSettings, + pub google: GoogleSettings, + pub lmstudio: LmStudioSettings, pub mistral: MistralSettings, + pub ollama: OllamaSettings, + pub open_router: OpenRouterSettings, + pub openai: OpenAiSettings, + pub vercel: VercelSettings, + pub x_ai: XAiSettings, + pub zed_dot_dev: ZedDotDevSettings, } #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct AllLanguageModelSettingsContent { pub anthropic: Option, pub bedrock: Option, - pub ollama: Option, + pub deepseek: Option, + pub google: Option, pub lmstudio: Option, - pub openai: Option, + pub mistral: Option, + pub ollama: Option, pub open_router: Option, + pub openai: Option, + pub vercel: Option, + pub x_ai: Option, #[serde(rename = "zed.dev")] pub zed_dot_dev: Option, - pub google: Option, - pub deepseek: Option, - pub vercel: Option, - - pub mistral: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] @@ -114,6 +115,12 @@ pub struct GoogleSettingsContent { pub available_models: Option>, } +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct XAiSettingsContent { + pub api_url: Option, + pub available_models: Option>, +} + #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct ZedDotDevSettingsContent { available_models: Option>, @@ -230,6 +237,18 @@ impl settings::Settings for AllLanguageModelSettings { vercel.as_ref().and_then(|s| s.available_models.clone()), ); + // XAI + let x_ai = value.x_ai.clone(); + merge( + &mut settings.x_ai.api_url, + x_ai.as_ref().and_then(|s| s.api_url.clone()), + ); + merge( + &mut settings.x_ai.available_models, + x_ai.as_ref().and_then(|s| s.available_models.clone()), + ); + + // ZedDotDev merge( &mut settings.zed_dot_dev.available_models, value diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8e1026421e..85036eca86 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3362,8 +3362,14 @@ impl Project { cx: &mut Context, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.definitions(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } @@ -3374,8 +3380,14 @@ impl Project { cx: &mut Context, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.declarations(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } @@ -3386,8 +3398,14 @@ impl Project { cx: &mut Context, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.type_definitions(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } @@ -3398,8 +3416,14 @@ impl Project { cx: &mut Context, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.implementations(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } @@ -3410,8 +3434,14 @@ impl Project { cx: &mut Context, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.references(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 66f589bfd3..49eca26838 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -611,7 +611,7 @@ impl RulesLibrary { this.update_in(cx, |this, window, cx| match rule { Ok(rule) => { let title_editor = cx.new(|cx| { - let mut editor = Editor::auto_width(window, cx); + let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Untitled", cx); editor.set_text(rule_metadata.title.unwrap_or_default(), window, cx); if prompt_id.is_built_in() { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c34d892644..7f435c67ac 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -127,7 +127,7 @@ impl BatchedTextRun { cx: &mut App, ) { let pos = Point::new( - (origin.x + self.start_point.column as f32 * dimensions.cell_width).floor(), + origin.x + self.start_point.column as f32 * dimensions.cell_width, origin.y + self.start_point.line as f32 * dimensions.line_height, ); diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs index 40ba7bd5a6..1adfc4b5d8 100644 --- a/crates/theme_selector/src/icon_theme_selector.rs +++ b/crates/theme_selector/src/icon_theme_selector.rs @@ -327,6 +327,7 @@ impl PickerDelegate for IconThemeSelectorDelegate { window.dispatch_action( Box::new(Extensions { category_filter: Some(ExtensionCategoryFilter::IconThemes), + id: None, }), cx, ); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 09d9877df8..022daced7a 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -385,6 +385,7 @@ impl PickerDelegate for ThemeSelectorDelegate { window.dispatch_action( Box::new(Extensions { category_filter: Some(ExtensionCategoryFilter::Themes), + id: None, }), cx, ); diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 47b9fe92fd..e2ae74b52b 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -230,7 +230,11 @@ fn scroll_editor( // column position, or the right-most column in the current // line, seeing as the cursor might be in a short line, in which // case we don't want to go past its last column. - let max_row_column = map.line_len(new_row); + let max_row_column = if new_row <= map.max_point().row() { + map.line_len(new_row) + } else { + 0 + }; let max_column = match min_column + visible_column_count as u32 { max_column if max_column >= max_row_column => max_row_column, max_column => max_column, diff --git a/crates/x_ai/Cargo.toml b/crates/x_ai/Cargo.toml new file mode 100644 index 0000000000..7ca0ca0939 --- /dev/null +++ b/crates/x_ai/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "x_ai" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/x_ai.rs" + +[features] +default = [] +schemars = ["dep:schemars"] + +[dependencies] +anyhow.workspace = true +schemars = { workspace = true, optional = true } +serde.workspace = true +strum.workspace = true +workspace-hack.workspace = true diff --git a/crates/x_ai/LICENSE-GPL b/crates/x_ai/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/x_ai/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs new file mode 100644 index 0000000000..ac116b2f8f --- /dev/null +++ b/crates/x_ai/src/x_ai.rs @@ -0,0 +1,126 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use strum::EnumIter; + +pub const XAI_API_URL: &str = "https://api.x.ai/v1"; + +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] +pub enum Model { + #[serde(rename = "grok-2-vision-latest")] + Grok2Vision, + #[default] + #[serde(rename = "grok-3-latest")] + Grok3, + #[serde(rename = "grok-3-mini-latest")] + Grok3Mini, + #[serde(rename = "grok-3-fast-latest")] + Grok3Fast, + #[serde(rename = "grok-3-mini-fast-latest")] + Grok3MiniFast, + #[serde(rename = "grok-4-latest")] + Grok4, + #[serde(rename = "custom")] + Custom { + name: String, + /// The name displayed in the UI, such as in the assistant panel model dropdown menu. + display_name: Option, + max_tokens: u64, + max_output_tokens: Option, + max_completion_tokens: Option, + }, +} + +impl Model { + pub fn default_fast() -> Self { + Self::Grok3Fast + } + + pub fn from_id(id: &str) -> Result { + match id { + "grok-2-vision" => Ok(Self::Grok2Vision), + "grok-3" => Ok(Self::Grok3), + "grok-3-mini" => Ok(Self::Grok3Mini), + "grok-3-fast" => Ok(Self::Grok3Fast), + "grok-3-mini-fast" => Ok(Self::Grok3MiniFast), + _ => anyhow::bail!("invalid model id '{id}'"), + } + } + + pub fn id(&self) -> &str { + match self { + Self::Grok2Vision => "grok-2-vision", + Self::Grok3 => "grok-3", + Self::Grok3Mini => "grok-3-mini", + Self::Grok3Fast => "grok-3-fast", + Self::Grok3MiniFast => "grok-3-mini-fast", + Self::Grok4 => "grok-4", + Self::Custom { name, .. } => name, + } + } + + pub fn display_name(&self) -> &str { + match self { + Self::Grok2Vision => "Grok 2 Vision", + Self::Grok3 => "Grok 3", + Self::Grok3Mini => "Grok 3 Mini", + Self::Grok3Fast => "Grok 3 Fast", + Self::Grok3MiniFast => "Grok 3 Mini Fast", + Self::Grok4 => "Grok 4", + Self::Custom { + name, display_name, .. + } => display_name.as_ref().unwrap_or(name), + } + } + + pub fn max_token_count(&self) -> u64 { + match self { + Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => 131_072, + Self::Grok4 => 256_000, + Self::Grok2Vision => 8_192, + Self::Custom { max_tokens, .. } => *max_tokens, + } + } + + pub fn max_output_tokens(&self) -> Option { + match self { + Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => Some(8_192), + Self::Grok4 => Some(64_000), + Self::Grok2Vision => Some(4_096), + Self::Custom { + max_output_tokens, .. + } => *max_output_tokens, + } + } + + pub fn supports_parallel_tool_calls(&self) -> bool { + match self { + Self::Grok2Vision + | Self::Grok3 + | Self::Grok3Mini + | Self::Grok3Fast + | Self::Grok3MiniFast + | Self::Grok4 => true, + Model::Custom { .. } => false, + } + } + + pub fn supports_tool(&self) -> bool { + match self { + Self::Grok2Vision + | Self::Grok3 + | Self::Grok3Mini + | Self::Grok3Fast + | Self::Grok3MiniFast + | Self::Grok4 => true, + Model::Custom { .. } => false, + } + } + + pub fn supports_images(&self) -> bool { + match self { + Self::Grok2Vision => true, + _ => false, + } + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 884443e770..0315d0653e 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.195.0" +version = "0.195.2" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL index 38f8e886e1..4de2f126df 100644 --- a/crates/zed/RELEASE_CHANNEL +++ b/crates/zed/RELEASE_CHANNEL @@ -1 +1 @@ -dev +preview \ No newline at end of file diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b5efea10e2..d3727a06dc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -725,6 +725,23 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut return; } + if let Some(extension) = request.extension_id { + cx.spawn(async move |cx| { + let workspace = workspace::get_any_active_workspace(app_state, cx.clone()).await?; + workspace.update(cx, |_, window, cx| { + window.dispatch_action( + Box::new(zed_actions::Extensions { + category_filter: None, + id: Some(extension), + }), + cx, + ); + }) + }) + .detach_and_log_err(cx); + return; + } + if let Some(connection_options) = request.ssh_connection { cx.spawn(async move |mut cx| { let paths: Vec = request.open_paths.into_iter().map(PathBuf::from).collect(); diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 0fb08d1be5..42eb8198a4 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -37,6 +37,7 @@ pub struct OpenRequest { pub join_channel: Option, pub ssh_connection: Option, pub dock_menu_action: Option, + pub extension_id: Option, } impl OpenRequest { @@ -54,6 +55,8 @@ impl OpenRequest { } else if let Some(file) = url.strip_prefix("zed://ssh") { let ssh_url = "ssh:/".to_string() + file; this.parse_ssh_file_path(&ssh_url, cx)? + } else if let Some(file) = url.strip_prefix("zed://extension/") { + this.extension_id = Some(file.to_string()) } else if url.starts_with("ssh://") { this.parse_ssh_file_path(&url, cx)? } else if let Some(request_path) = parse_zed_link(&url, cx) { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index ffe232ad7b..2894a0e52f 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -76,6 +76,9 @@ pub struct Extensions { /// Filters the extensions page down to extensions that are in the specified category. #[serde(default)] pub category_filter: Option, + /// Focuses just the extension with the specified ID. + #[serde(default)] + pub id: Option, } /// Decreases the font size in the editor buffer. diff --git a/docs/src/ai/configuration.md b/docs/src/ai/configuration.md index 907e318d05..0d10aba8f2 100644 --- a/docs/src/ai/configuration.md +++ b/docs/src/ai/configuration.md @@ -23,6 +23,8 @@ Here's an overview of the supported providers and tool call support: | [OpenAI](#openai) | ✅ | | [OpenAI API Compatible](#openai-api-compatible) | 🚫 | | [OpenRouter](#openrouter) | ✅ | +| [Vercel](#vercel-v0) | ✅ | +| [xAI](#xai) | ✅ | ## Use Your Own Keys {#use-your-own-keys} @@ -442,27 +444,30 @@ Custom models will be listed in the model dropdown in the Agent Panel. Zed supports using OpenAI compatible APIs by specifying a custom `endpoint` and `available_models` for the OpenAI provider. -You can add a custom API URL for OpenAI either via the UI or by editing your `settings.json`. -Here are a few model examples you can plug in by using this feature: +Zed supports using OpenAI compatible APIs by specifying a custom `api_url` and `available_models` for the OpenAI provider. This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models. -#### X.ai Grok +To configure a compatible API, you can add a custom API URL for OpenAI either via the UI or by editing your `settings.json`. For example, to connect to [Together AI](https://www.together.ai/): -Example configuration for using X.ai Grok with Zed: +1. Get an API key from your [Together AI account](https://api.together.ai/settings/api-keys). +2. Add the following to your `settings.json`: ```json +{ "language_models": { "openai": { - "api_url": "https://api.x.ai/v1", + "api_url": "https://api.together.xyz/v1", + "api_key": "YOUR_TOGETHER_AI_API_KEY", "available_models": [ { - "name": "grok-beta", - "display_name": "X.ai Grok (Beta)", - "max_tokens": 131072 + "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", + "display_name": "Together Mixtral 8x7B", + "max_tokens": 32768, + "supports_tools": true } - ], - "version": "1" - }, + ] + } } +} ``` ### OpenRouter {#openrouter} @@ -523,7 +528,9 @@ You can find available models and their specifications on the [OpenRouter models Custom models will be listed in the model dropdown in the Agent Panel. -### Vercel v0 +### Vercel v0 {#vercel-v0} + +> ✅ Supports tool use [Vercel v0](https://vercel.com/docs/v0/api) is an expert model for generating full-stack apps, with framework-aware completions optimized for modern stacks like Next.js and Vercel. It supports text and image inputs and provides fast streaming responses. @@ -535,6 +542,49 @@ Once you have it, paste it directly into the Vercel provider section in the pane You should then find it as `v0-1.5-md` in the model dropdown in the Agent Panel. +### xAI {#xai} + +> ✅ Supports tool use + +Zed has first-class support for [xAI](https://x.ai/) models. You can use your own API key to access Grok models. + +1. [Create an API key in the xAI Console](https://console.x.ai/team/default/api-keys) +2. Open the settings view (`agent: open configuration`) and go to the **xAI** section +3. Enter your xAI API key + +The xAI API key will be saved in your keychain. Zed will also use the `XAI_API_KEY` environment variable if it's defined. + +> **Note:** While the xAI API is OpenAI-compatible, Zed has first-class support for it as a dedicated provider. For the best experience, we recommend using the dedicated `x_ai` provider configuration instead of the [OpenAI API Compatible](#openai-api-compatible) method. + +#### Custom Models {#xai-custom-models} + +The Zed agent comes pre-configured with common Grok models. If you wish to use alternate models or customize their parameters, you can do so by adding the following to your Zed `settings.json`: + +```json +{ + "language_models": { + "x_ai": { + "api_url": "https://api.x.ai/v1", + "available_models": [ + { + "name": "grok-1.5", + "display_name": "Grok 1.5", + "max_tokens": 131072, + "max_output_tokens": 8192 + }, + { + "name": "grok-1.5v", + "display_name": "Grok 1.5V (Vision)", + "max_tokens": 131072, + "max_output_tokens": 8192, + "supports_images": true + } + ] + } + } +} +``` + ## Advanced Configuration {#advanced-configuration} ### Custom Provider Endpoints {#custom-provider-endpoint} diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index 9b61d220cf..dc0bdb9b7d 100644 --- a/script/bundle-windows.ps1 +++ b/script/bundle-windows.ps1 @@ -44,8 +44,6 @@ function CheckEnvironmentVariables { } } -$innoDir = "$env:ZED_WORKSPACE\inno" - function PrepareForBundle { if (Test-Path "$innoDir") { Remove-Item -Path "$innoDir" -Recurse -Force @@ -236,6 +234,8 @@ function BuildInstaller { } ParseZedWorkspace +$innoDir = "$env:ZED_WORKSPACE\inno" + CheckEnvironmentVariables PrepareForBundle BuildZedAndItsFriends