Compare commits
32 Commits
v0.206.3-p
...
v0.203.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63cab3ccc8 | ||
|
|
8aa6a94610 | ||
|
|
63860fc5c6 | ||
|
|
996f9cad68 | ||
|
|
21aa3d5cd7 | ||
|
|
c5d36e05f8 | ||
|
|
b63c715a53 | ||
|
|
d287b8bbf7 | ||
|
|
bbbfa10fe1 | ||
|
|
0c1ad95e8c | ||
|
|
29b7863403 | ||
|
|
66fbfd7080 | ||
|
|
92912915fa | ||
|
|
018935de63 | ||
|
|
fbec5e8dd5 | ||
|
|
bda30bb0eb | ||
|
|
aa95dbb670 | ||
|
|
ae4617a47e | ||
|
|
a0756db99b | ||
|
|
80f42ccd26 | ||
|
|
93066f1c52 | ||
|
|
b482eba919 | ||
|
|
377c67be0d | ||
|
|
961f44ea82 | ||
|
|
6f8cfc4908 | ||
|
|
7954420a12 | ||
|
|
9aa31527d8 | ||
|
|
11c1c5a4bb | ||
|
|
439f8bdd72 | ||
|
|
bbaf704599 | ||
|
|
a009bd6915 | ||
|
|
33a97504ca |
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -195,9 +195,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.2.0-alpha.4"
|
||||
version = "0.2.0-alpha.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "603941db1d130ee275840c465b73a2312727d4acef97449550ccf033de71301f"
|
||||
checksum = "6d02292efd75080932b6466471d428c70e2ac06908ae24792fc7c36ecbaf67ca"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -15313,6 +15313,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"indoc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"smol",
|
||||
"sqlformat",
|
||||
@@ -20398,7 +20399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.203.0"
|
||||
version = "0.203.4"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
|
||||
@@ -428,7 +428,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "0.2.0-alpha.4", features = ["unstable"]}
|
||||
agent-client-protocol = { version = "0.2.0-alpha.6", features = ["unstable"]}
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 29 KiB |
@@ -16,6 +16,7 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
|
||||
@@ -785,7 +785,6 @@ pub struct AcpThread {
|
||||
session_id: acp::SessionId,
|
||||
token_usage: Option<TokenUsage>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
available_commands: Vec<acp::AvailableCommand>,
|
||||
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||
determine_shell: Shared<Task<String>>,
|
||||
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
@@ -805,6 +804,7 @@ pub enum AcpThreadEvent {
|
||||
LoadError(LoadError),
|
||||
PromptCapabilitiesUpdated,
|
||||
Refusal,
|
||||
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
@@ -860,7 +860,6 @@ impl AcpThread {
|
||||
action_log: Entity<ActionLog>,
|
||||
session_id: acp::SessionId,
|
||||
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
available_commands: Vec<acp::AvailableCommand>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
||||
@@ -900,7 +899,6 @@ impl AcpThread {
|
||||
session_id,
|
||||
token_usage: None,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
_observe_prompt_capabilities: task,
|
||||
terminals: HashMap::default(),
|
||||
determine_shell,
|
||||
@@ -911,10 +909,6 @@ impl AcpThread {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
pub fn available_commands(&self) -> Vec<acp::AvailableCommand> {
|
||||
self.available_commands.clone()
|
||||
}
|
||||
|
||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||
&self.connection
|
||||
}
|
||||
@@ -1010,6 +1004,9 @@ impl AcpThread {
|
||||
acp::SessionUpdate::Plan(plan) => {
|
||||
self.update_plan(plan, cx);
|
||||
}
|
||||
acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
|
||||
cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1643,13 +1640,13 @@ impl AcpThread {
|
||||
cx.foreground_executor().spawn(send_task)
|
||||
}
|
||||
|
||||
/// Rewinds this thread to before the entry at `index`, removing it and all
|
||||
/// subsequent entries while reverting any changes made from that point.
|
||||
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("not supported")));
|
||||
};
|
||||
let Some(message) = self.user_message(&id) else {
|
||||
/// Restores the git working tree to the state at the given checkpoint (if one exists)
|
||||
pub fn restore_checkpoint(
|
||||
&mut self,
|
||||
id: UserMessageId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some((_, message)) = self.user_message_mut(&id) else {
|
||||
return Task::ready(Err(anyhow!("message not found")));
|
||||
};
|
||||
|
||||
@@ -1657,15 +1654,30 @@ impl AcpThread {
|
||||
.checkpoint
|
||||
.as_ref()
|
||||
.map(|c| c.git_checkpoint.clone());
|
||||
|
||||
let rewind = self.rewind(id.clone(), cx);
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
rewind.await?;
|
||||
if let Some(checkpoint) = checkpoint {
|
||||
git_store
|
||||
.update(cx, |git, cx| git.restore_checkpoint(checkpoint, cx))?
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Rewinds this thread to before the entry at `index`, removing it and all
|
||||
/// subsequent entries while rejecting any action_log changes made from that point.
|
||||
/// Unlike `restore_checkpoint`, this method does not restore from git.
|
||||
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("not supported")));
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some((ix, _)) = this.user_message_mut(&id) {
|
||||
@@ -1673,7 +1685,11 @@ impl AcpThread {
|
||||
this.entries.truncate(ix);
|
||||
cx.emit(AcpThreadEvent::EntriesRemoved(range));
|
||||
}
|
||||
})
|
||||
this.action_log()
|
||||
.update(cx, |action_log, cx| action_log.reject_all_edits(cx))
|
||||
})?
|
||||
.await;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1730,20 +1746,6 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> {
|
||||
self.entries.iter().find_map(|entry| {
|
||||
if let AgentThreadEntry::UserMessage(message) = entry {
|
||||
if message.id.as_ref() == Some(id) {
|
||||
Some(message)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> {
|
||||
self.entries.iter_mut().enumerate().find_map(|(ix, entry)| {
|
||||
if let AgentThreadEntry::UserMessage(message) = entry {
|
||||
@@ -2687,7 +2689,7 @@ mod tests {
|
||||
let AgentThreadEntry::UserMessage(message) = &thread.entries[2] else {
|
||||
panic!("unexpected entries {:?}", thread.entries)
|
||||
};
|
||||
thread.rewind(message.id.clone().unwrap(), cx)
|
||||
thread.restore_checkpoint(message.id.clone().unwrap(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3080,7 +3082,6 @@ mod tests {
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -338,7 +338,6 @@ mod test_support {
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -292,7 +292,6 @@ impl NativeAgent {
|
||||
action_log.clone(),
|
||||
session_id.clone(),
|
||||
prompt_capabilities_rx,
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ use futures::AsyncBufReadExt as _;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use std::{any::Any, cell::RefCell};
|
||||
use std::{path::Path, rc::Rc};
|
||||
@@ -29,6 +30,9 @@ pub struct AcpConnection {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
agent_capabilities: acp::AgentCapabilities,
|
||||
// NB: Don't move this into the wait_task, since we need to ensure the process is
|
||||
// killed on drop (setting kill_on_drop on the command seems to not always work).
|
||||
child: smol::process::Child,
|
||||
_io_task: Task<Result<()>>,
|
||||
_wait_task: Task<Result<()>>,
|
||||
_stderr_task: Task<Result<()>>,
|
||||
@@ -65,7 +69,6 @@ impl AcpConnection {
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
@@ -102,8 +105,9 @@ impl AcpConnection {
|
||||
|
||||
let wait_task = cx.spawn({
|
||||
let sessions = sessions.clone();
|
||||
let status_fut = child.status();
|
||||
async move |cx| {
|
||||
let status = child.status().await?;
|
||||
let status = status_fut.await?;
|
||||
|
||||
for session in sessions.borrow().values() {
|
||||
session
|
||||
@@ -152,6 +156,7 @@ impl AcpConnection {
|
||||
_io_task: io_task,
|
||||
_wait_task: wait_task,
|
||||
_stderr_task: stderr_task,
|
||||
child,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -160,6 +165,13 @@ impl AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AcpConnection {
|
||||
fn drop(&mut self) {
|
||||
// See the comment on the child field.
|
||||
self.child.kill().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
@@ -224,7 +236,6 @@ impl AgentConnection for AcpConnection {
|
||||
session_id.clone(),
|
||||
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
||||
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
|
||||
response.available_commands,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -45,11 +45,20 @@ pub fn init(cx: &mut App) {
|
||||
pub struct AgentServerDelegate {
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_available: Option<watch::Sender<Option<String>>>,
|
||||
}
|
||||
|
||||
impl AgentServerDelegate {
|
||||
pub fn new(project: Entity<Project>, status_tx: Option<watch::Sender<SharedString>>) -> Self {
|
||||
Self { project, status_tx }
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_tx: Option<watch::Sender<Option<String>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
status_tx,
|
||||
new_version_available: new_version_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
@@ -73,6 +82,7 @@ impl AgentServerDelegate {
|
||||
)));
|
||||
};
|
||||
let status_tx = self.status_tx;
|
||||
let new_version_available = self.new_version_available;
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if !ignore_system_version {
|
||||
@@ -101,9 +111,11 @@ impl AgentServerDelegate {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(version) = file_name
|
||||
.to_str()
|
||||
.and_then(|name| semver::Version::from_str(&name).ok())
|
||||
if let Some(name) = file_name.to_str()
|
||||
&& let Some(version) = semver::Version::from_str(name).ok()
|
||||
&& fs
|
||||
.is_file(&dir.join(file_name).join(&entrypoint_path))
|
||||
.await
|
||||
{
|
||||
versions.push((version, file_name.to_owned()));
|
||||
} else {
|
||||
@@ -146,6 +158,7 @@ impl AgentServerDelegate {
|
||||
cx.background_spawn({
|
||||
let file_name = file_name.clone();
|
||||
let dir = dir.clone();
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
let latest_version =
|
||||
node_runtime.npm_package_latest_version(&package_name).await;
|
||||
@@ -160,6 +173,9 @@ impl AgentServerDelegate {
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
if let Some(mut new_version_available) = new_version_available {
|
||||
new_version_available.send(Some(latest_version)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -171,7 +187,7 @@ impl AgentServerDelegate {
|
||||
}
|
||||
let dir = dir.clone();
|
||||
cx.background_spawn(Self::download_latest_version(
|
||||
fs,
|
||||
fs.clone(),
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name,
|
||||
@@ -179,14 +195,18 @@ impl AgentServerDelegate {
|
||||
.await?
|
||||
.into()
|
||||
};
|
||||
|
||||
let agent_server_path = dir.join(version).join(entrypoint_path);
|
||||
let agent_server_path_exists = fs.is_file(&agent_server_path).await;
|
||||
anyhow::ensure!(
|
||||
agent_server_path_exists,
|
||||
"Missing entrypoint path {} after installation",
|
||||
agent_server_path.to_string_lossy()
|
||||
);
|
||||
|
||||
anyhow::Ok(AgentServerCommand {
|
||||
path: node_path,
|
||||
args: vec![
|
||||
dir.join(version)
|
||||
.join(entrypoint_path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
],
|
||||
args: vec![agent_server_path.to_string_lossy().to_string()],
|
||||
env: Default::default(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use settings::SettingsStore;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
@@ -40,7 +39,7 @@ impl ClaudeCode {
|
||||
Self::PACKAGE_NAME.into(),
|
||||
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
|
||||
true,
|
||||
None,
|
||||
Some("0.2.5".parse().unwrap()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
@@ -76,12 +75,20 @@ impl AgentServer for ClaudeCode {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let fs = delegate.project().read(cx).fs().clone();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
let project = delegate.project().clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut project_env = project
|
||||
.update(cx, |project, cx| {
|
||||
project.directory_environment(root_dir.as_path().into(), cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let mut command = if let Some(settings) = settings {
|
||||
settings.command
|
||||
} else {
|
||||
@@ -97,17 +104,20 @@ impl AgentServer for ClaudeCode {
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
project_env.extend(command.env.take().unwrap_or_default());
|
||||
command.env = Some(project_env);
|
||||
|
||||
if let Some(api_key) = cx
|
||||
.update(AnthropicLanguageModelProvider::api_key)?
|
||||
.await
|
||||
.ok()
|
||||
{
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned());
|
||||
|
||||
let root_dir_exists = fs.is_dir(&root_dir).await;
|
||||
anyhow::ensure!(
|
||||
root_dir_exists,
|
||||
"Session root {} does not exist or is not a directory",
|
||||
root_dir.to_string_lossy()
|
||||
);
|
||||
|
||||
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
|
||||
})
|
||||
|
||||
@@ -498,7 +498,7 @@ pub async fn new_test_thread(
|
||||
current_dir: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<AcpThread> {
|
||||
let delegate = AgentServerDelegate::new(project.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(project.clone(), None, None);
|
||||
|
||||
let connection = cx
|
||||
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
|
||||
|
||||
@@ -36,16 +36,24 @@ impl AgentServer for Gemini {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let fs = delegate.project().read(cx).fs().clone();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
});
|
||||
let project = delegate.project().clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let ignore_system_version = settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.ignore_system_version)
|
||||
.unwrap_or(true);
|
||||
let mut project_env = project
|
||||
.update(cx, |project, cx| {
|
||||
project.directory_environment(root_dir.as_path().into(), cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let mut command = if let Some(settings) = settings
|
||||
&& let Some(command) = settings.custom_command()
|
||||
{
|
||||
@@ -66,13 +74,19 @@ impl AgentServer for Gemini {
|
||||
if !command.args.contains(&ACP_ARG.into()) {
|
||||
command.args.push(ACP_ARG.into());
|
||||
}
|
||||
|
||||
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
project_env
|
||||
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
project_env.extend(command.env.take().unwrap_or_default());
|
||||
command.env = Some(project_env);
|
||||
|
||||
let root_dir_exists = fs.is_dir(&root_dir).await;
|
||||
anyhow::ensure!(
|
||||
root_dir_exists,
|
||||
"Session root {} does not exist or is not a directory",
|
||||
root_dir.to_string_lossy()
|
||||
);
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
match &result {
|
||||
@@ -92,7 +106,7 @@ impl AgentServer for Gemini {
|
||||
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
@@ -129,7 +143,7 @@ impl AgentServer for Gemini {
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
|
||||
@@ -207,7 +207,7 @@ impl EntryViewState {
|
||||
self.entries.drain(range);
|
||||
}
|
||||
|
||||
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
|
||||
@@ -700,7 +700,7 @@ impl MessageEditor {
|
||||
self.project.read(cx).fs().clone(),
|
||||
self.history_store.clone(),
|
||||
));
|
||||
let delegate = AgentServerDelegate::new(self.project.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(self.project.clone(), None, None);
|
||||
let connection = server.connect(Path::new(""), delegate, cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
let agent = connection.await?;
|
||||
|
||||
@@ -43,10 +43,10 @@ use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
||||
use task::SpawnInTerminal;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::Anchor;
|
||||
use theme::ThemeSettings;
|
||||
use theme::{AgentFontSize, ThemeSettings};
|
||||
use ui::{
|
||||
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
|
||||
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
|
||||
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
@@ -288,8 +288,9 @@ pub struct AcpThreadView {
|
||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
is_loading_contents: bool,
|
||||
new_server_version_available: Option<SharedString>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
_subscriptions: [Subscription; 4],
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -332,6 +333,11 @@ impl AcpThreadView {
|
||||
|
||||
let placeholder = if agent.name() == "Zed Agent" {
|
||||
format!("Message the {} — @ to include context", agent.name())
|
||||
} else if agent.name() == "Claude Code" || !available_commands.borrow().is_empty() {
|
||||
format!(
|
||||
"Message {} — @ to include context, / for commands",
|
||||
agent.name()
|
||||
)
|
||||
} else {
|
||||
format!("Message {} — @ to include context", agent.name())
|
||||
};
|
||||
@@ -374,7 +380,8 @@ impl AcpThreadView {
|
||||
});
|
||||
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
|
||||
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
|
||||
cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
|
||||
];
|
||||
@@ -411,9 +418,24 @@ impl AcpThreadView {
|
||||
_subscriptions: subscriptions,
|
||||
_cancel_task: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
new_server_version_available: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_state = Self::initial_state(
|
||||
self.agent.clone(),
|
||||
None,
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.available_commands.replace(vec![]);
|
||||
self.new_server_version_available.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn initial_state(
|
||||
agent: Rc<dyn AgentServer>,
|
||||
resume_thread: Option<DbThreadMetadata>,
|
||||
@@ -427,14 +449,32 @@ impl AcpThreadView {
|
||||
"External agents are not yet supported for remote projects.".into(),
|
||||
));
|
||||
}
|
||||
let root_dir = project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
// Pick the first non-single-file worktree for the root directory if there are any,
|
||||
// and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
|
||||
worktrees.sort_by(|l, r| {
|
||||
l.read(cx)
|
||||
.is_single_file()
|
||||
.cmp(&r.read(cx).is_single_file())
|
||||
});
|
||||
let root_dir = worktrees
|
||||
.into_iter()
|
||||
.filter_map(|worktree| {
|
||||
if worktree.read(cx).is_single_file() {
|
||||
Some(worktree.read(cx).abs_path().parent()?.into())
|
||||
} else {
|
||||
Some(worktree.read(cx).abs_path())
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.unwrap_or_else(|| paths::home_dir().as_path().into());
|
||||
let (tx, mut rx) = watch::channel("Loading…".into());
|
||||
let delegate = AgentServerDelegate::new(project.clone(), Some(tx));
|
||||
let (status_tx, mut status_rx) = watch::channel("Loading…".into());
|
||||
let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
|
||||
let delegate = AgentServerDelegate::new(
|
||||
project.clone(),
|
||||
Some(status_tx),
|
||||
Some(new_version_available_tx),
|
||||
);
|
||||
|
||||
let connect_task = agent.connect(&root_dir, delegate, cx);
|
||||
let load_task = cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -497,26 +537,6 @@ impl AcpThreadView {
|
||||
Ok(thread) => {
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
|
||||
let mut available_commands = thread.read(cx).available_commands();
|
||||
|
||||
if connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
available_commands.push(acp::AvailableCommand {
|
||||
name: "login".to_owned(),
|
||||
description: "Authenticate".to_owned(),
|
||||
input: None,
|
||||
});
|
||||
available_commands.push(acp::AvailableCommand {
|
||||
name: "logout".to_owned(),
|
||||
description: "Authenticate".to_owned(),
|
||||
input: None,
|
||||
});
|
||||
}
|
||||
this.available_commands.replace(available_commands);
|
||||
|
||||
this.prompt_capabilities
|
||||
.set(thread.read(cx).prompt_capabilities());
|
||||
|
||||
@@ -609,10 +629,23 @@ impl AcpThreadView {
|
||||
.log_err();
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Ok(new_version) = new_version_available_rx.recv().await {
|
||||
if let Some(new_version) = new_version {
|
||||
this.update(cx, |this, cx| {
|
||||
this.new_server_version_available = Some(new_version.into());
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let loading_view = cx.new(|cx| {
|
||||
let update_title_task = cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
let status = rx.recv().await?;
|
||||
let status = status_rx.recv().await?;
|
||||
this.update(cx, |this: &mut LoadingView, cx| {
|
||||
this.title = status;
|
||||
cx.notify();
|
||||
@@ -648,17 +681,13 @@ impl AcpThreadView {
|
||||
move |_, ev, window, cx| {
|
||||
if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
|
||||
&& &provider_id == updated_provider_id
|
||||
&& LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.provider(&provider_id)
|
||||
.map_or(false, |provider| provider.is_authenticated(cx))
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent.clone(),
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
this.reset(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -884,7 +913,7 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
|
||||
self.regenerate(event.entry_index, editor, window, cx);
|
||||
self.regenerate(event.entry_index, editor.clone(), window, cx);
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
|
||||
self.cancel_editing(&Default::default(), window, cx);
|
||||
@@ -955,7 +984,7 @@ impl AcpThreadView {
|
||||
this,
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
@@ -1108,7 +1137,7 @@ impl AcpThreadView {
|
||||
fn regenerate(
|
||||
&mut self,
|
||||
entry_ix: usize,
|
||||
message_editor: &Entity<MessageEditor>,
|
||||
message_editor: Entity<MessageEditor>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1125,16 +1154,18 @@ impl AcpThreadView {
|
||||
return;
|
||||
};
|
||||
|
||||
let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||
|
||||
let task = cx.spawn(async move |_, cx| {
|
||||
let contents = contents.await?;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
||||
.await?;
|
||||
Ok(contents)
|
||||
});
|
||||
self.send_impl(task, window, cx);
|
||||
let contents =
|
||||
message_editor.update(cx, |message_editor, cx| message_editor.contents(cx))?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.send_impl(contents, window, cx);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -1296,6 +1327,30 @@ impl AcpThreadView {
|
||||
.set(thread.read(cx).prompt_capabilities());
|
||||
}
|
||||
AcpThreadEvent::TokenUsageUpdated => {}
|
||||
AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
|
||||
let mut available_commands = available_commands.clone();
|
||||
|
||||
if thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
available_commands.push(acp::AvailableCommand {
|
||||
name: "login".to_owned(),
|
||||
description: "Authenticate".to_owned(),
|
||||
input: None,
|
||||
});
|
||||
available_commands.push(acp::AvailableCommand {
|
||||
name: "logout".to_owned(),
|
||||
description: "Authenticate".to_owned(),
|
||||
input: None,
|
||||
});
|
||||
}
|
||||
|
||||
self.available_commands.replace(available_commands);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1347,11 +1402,11 @@ impl AcpThreadView {
|
||||
.read(cx)
|
||||
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
|
||||
.unwrap();
|
||||
if !provider.is_authenticated(cx) {
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
if !provider.is_authenticated(cx) {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
@@ -1363,9 +1418,21 @@ impl AcpThreadView {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if method.0.as_ref() == "vertex-ai"
|
||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||
@@ -1409,7 +1476,6 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
self.auth_task =
|
||||
Some(cx.spawn_in(window, {
|
||||
let project = self.project.clone();
|
||||
let agent = self.agent.clone();
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
@@ -1438,14 +1504,7 @@ impl AcpThreadView {
|
||||
}
|
||||
this.handle_thread_error(err, cx);
|
||||
} else {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.reset(window, cx);
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
@@ -1467,7 +1526,7 @@ impl AcpThreadView {
|
||||
let cwd = project.first_project_directory(cx);
|
||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||
|
||||
let delegate = AgentServerDelegate::new(project_entity.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(project_entity.clone(), None, None);
|
||||
let command = ClaudeCode::login_command(delegate, cx);
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
@@ -1569,14 +1628,16 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn rewind(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
|
||||
fn restore_checkpoint(&mut self, message_id: &UserMessageId, cx: &mut Context<Self>) {
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.rewind(message_id.clone(), cx))
|
||||
.update(cx, |thread, cx| {
|
||||
thread.restore_checkpoint(message_id.clone(), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_entry(
|
||||
@@ -1646,8 +1707,9 @@ impl AcpThreadView {
|
||||
.label_size(LabelSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation."))
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.rewind(&message_id, cx);
|
||||
this.restore_checkpoint(&message_id, cx);
|
||||
}))
|
||||
)
|
||||
.child(Divider::horizontal())
|
||||
@@ -1718,7 +1780,7 @@ impl AcpThreadView {
|
||||
let editor = editor.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.regenerate(
|
||||
entry_ix, &editor, window, cx,
|
||||
entry_ix, editor.clone(), window, cx,
|
||||
);
|
||||
}
|
||||
})).into_any_element()
|
||||
@@ -2954,6 +3016,8 @@ impl AcpThreadView {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
v_flex().flex_1().size_full().justify_end().child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
@@ -2984,21 +3048,23 @@ impl AcpThreadView {
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.when(
|
||||
show_description,
|
||||
|el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}. Please choose one of the following options:",
|
||||
self.agent.name()
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(show_description, |el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}.{}",
|
||||
self.agent.name(),
|
||||
if auth_methods.len() > 1 {
|
||||
" Please choose one of the following options:"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
})
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
el.child(
|
||||
h_flex()
|
||||
@@ -3010,12 +3076,12 @@ impl AcpThreadView {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(Label::new("Authenticating…").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
.when(!connection.auth_methods().is_empty(), |this| {
|
||||
.when(!auth_methods.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
@@ -3027,38 +3093,32 @@ impl AcpThreadView {
|
||||
.pt_2()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
})
|
||||
.children(
|
||||
connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, method)| {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
)
|
||||
.when(ix == 0, |el| {
|
||||
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
})
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = this.agent.telemetry_id(),
|
||||
method = method_id
|
||||
);
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
)
|
||||
.when(ix == 0, |el| {
|
||||
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
})
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = this.agent.telemetry_id(),
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
})
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
})
|
||||
}),
|
||||
),
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4681,9 +4741,9 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.entry_view_state.update(cx, |entry_view_state, cx| {
|
||||
entry_view_state.settings_changed(cx);
|
||||
entry_view_state.agent_font_size_changed(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4766,6 +4826,38 @@ impl AcpThreadView {
|
||||
Some(div().child(content))
|
||||
}
|
||||
|
||||
fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
|
||||
v_flex().w_full().justify_end().child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.pr_3()
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Download)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new("New version available").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
Button::new("update-button", format!("Update to v{}", version))
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.reset(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_current_model_name(&self, cx: &App) -> SharedString {
|
||||
// For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
|
||||
// For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
|
||||
@@ -5176,6 +5268,12 @@ impl Render for AcpThreadView {
|
||||
})
|
||||
.children(self.render_thread_retry_status_callout(window, cx))
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
|
||||
}),
|
||||
|this, version| this.child(self.render_new_version_callout(&version, cx)),
|
||||
)
|
||||
.children(
|
||||
if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
|
||||
Some(usage_callout.into_any_element())
|
||||
@@ -5656,7 +5754,6 @@ pub(crate) mod tests {
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
@@ -5716,7 +5813,6 @@ pub(crate) mod tests {
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
Vec::new(),
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
|
||||
@@ -1528,6 +1528,7 @@ impl AgentDiff {
|
||||
| AcpThreadEvent::EntriesRemoved(_)
|
||||
| AcpThreadEvent::ToolAuthorizationRequired
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::AvailableCommandsUpdated(_)
|
||||
| AcpThreadEvent::Retry(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_actions::agent::ReauthenticateAgent;
|
||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::ui::AcpOnboardingModal;
|
||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
@@ -207,6 +207,9 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
|
||||
AcpOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|workspace, _: &OpenClaudeCodeOnboardingModal, window, cx| {
|
||||
ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||
window.refresh();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod acp_onboarding_modal;
|
||||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod claude_code_onboarding_modal;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
mod onboarding_modal;
|
||||
@@ -10,6 +11,7 @@ mod unavailable_editing_tooltip;
|
||||
pub use acp_onboarding_modal::*;
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use claude_code_onboarding_modal::*;
|
||||
pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
||||
@@ -141,20 +141,12 @@ impl Render for AcpOnboardingModal {
|
||||
.bg(gpui::black().opacity(0.15)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(111.),
|
||||
rems_from_px(41.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
),
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(257.),
|
||||
rems_from_px(47.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
|
||||
254
crates/agent_ui/src/ui/claude_code_onboarding_modal.rs
Normal file
254
crates/agent_ui/src/ui/claude_code_onboarding_modal.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use client::zed_urls;
|
||||
use gpui::{
|
||||
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
linear_color_stop, linear_gradient,
|
||||
};
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_panel::{AgentPanel, AgentType};
|
||||
|
||||
macro_rules! claude_code_onboarding_event {
|
||||
($name:expr) => {
|
||||
telemetry::event!($name, source = "ACP Claude Code Onboarding");
|
||||
};
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||
telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+);
|
||||
};
|
||||
}
|
||||
|
||||
pub struct ClaudeCodeOnboardingModal {
|
||||
focus_handle: FocusHandle,
|
||||
workspace: Entity<Workspace>,
|
||||
}
|
||||
|
||||
impl ClaudeCodeOnboardingModal {
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let workspace_entity = cx.entity();
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
workspace: workspace_entity,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(AgentType::ClaudeCode, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
claude_code_onboarding_event!("Open Panel Clicked");
|
||||
}
|
||||
|
||||
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url(&zed_urls::external_agents_docs(cx));
|
||||
cx.notify();
|
||||
|
||||
claude_code_onboarding_event!("Documentation Link Clicked");
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ClaudeCodeOnboardingModal {}
|
||||
|
||||
impl Focusable for ClaudeCodeOnboardingModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ClaudeCodeOnboardingModal {}
|
||||
|
||||
impl Render for ClaudeCodeOnboardingModal {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let illustration_element = |icon: IconName, label: Option<SharedString>, opacity: f32| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.05))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_dashed()
|
||||
.child(
|
||||
Icon::new(icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(label_text) = label {
|
||||
this.child(
|
||||
Label::new(label_text)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div().w_16().h_1().rounded_full().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_active
|
||||
.opacity(0.6)),
|
||||
)
|
||||
}
|
||||
})
|
||||
.opacity(opacity)
|
||||
};
|
||||
|
||||
let illustration = h_flex()
|
||||
.relative()
|
||||
.h(rems_from_px(126.))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_center()
|
||||
.gap_8()
|
||||
.rounded_t_md()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
|
||||
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
|
||||
),
|
||||
)
|
||||
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.1),
|
||||
0.9,
|
||||
),
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.),
|
||||
0.,
|
||||
),
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(gpui::black().opacity(0.15)),
|
||||
)
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(257.),
|
||||
rems_from_px(47.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(illustration_element(IconName::Stop, None, 0.15))
|
||||
.child(illustration_element(
|
||||
IconName::AiGemini,
|
||||
Some("New Gemini CLI Thread".into()),
|
||||
0.3,
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::AiClaude)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("New Claude Code Thread").size(LabelSize::Small)),
|
||||
)
|
||||
.child(illustration_element(
|
||||
IconName::Stop,
|
||||
Some("Your Agent Here".into()),
|
||||
0.3,
|
||||
))
|
||||
.child(illustration_element(IconName::Stop, None, 0.15)),
|
||||
);
|
||||
|
||||
let heading = v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Beta Release")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Claude Code: Natively in Zed").size(HeadlineSize::Large));
|
||||
|
||||
let copy = "Powered by the Agent Client Protocol, you can now run Claude Code as\na first-class citizen in Zed's agent panel.";
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Start with Claude Code")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let docs_button = Button::new("add-other-agents", "Add Other Agents")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::view_docs));
|
||||
|
||||
let close_button = h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "X click");
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.id("acp-onboarding")
|
||||
.key_context("AcpOnboardingModal")
|
||||
.relative()
|
||||
.w(rems(34.))
|
||||
.h_full()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.child(heading)
|
||||
.child(Label::new(copy).color(Color::Muted))
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.child(open_panel_button)
|
||||
.child(docs_button),
|
||||
),
|
||||
)
|
||||
.child(close_button)
|
||||
}
|
||||
}
|
||||
@@ -86,10 +86,16 @@ impl RenderOnce for AiUpsellCard {
|
||||
)
|
||||
.child(plan_definitions.free_plan());
|
||||
|
||||
let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
|
||||
Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
|
||||
.color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
|
||||
);
|
||||
let grid_bg = h_flex()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.w_full()
|
||||
.h(px(240.))
|
||||
.bg(gpui::pattern_slash(
|
||||
cx.theme().colors().border.opacity(0.1),
|
||||
2.,
|
||||
25.,
|
||||
));
|
||||
|
||||
let gradient_bg = div()
|
||||
.absolute()
|
||||
|
||||
@@ -28,8 +28,8 @@ pub enum StackFrameListEvent {
|
||||
}
|
||||
|
||||
/// Represents the filter applied to the stack frame list
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
enum StackFrameFilter {
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub(crate) enum StackFrameFilter {
|
||||
/// Show all frames
|
||||
All,
|
||||
/// Show only frames from the user's code
|
||||
@@ -174,19 +174,29 @@ impl StackFrameList {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
|
||||
self.stack_frames(cx)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(ix, _)| {
|
||||
self.list_filter == StackFrameFilter::All
|
||||
|| self
|
||||
.filter_entries_indices
|
||||
.binary_search_by_key(&ix, |ix| ix)
|
||||
.is_ok()
|
||||
})
|
||||
.map(|(_, stack_frame)| stack_frame.dap)
|
||||
.collect()
|
||||
match self.list_filter {
|
||||
StackFrameFilter::All => self
|
||||
.stack_frames(cx)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|stack_frame| stack_frame.dap)
|
||||
.collect(),
|
||||
StackFrameFilter::OnlyUserFrames => self
|
||||
.filter_entries_indices
|
||||
.iter()
|
||||
.map(|ix| match &self.entries[*ix] {
|
||||
StackFrameEntry::Label(label) => label,
|
||||
StackFrameEntry::Collapsed(_) => panic!("Collapsed tabs should not be visible"),
|
||||
StackFrameEntry::Normal(frame) => frame,
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn list_filter(&self) -> StackFrameFilter {
|
||||
self.list_filter
|
||||
}
|
||||
|
||||
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
@@ -246,6 +256,7 @@ impl StackFrameList {
|
||||
self.entries.clear();
|
||||
self.selected_ix = None;
|
||||
self.list_state.reset(0);
|
||||
self.filter_entries_indices.clear();
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
return;
|
||||
@@ -263,7 +274,7 @@ impl StackFrameList {
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut filter_entries_indices = Vec::default();
|
||||
for (ix, stack_frame) in stack_frames.iter().enumerate() {
|
||||
for stack_frame in stack_frames.iter() {
|
||||
let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
|
||||
source.path.as_ref().is_some_and(|path| {
|
||||
worktree_prefixes
|
||||
@@ -273,10 +284,6 @@ impl StackFrameList {
|
||||
})
|
||||
});
|
||||
|
||||
if frame_in_visible_worktree {
|
||||
filter_entries_indices.push(ix);
|
||||
}
|
||||
|
||||
match stack_frame.dap.presentation_hint {
|
||||
Some(dap::StackFramePresentationHint::Deemphasize)
|
||||
| Some(dap::StackFramePresentationHint::Subtle) => {
|
||||
@@ -302,6 +309,9 @@ impl StackFrameList {
|
||||
first_stack_frame_with_path.get_or_insert(entries.len());
|
||||
}
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
if frame_in_visible_worktree {
|
||||
filter_entries_indices.push(entries.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +319,6 @@ impl StackFrameList {
|
||||
let collapsed_entries = std::mem::take(&mut collapsed_entries);
|
||||
if !collapsed_entries.is_empty() {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries));
|
||||
self.filter_entries_indices.push(entries.len() - 1);
|
||||
}
|
||||
self.entries = entries;
|
||||
self.filter_entries_indices = filter_entries_indices;
|
||||
@@ -612,7 +621,16 @@ impl StackFrameList {
|
||||
let entries = std::mem::take(stack_frames)
|
||||
.into_iter()
|
||||
.map(StackFrameEntry::Normal);
|
||||
// HERE
|
||||
let entries_len = entries.len();
|
||||
self.entries.splice(ix..ix + 1, entries);
|
||||
let (Ok(filtered_indices_start) | Err(filtered_indices_start)) =
|
||||
self.filter_entries_indices.binary_search(&ix);
|
||||
|
||||
for idx in &mut self.filter_entries_indices[filtered_indices_start..] {
|
||||
*idx += entries_len - 1;
|
||||
}
|
||||
|
||||
self.selected_ix = Some(ix);
|
||||
self.list_state.reset(self.entries.len());
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
session::running::stack_frame_list::StackFrameEntry,
|
||||
session::running::stack_frame_list::{StackFrameEntry, StackFrameFilter},
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
use dap::{
|
||||
@@ -867,6 +867,28 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
},
|
||||
StackFrame {
|
||||
id: 4,
|
||||
name: "node:internal/modules/run_main2".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("run_main.js".into()),
|
||||
path: Some(path!("/usr/lib/node/internal/modules/run_main2.js").into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 50,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
|
||||
},
|
||||
StackFrame {
|
||||
id: 5,
|
||||
name: "doSomething".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("test.js".into()),
|
||||
@@ -957,83 +979,119 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
|
||||
let stack_frame_list = debug_panel_item
|
||||
.running_state()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
let stack_frame_list =
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
|
||||
let stack_frame_list = debug_panel_item
|
||||
.running_state()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.build_entries(true, window, cx);
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.build_entries(true, window, cx);
|
||||
|
||||
// Verify we have the expected collapsed structure
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Collapsed(vec![
|
||||
stack_frames_for_assertions[1].clone(),
|
||||
stack_frames_for_assertions[2].clone()
|
||||
]),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
]
|
||||
);
|
||||
// Verify we have the expected collapsed structure
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Collapsed(vec![
|
||||
stack_frames_for_assertions[1].clone(),
|
||||
stack_frames_for_assertions[2].clone(),
|
||||
stack_frames_for_assertions[3].clone()
|
||||
]),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Test 1: Verify filtering works
|
||||
let all_frames = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially");
|
||||
|
||||
// Toggle to user frames only
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
let user_frames = stack_frame_list.dap_stack_frames(cx);
|
||||
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
|
||||
assert_eq!(user_frames[0].name, "main");
|
||||
assert_eq!(user_frames[1].name, "doSomething");
|
||||
|
||||
// Test 2: Verify filtering toggles correctly
|
||||
// Check we can toggle back and see all frames again
|
||||
|
||||
// Toggle back to all frames
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
let all_frames_again = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(
|
||||
all_frames_again.len(),
|
||||
4,
|
||||
"Should see all 4 frames after toggling back"
|
||||
);
|
||||
|
||||
// Test 3: Verify collapsed entries stay expanded
|
||||
stack_frame_list.expand_collapsed_entry(1, cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
]
|
||||
);
|
||||
|
||||
// Toggle filter twice
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
// Verify entries remain expanded
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
],
|
||||
"Expanded entries should remain expanded after toggling filter"
|
||||
);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
let all_frames = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(all_frames.len(), 5, "Should see all 5 frames initially");
|
||||
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.list_filter(),
|
||||
StackFrameFilter::OnlyUserFrames
|
||||
);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
let user_frames = stack_frame_list.dap_stack_frames(cx);
|
||||
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
|
||||
assert_eq!(user_frames[0].name, "main");
|
||||
assert_eq!(user_frames[1].name, "doSomething");
|
||||
|
||||
// Toggle back to all frames
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
let all_frames_again = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(
|
||||
all_frames_again.len(),
|
||||
5,
|
||||
"Should see all 5 frames after toggling back"
|
||||
);
|
||||
|
||||
// Test 3: Verify collapsed entries stay expanded
|
||||
stack_frame_list.expand_collapsed_entry(1, cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
|
||||
]
|
||||
);
|
||||
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.list_filter(),
|
||||
StackFrameFilter::OnlyUserFrames
|
||||
);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.list_filter(),
|
||||
StackFrameFilter::OnlyUserFrames
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stack_frame_list.dap_stack_frames(cx).as_slice(),
|
||||
&[
|
||||
stack_frames_for_assertions[0].clone(),
|
||||
stack_frames_for_assertions[4].clone()
|
||||
]
|
||||
);
|
||||
|
||||
// Verify entries remain expanded
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
|
||||
],
|
||||
"Expanded entries should remain expanded after toggling filter"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3270,6 +3270,10 @@ impl EditorElement {
|
||||
if rows.start >= rows.end {
|
||||
return Vec::new();
|
||||
}
|
||||
if !base_background.is_opaque() {
|
||||
// We don't actually know what color is behind this editor.
|
||||
return Vec::new();
|
||||
}
|
||||
let highlight_iter = highlight_ranges.iter().cloned();
|
||||
let selection_iter = selections.iter().flat_map(|(player_color, layouts)| {
|
||||
let color = player_color.selection;
|
||||
@@ -10974,7 +10978,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_merge_overlapping_ranges() {
|
||||
let base_bg = Hsla::default();
|
||||
let base_bg = Hsla::white();
|
||||
let color1 = Hsla {
|
||||
h: 0.0,
|
||||
s: 0.5,
|
||||
@@ -11044,7 +11048,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_bg_segments_per_row() {
|
||||
let base_bg = Hsla::default();
|
||||
let base_bg = Hsla::white();
|
||||
|
||||
// Case A: selection spans three display rows: row 1 [5, end), full row 2, row 3 [0, 7)
|
||||
{
|
||||
|
||||
@@ -40,8 +40,7 @@ use gpui::{
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use multi_buffer::ExcerptInfo;
|
||||
@@ -1860,13 +1859,17 @@ impl GitPanel {
|
||||
|
||||
/// Generates a commit message using an LLM.
|
||||
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
|
||||
if !self.can_commit() || DisableAiSettings::get_global(cx).disable_ai {
|
||||
if !self.can_commit()
|
||||
|| DisableAiSettings::get_global(cx).disable_ai
|
||||
|| !agent_settings::AgentSettings::get_global(cx).enabled
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let model = match current_language_model(cx) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
let Some(ConfiguredModel { provider, model }) =
|
||||
LanguageModelRegistry::read_global(cx).commit_message_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(repo) = self.active_repository.as_ref() else {
|
||||
@@ -1891,6 +1894,16 @@ impl GitPanel {
|
||||
this.generate_commit_message_task.take();
|
||||
});
|
||||
|
||||
if let Some(task) = cx.update(|cx| {
|
||||
if !provider.is_authenticated(cx) {
|
||||
Some(provider.authenticate(cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})? {
|
||||
task.await.log_err();
|
||||
};
|
||||
|
||||
let mut diff_text = match diff.await {
|
||||
Ok(result) => match result {
|
||||
Ok(text) => text,
|
||||
@@ -3080,9 +3093,18 @@ impl GitPanel {
|
||||
&self,
|
||||
cx: &Context<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
current_language_model(cx).is_some().then(|| {
|
||||
if self.generate_commit_message_task.is_some() {
|
||||
return h_flex()
|
||||
if !agent_settings::AgentSettings::get_global(cx).enabled
|
||||
|| DisableAiSettings::get_global(cx).disable_ai
|
||||
|| LanguageModelRegistry::read_global(cx)
|
||||
.commit_message_model()
|
||||
.is_none()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.generate_commit_message_task.is_some() {
|
||||
return Some(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
@@ -3095,11 +3117,13 @@ impl GitPanel {
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
|
||||
let can_commit = self.can_commit();
|
||||
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
||||
let can_commit = self.can_commit();
|
||||
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
||||
Some(
|
||||
IconButton::new("generate-commit-message", IconName::AiEdit)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -3120,8 +3144,8 @@ impl GitPanel {
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.generate_commit_message(cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_co_authors(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||
@@ -4453,20 +4477,6 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
|
||||
let is_enabled = agent_settings::AgentSettings::get_global(cx).enabled
|
||||
&& !DisableAiSettings::get_global(cx).disable_ai;
|
||||
|
||||
is_enabled
|
||||
.then(|| {
|
||||
let ConfiguredModel { provider, model } =
|
||||
LanguageModelRegistry::read_global(cx).commit_message_model()?;
|
||||
|
||||
provider.is_authenticated(cx).then(|| model)
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
impl Render for GitPanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let project = self.project.read(cx);
|
||||
|
||||
@@ -473,6 +473,11 @@ impl Hsla {
|
||||
self.a == 0.0
|
||||
}
|
||||
|
||||
/// Returns true if the HSLA color is fully opaque, false otherwise.
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
self.a == 1.0
|
||||
}
|
||||
|
||||
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
|
||||
///
|
||||
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
|
||||
|
||||
@@ -325,7 +325,7 @@ impl LspLogView {
|
||||
let server_info = format!(
|
||||
"* Server: {NAME} (id {ID})
|
||||
|
||||
* Binary: {BINARY:#?}
|
||||
* Binary: {BINARY}
|
||||
|
||||
* Registered workspace folders:
|
||||
{WORKSPACE_FOLDERS}
|
||||
@@ -335,10 +335,10 @@ impl LspLogView {
|
||||
* Configuration: {CONFIGURATION}",
|
||||
NAME = info.name,
|
||||
ID = info.id,
|
||||
BINARY = info.binary.as_ref().map_or_else(
|
||||
|| "Unknown".to_string(),
|
||||
|bin| bin.path.as_path().to_string_lossy().to_string()
|
||||
),
|
||||
BINARY = info
|
||||
.binary
|
||||
.as_ref()
|
||||
.map_or_else(|| "Unknown".to_string(), |binary| format!("{binary:#?}")),
|
||||
WORKSPACE_FOLDERS = info.workspace_folders.join(", "),
|
||||
CAPABILITIES = serde_json::to_string_pretty(&info.capabilities)
|
||||
.unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
|
||||
@@ -990,10 +990,16 @@ impl Render for LspLogToolbarItemView {
|
||||
let server_id = server.server_id;
|
||||
let rpc_trace_enabled = server.rpc_trace_enabled;
|
||||
let log_view = log_view.clone();
|
||||
let label = match server.selected_entry {
|
||||
LogKind::Rpc => RPC_MESSAGES,
|
||||
LogKind::Trace => SERVER_TRACE,
|
||||
LogKind::Logs => SERVER_LOGS,
|
||||
LogKind::ServerInfo => SERVER_INFO,
|
||||
};
|
||||
PopoverMenu::new("LspViewSelector")
|
||||
.anchor(Corner::TopLeft)
|
||||
.trigger(
|
||||
Button::new("language_server_menu_header", server.selected_entry.label())
|
||||
Button::new("language_server_menu_header", label)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
|
||||
@@ -68,6 +68,12 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
|
||||
MODE_NAMES[mode as usize].clone(),
|
||||
move |_, _, cx| {
|
||||
write_mode_change(mode, cx);
|
||||
|
||||
telemetry::event!(
|
||||
"Welcome Theme mode Changed",
|
||||
from = theme_mode,
|
||||
to = mode
|
||||
);
|
||||
},
|
||||
)
|
||||
}),
|
||||
@@ -105,7 +111,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
|
||||
ThemeMode::Dark => Appearance::Dark,
|
||||
ThemeMode::System => *system_appearance,
|
||||
};
|
||||
let current_theme_name = theme_selection.theme(appearance);
|
||||
let current_theme_name = SharedString::new(theme_selection.theme(appearance));
|
||||
|
||||
let theme_names = match appearance {
|
||||
Appearance::Light => LIGHT_THEMES,
|
||||
@@ -149,8 +155,15 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
|
||||
})
|
||||
.on_click({
|
||||
let theme_name = theme.name.clone();
|
||||
let current_theme_name = current_theme_name.clone();
|
||||
|
||||
move |_, _, cx| {
|
||||
write_theme_change(theme_name.clone(), theme_mode, cx);
|
||||
telemetry::event!(
|
||||
"Welcome Theme Changed",
|
||||
from = current_theme_name,
|
||||
to = theme_name
|
||||
);
|
||||
}
|
||||
})
|
||||
.map(|this| {
|
||||
@@ -239,6 +252,17 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
|
||||
cx,
|
||||
move |setting, _| setting.metrics = Some(enabled),
|
||||
);
|
||||
|
||||
// This telemetry event shouldn't fire when it's off. If it does we're be alerted
|
||||
// and can fix it in a timely manner to respect a user's choice.
|
||||
telemetry::event!("Welcome Page Telemetry Metrics Toggled",
|
||||
options = if enabled {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
);
|
||||
|
||||
}},
|
||||
).tab_index({
|
||||
*tab_index += 1;
|
||||
@@ -267,6 +291,16 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
|
||||
cx,
|
||||
move |setting, _| setting.diagnostics = Some(enabled),
|
||||
);
|
||||
|
||||
// This telemetry event shouldn't fire when it's off. If it does we're be alerted
|
||||
// and can fix it in a timely manner to respect a user's choice.
|
||||
telemetry::event!("Welcome Page Telemetry Diagnostics Toggled",
|
||||
options = if enabled {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
).tab_index({
|
||||
@@ -327,6 +361,8 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE
|
||||
update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
|
||||
setting.base_keymap = Some(keymap_base);
|
||||
});
|
||||
|
||||
telemetry::event!("Welcome Keymap Changed", keymap = keymap_base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,13 +380,21 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
|
||||
{
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
move |&selection, _, cx| {
|
||||
update_settings_file::<VimModeSetting>(fs.clone(), cx, move |setting, _| {
|
||||
*setting = match selection {
|
||||
ToggleState::Selected => Some(true),
|
||||
ToggleState::Unselected => Some(false),
|
||||
ToggleState::Indeterminate => None,
|
||||
let vim_mode = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected => false,
|
||||
ToggleState::Indeterminate => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
update_settings_file::<VimModeSetting>(fs.clone(), cx, move |setting, _| {
|
||||
*setting = Some(vim_mode);
|
||||
});
|
||||
|
||||
telemetry::event!(
|
||||
"Welcome Vim Mode Toggled",
|
||||
options = if vim_mode { "on" } else { "off" },
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -449,28 +449,28 @@ impl FontPickerDelegate {
|
||||
) -> Self {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
|
||||
let fonts: Vec<SharedString> = font_family_cache
|
||||
.list_font_families(cx)
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let fonts = font_family_cache
|
||||
.try_list_font_families()
|
||||
.unwrap_or_else(|| vec![current_font.clone()]);
|
||||
let selected_index = fonts
|
||||
.iter()
|
||||
.position(|font| *font == current_font)
|
||||
.unwrap_or(0);
|
||||
|
||||
let filtered_fonts = fonts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, font)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: font.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
fonts: fonts.clone(),
|
||||
filtered_fonts: fonts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, font)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: font.to_string(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect(),
|
||||
fonts,
|
||||
filtered_fonts,
|
||||
selected_index,
|
||||
current_font,
|
||||
on_font_changed: Arc::new(on_font_changed),
|
||||
|
||||
@@ -242,12 +242,25 @@ struct Onboarding {
|
||||
|
||||
impl Onboarding {
|
||||
fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
workspace: workspace.weak_handle(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
selected_page: SelectedPage::Basics,
|
||||
user_store: workspace.user_store().clone(),
|
||||
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
let font_family_cache = theme::FontFamilyCache::global(cx);
|
||||
|
||||
cx.new(|cx| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
font_family_cache.prefetch(cx).await;
|
||||
this.update(cx, |_, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
workspace: workspace.weak_handle(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
selected_page: SelectedPage::Basics,
|
||||
user_store: workspace.user_store().clone(),
|
||||
_settings_subscription: cx
|
||||
.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -476,6 +489,7 @@ impl Onboarding {
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(|_, window, cx| {
|
||||
telemetry::event!("Welcome Sign In Clicked");
|
||||
window.dispatch_action(SignIn.boxed_clone(), cx);
|
||||
})
|
||||
.into_any_element()
|
||||
|
||||
@@ -16,11 +16,6 @@ const SEND_LINE: &str = "\n// Send:";
|
||||
const RECEIVE_LINE: &str = "\n// Receive:";
|
||||
const MAX_STORED_LOG_ENTRIES: usize = 2000;
|
||||
|
||||
const RPC_MESSAGES: &str = "RPC Messages";
|
||||
const SERVER_LOGS: &str = "Server Logs";
|
||||
const SERVER_TRACE: &str = "Server Trace";
|
||||
const SERVER_INFO: &str = "Server Info";
|
||||
|
||||
pub fn init(on_headless_host: bool, cx: &mut App) -> Entity<LogStore> {
|
||||
let log_store = cx.new(|cx| LogStore::new(on_headless_host, cx));
|
||||
cx.set_global(GlobalLogStore(log_store.clone()));
|
||||
@@ -216,15 +211,6 @@ impl LogKind {
|
||||
LanguageServerLogType::Rpc { .. } => Self::Rpc,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
LogKind::Rpc => RPC_MESSAGES,
|
||||
LogKind::Trace => SERVER_TRACE,
|
||||
LogKind::Logs => SERVER_LOGS,
|
||||
LogKind::ServerInfo => SERVER_INFO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogStore {
|
||||
|
||||
@@ -49,14 +49,6 @@ impl Project {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Terminal>>> {
|
||||
let is_via_remote = self.remote_client.is_some();
|
||||
let project_path_context = self
|
||||
.active_entry()
|
||||
.and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
|
||||
.or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
|
||||
.map(|worktree_id| ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("")),
|
||||
});
|
||||
|
||||
let path: Option<Arc<Path>> = if let Some(cwd) = &spawn_task.cwd {
|
||||
if is_via_remote {
|
||||
@@ -124,23 +116,42 @@ impl Project {
|
||||
},
|
||||
};
|
||||
|
||||
let toolchain = project_path_context
|
||||
let project_path_contexts = self
|
||||
.active_entry()
|
||||
.and_then(|entry_id| self.path_for_entry(entry_id, cx))
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.visible_worktrees(cx)
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.map(|worktree_id| ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("")),
|
||||
}),
|
||||
);
|
||||
let toolchains = project_path_contexts
|
||||
.filter(|_| detect_venv)
|
||||
.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
|
||||
.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx))
|
||||
.collect::<Vec<_>>();
|
||||
let lang_registry = self.languages.clone();
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(async move |project, cx| {
|
||||
let activation_script = maybe!(async {
|
||||
let toolchain = toolchain?.await?;
|
||||
Some(
|
||||
lang_registry
|
||||
for toolchain in toolchains {
|
||||
let Some(toolchain) = toolchain.await else {
|
||||
continue;
|
||||
};
|
||||
let language = lang_registry
|
||||
.language_for_name(&toolchain.language_name.0)
|
||||
.await
|
||||
.ok()?
|
||||
.toolchain_lister()?
|
||||
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
|
||||
.await,
|
||||
)
|
||||
.ok();
|
||||
let lister = language?.toolchain_lister();
|
||||
return Some(
|
||||
lister?
|
||||
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
|
||||
.await,
|
||||
);
|
||||
}
|
||||
None
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
@@ -268,14 +279,6 @@ impl Project {
|
||||
cwd: Option<PathBuf>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Terminal>>> {
|
||||
let project_path_context = self
|
||||
.active_entry()
|
||||
.and_then(|entry_id| self.worktree_id_for_entry(entry_id, cx))
|
||||
.or_else(|| self.visible_worktrees(cx).next().map(|wt| wt.read(cx).id()))
|
||||
.map(|worktree_id| ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("")),
|
||||
});
|
||||
let path = cwd.map(|p| Arc::from(&*p));
|
||||
let is_via_remote = self.remote_client.is_some();
|
||||
|
||||
@@ -303,9 +306,22 @@ impl Project {
|
||||
|
||||
let local_path = if is_via_remote { None } else { path.clone() };
|
||||
|
||||
let toolchain = project_path_context
|
||||
let project_path_contexts = self
|
||||
.active_entry()
|
||||
.and_then(|entry_id| self.path_for_entry(entry_id, cx))
|
||||
.into_iter()
|
||||
.chain(
|
||||
self.visible_worktrees(cx)
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.map(|worktree_id| ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new("")),
|
||||
}),
|
||||
);
|
||||
let toolchains = project_path_contexts
|
||||
.filter(|_| detect_venv)
|
||||
.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx));
|
||||
.map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx))
|
||||
.collect::<Vec<_>>();
|
||||
let remote_client = self.remote_client.clone();
|
||||
let shell = match &remote_client {
|
||||
Some(remote_client) => remote_client
|
||||
@@ -327,17 +343,22 @@ impl Project {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(async move |project, cx| {
|
||||
let activation_script = maybe!(async {
|
||||
let toolchain = toolchain?.await?;
|
||||
let language = lang_registry
|
||||
.language_for_name(&toolchain.language_name.0)
|
||||
.await
|
||||
.ok();
|
||||
let lister = language?.toolchain_lister();
|
||||
Some(
|
||||
lister?
|
||||
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
|
||||
.await,
|
||||
)
|
||||
for toolchain in toolchains {
|
||||
let Some(toolchain) = toolchain.await else {
|
||||
continue;
|
||||
};
|
||||
let language = lang_registry
|
||||
.language_for_name(&toolchain.language_name.0)
|
||||
.await
|
||||
.ok();
|
||||
let lister = language?.toolchain_lister();
|
||||
return Some(
|
||||
lister?
|
||||
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
|
||||
.await,
|
||||
);
|
||||
}
|
||||
None
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -254,7 +254,7 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
|
||||
let ssh_proxy_process = match self
|
||||
.socket
|
||||
.ssh_command("sh", &["-lc", &start_proxy_command])
|
||||
.ssh_command("sh", &["-c", &start_proxy_command])
|
||||
// IMPORTANT: we kill this process when we drop the task that uses it.
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
@@ -529,7 +529,7 @@ impl SshRemoteConnection {
|
||||
.run_command(
|
||||
"sh",
|
||||
&[
|
||||
"-lc",
|
||||
"-c",
|
||||
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
||||
],
|
||||
)
|
||||
@@ -607,7 +607,7 @@ impl SshRemoteConnection {
|
||||
.run_command(
|
||||
"sh",
|
||||
&[
|
||||
"-lc",
|
||||
"-c",
|
||||
&shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()),
|
||||
],
|
||||
)
|
||||
@@ -655,7 +655,7 @@ impl SshRemoteConnection {
|
||||
dst_path = &dst_path.to_string()
|
||||
)
|
||||
};
|
||||
self.socket.run_command("sh", &["-lc", &script]).await?;
|
||||
self.socket.run_command("sh", &["-c", &script]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -797,7 +797,7 @@ impl SshSocket {
|
||||
}
|
||||
|
||||
async fn platform(&self) -> Result<RemotePlatform> {
|
||||
let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?;
|
||||
let uname = self.run_command("sh", &["-c", "uname -sm"]).await?;
|
||||
let Some((os, arch)) = uname.split_once(" ") else {
|
||||
anyhow::bail!("unknown uname: {uname:?}")
|
||||
};
|
||||
@@ -828,7 +828,7 @@ impl SshSocket {
|
||||
}
|
||||
|
||||
async fn shell(&self) -> String {
|
||||
match self.run_command("sh", &["-lc", "echo $SHELL"]).await {
|
||||
match self.run_command("sh", &["-c", "echo $SHELL"]).await {
|
||||
Ok(shell) => shell.trim().to_owned(),
|
||||
Err(e) => {
|
||||
log::error!("Failed to get shell: {e}");
|
||||
|
||||
@@ -73,6 +73,7 @@ impl SettingsValue<serde_json::Value> {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
let rx = settings_store.update_settings_file_at_path(fs.clone(), path.as_slice(), value);
|
||||
|
||||
let path = path.clone();
|
||||
cx.background_spawn(async move {
|
||||
rx.await?
|
||||
|
||||
@@ -14,6 +14,7 @@ collections.workspace = true
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
libsqlite3-sys.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
smol.workspace = true
|
||||
sqlformat.workspace = true
|
||||
|
||||
@@ -59,6 +59,7 @@ impl Connection {
|
||||
let mut store_completed_migration = self
|
||||
.exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
|
||||
|
||||
let mut did_migrate = false;
|
||||
for (index, migration) in migrations.iter().enumerate() {
|
||||
let migration =
|
||||
sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default());
|
||||
@@ -70,9 +71,7 @@ impl Connection {
|
||||
&sqlformat::QueryParams::None,
|
||||
Default::default(),
|
||||
);
|
||||
if completed_migration == migration
|
||||
|| migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE")
|
||||
{
|
||||
if completed_migration == migration {
|
||||
// Migration already run. Continue
|
||||
continue;
|
||||
} else if should_allow_migration_change(index, &completed_migration, &migration)
|
||||
@@ -91,12 +90,58 @@ impl Connection {
|
||||
}
|
||||
|
||||
self.eager_exec(&migration)?;
|
||||
did_migrate = true;
|
||||
store_completed_migration((domain, index, migration))?;
|
||||
}
|
||||
|
||||
if did_migrate {
|
||||
self.delete_rows_with_orphaned_foreign_key_references()?;
|
||||
self.exec("PRAGMA foreign_key_check;")?()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete any rows that were orphaned by a migration. This is needed
|
||||
/// because we disable foreign key constraints during migrations, so
|
||||
/// that it's possible to re-create a table with the same name, without
|
||||
/// deleting all associated data.
|
||||
fn delete_rows_with_orphaned_foreign_key_references(&self) -> Result<()> {
|
||||
let foreign_key_info: Vec<(String, String, String, String)> = self.select(
|
||||
r#"
|
||||
SELECT DISTINCT
|
||||
schema.name as child_table,
|
||||
foreign_keys.[from] as child_key,
|
||||
foreign_keys.[table] as parent_table,
|
||||
foreign_keys.[to] as parent_key
|
||||
FROM sqlite_schema schema
|
||||
JOIN pragma_foreign_key_list(schema.name) foreign_keys
|
||||
WHERE
|
||||
schema.type = 'table' AND
|
||||
schema.name NOT LIKE "sqlite_%"
|
||||
"#,
|
||||
)?()?;
|
||||
|
||||
if !foreign_key_info.is_empty() {
|
||||
log::info!(
|
||||
"Found {} foreign key relationships to check",
|
||||
foreign_key_info.len()
|
||||
);
|
||||
}
|
||||
|
||||
for (child_table, child_key, parent_table, parent_key) in foreign_key_info {
|
||||
self.exec(&format!(
|
||||
"
|
||||
DELETE FROM {child_table}
|
||||
WHERE {child_key} IS NOT NULL and {child_key} NOT IN
|
||||
(SELECT {parent_key} FROM {parent_table})
|
||||
"
|
||||
))?()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -95,6 +95,14 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
|
||||
let mut migration_result =
|
||||
anyhow::Result::<()>::Err(anyhow::anyhow!("Migration never run"));
|
||||
|
||||
let foreign_keys_enabled: bool =
|
||||
connection.select_row::<i32>("PRAGMA foreign_keys")?()
|
||||
.unwrap_or(None)
|
||||
.map(|enabled| enabled != 0)
|
||||
.unwrap_or(false);
|
||||
|
||||
connection.exec("PRAGMA foreign_keys = OFF;")?()?;
|
||||
|
||||
for _ in 0..MIGRATION_RETRIES {
|
||||
migration_result = connection
|
||||
.with_savepoint("thread_safe_multi_migration", || M::migrate(connection));
|
||||
@@ -104,6 +112,9 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
|
||||
}
|
||||
}
|
||||
|
||||
if foreign_keys_enabled {
|
||||
connection.exec("PRAGMA foreign_keys = ON;")?()?;
|
||||
}
|
||||
migration_result
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::{cmp::Ordering, fmt::Debug};
|
||||
|
||||
use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary};
|
||||
|
||||
/// A cheaply-cloneable ordered map based on a [SumTree](crate::SumTree).
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
|
||||
where
|
||||
|
||||
@@ -1192,8 +1192,8 @@ impl Element for TerminalElement {
|
||||
bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top);
|
||||
|
||||
let marked_text_cloned: Option<String> = {
|
||||
let ime_state = self.terminal_view.read(cx);
|
||||
ime_state.marked_text.clone()
|
||||
let ime_state = &self.terminal_view.read(cx).ime_state;
|
||||
ime_state.as_ref().map(|state| state.marked_text.clone())
|
||||
};
|
||||
|
||||
let terminal_input_handler = TerminalInputHandler {
|
||||
@@ -1421,11 +1421,9 @@ impl InputHandler for TerminalInputHandler {
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if let Some(range) = new_marked_range {
|
||||
self.terminal_view.update(cx, |view, view_cx| {
|
||||
view.set_marked_text(new_text.to_string(), range, view_cx);
|
||||
});
|
||||
}
|
||||
self.terminal_view.update(cx, |view, view_cx| {
|
||||
view.set_marked_text(new_text.to_string(), new_marked_range, view_cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _window: &mut Window, cx: &mut App) {
|
||||
|
||||
@@ -62,6 +62,11 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
struct ImeState {
|
||||
marked_text: String,
|
||||
marked_range_utf16: Option<Range<usize>>,
|
||||
}
|
||||
|
||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.);
|
||||
|
||||
@@ -138,8 +143,7 @@ pub struct TerminalView {
|
||||
scroll_handle: TerminalScrollHandle,
|
||||
show_scrollbar: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
marked_text: Option<String>,
|
||||
marked_range_utf16: Option<Range<usize>>,
|
||||
ime_state: Option<ImeState>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_terminal_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -263,8 +267,7 @@ impl TerminalView {
|
||||
show_scrollbar: !Self::should_autohide_scrollbar(cx),
|
||||
hide_scrollbar_task: None,
|
||||
cwd_serialized: false,
|
||||
marked_text: None,
|
||||
marked_range_utf16: None,
|
||||
ime_state: None,
|
||||
_subscriptions: vec![
|
||||
focus_in,
|
||||
focus_out,
|
||||
@@ -323,24 +326,27 @@ impl TerminalView {
|
||||
pub(crate) fn set_marked_text(
|
||||
&mut self,
|
||||
text: String,
|
||||
range: Range<usize>,
|
||||
range: Option<Range<usize>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.marked_text = Some(text);
|
||||
self.marked_range_utf16 = Some(range);
|
||||
self.ime_state = Some(ImeState {
|
||||
marked_text: text,
|
||||
marked_range_utf16: range,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Gets the current marked range (UTF-16).
|
||||
pub(crate) fn marked_text_range(&self) -> Option<Range<usize>> {
|
||||
self.marked_range_utf16.clone()
|
||||
self.ime_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.marked_range_utf16.clone())
|
||||
}
|
||||
|
||||
/// Clears the marked (pre-edit) text state.
|
||||
pub(crate) fn clear_marked_text(&mut self, cx: &mut Context<Self>) {
|
||||
if self.marked_text.is_some() {
|
||||
self.marked_text = None;
|
||||
self.marked_range_utf16 = None;
|
||||
if self.ime_state.is_some() {
|
||||
self.ime_state = None;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ struct FontFamilyCacheState {
|
||||
/// so we do it once and then use the cached values each render.
|
||||
#[derive(Default)]
|
||||
pub struct FontFamilyCache {
|
||||
state: RwLock<FontFamilyCacheState>,
|
||||
state: Arc<RwLock<FontFamilyCacheState>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -52,4 +52,44 @@ impl FontFamilyCache {
|
||||
|
||||
lock.font_families.clone()
|
||||
}
|
||||
|
||||
/// Returns the list of font families if they have been loaded
|
||||
pub fn try_list_font_families(&self) -> Option<Vec<SharedString>> {
|
||||
self.state
|
||||
.try_read()
|
||||
.filter(|state| state.loaded_at.is_some())
|
||||
.map(|state| state.font_families.clone())
|
||||
}
|
||||
|
||||
/// Prefetch all font names in the background
|
||||
pub async fn prefetch(&self, cx: &gpui::AsyncApp) {
|
||||
if self
|
||||
.state
|
||||
.try_read()
|
||||
.is_none_or(|state| state.loaded_at.is_some())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(text_system) = cx.update(|cx| App::text_system(cx).clone()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let state = self.state.clone();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
// We take this lock in the background executor to ensure that synchronous calls to `list_font_families` are blocked while we are prefetching,
|
||||
// while not blocking the main thread and risking deadlocks
|
||||
let mut lock = state.write();
|
||||
let all_font_names = text_system
|
||||
.all_font_names()
|
||||
.into_iter()
|
||||
.map(SharedString::from)
|
||||
.collect();
|
||||
lock.font_families = all_font_names;
|
||||
lock.loaded_at = Some(Instant::now());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,8 +253,9 @@ pub(crate) struct UiFontSize(Pixels);
|
||||
|
||||
impl Global for UiFontSize {}
|
||||
|
||||
/// In-memory override for the font size in the agent panel.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AgentFontSize(Pixels);
|
||||
pub struct AgentFontSize(Pixels);
|
||||
|
||||
impl Global for AgentFontSize {}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct OnboardingBanner {
|
||||
dismissed: bool,
|
||||
source: String,
|
||||
details: BannerDetails,
|
||||
visible_when: Option<Box<dyn Fn(&mut App) -> bool>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -42,12 +43,18 @@ impl OnboardingBanner {
|
||||
label: label.into(),
|
||||
subtitle: subtitle.or(Some(SharedString::from("Introducing:"))),
|
||||
},
|
||||
visible_when: None,
|
||||
dismissed: get_dismissed(source),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show(&self, _cx: &mut App) -> bool {
|
||||
!self.dismissed
|
||||
pub fn visible_when(mut self, predicate: impl Fn(&mut App) -> bool + 'static) -> Self {
|
||||
self.visible_when = Some(Box::new(predicate));
|
||||
self
|
||||
}
|
||||
|
||||
fn should_show(&self, cx: &mut App) -> bool {
|
||||
!self.dismissed && self.visible_when.as_ref().map_or(true, |f| f(cx))
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -279,13 +279,15 @@ impl TitleBar {
|
||||
|
||||
let banner = cx.new(|cx| {
|
||||
OnboardingBanner::new(
|
||||
"ACP Onboarding",
|
||||
IconName::Sparkle,
|
||||
"Bring Your Own Agent",
|
||||
"ACP Claude Code Onboarding",
|
||||
IconName::AiClaude,
|
||||
"Claude Code",
|
||||
Some("Introducing:".into()),
|
||||
zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(),
|
||||
zed_actions::agent::OpenClaudeCodeOnboardingModal.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
// When updating this to a non-AI feature release, remove this line.
|
||||
.visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai)
|
||||
});
|
||||
|
||||
let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.203.0"
|
||||
version = "0.203.4"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -1317,15 +1317,31 @@ pub fn handle_keymap_file_changes(
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut current_layout_id = cx.keyboard_layout().id().to_string();
|
||||
cx.on_keyboard_layout_change(move |cx| {
|
||||
let next_layout_id = cx.keyboard_layout().id();
|
||||
if next_layout_id != current_layout_id {
|
||||
current_layout_id = next_layout_id.to_string();
|
||||
keyboard_layout_tx.unbounded_send(()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut current_layout_id = cx.keyboard_layout().id().to_string();
|
||||
cx.on_keyboard_layout_change(move |cx| {
|
||||
let next_layout_id = cx.keyboard_layout().id();
|
||||
if next_layout_id != current_layout_id {
|
||||
current_layout_id = next_layout_id.to_string();
|
||||
keyboard_layout_tx.unbounded_send(()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned();
|
||||
cx.on_keyboard_layout_change(move |cx| {
|
||||
let next_mapping = cx.keyboard_mapper().get_key_equivalents();
|
||||
if current_mapping.as_ref() != next_mapping {
|
||||
current_mapping = next_mapping.cloned();
|
||||
keyboard_layout_tx.unbounded_send(()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
load_default_keymap(cx);
|
||||
|
||||
|
||||
@@ -286,6 +286,8 @@ pub mod agent {
|
||||
OpenOnboardingModal,
|
||||
/// Opens the ACP onboarding modal.
|
||||
OpenAcpOnboardingModal,
|
||||
/// Opens the Claude Code onboarding modal.
|
||||
OpenClaudeCodeOnboardingModal,
|
||||
/// Resets the agent onboarding state.
|
||||
ResetOnboarding,
|
||||
/// Starts a chat conversation with the agent.
|
||||
|
||||
Reference in New Issue
Block a user