diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index c610c53ea8d61d24ece6d3c80ec15505d259ea3b..6ac81639ca4e6e94cab22fc7cb7ee5344c92983e 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -45,11 +45,20 @@ pub fn init(cx: &mut App) { pub struct AgentServerDelegate { project: Entity, status_tx: Option>, + new_version_available: Option>>, } impl AgentServerDelegate { - pub fn new(project: Entity, status_tx: Option>) -> Self { - Self { project, status_tx } + pub fn new( + project: Entity, + status_tx: Option>, + new_version_tx: Option>>, + ) -> Self { + Self { + project, + status_tx, + new_version_available: new_version_tx, + } } pub fn project(&self) -> &Entity { @@ -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 { @@ -160,6 +170,9 @@ impl AgentServerDelegate { ) .await .log_err(); + if let Some(mut new_version_available) = new_version_available { + new_version_available.send(Some(latest_version)).ok(); + } } } }) diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 7988b86081351b29c8a19b676498db26d0b83fc3..f801ef246807f93c4bbdc26a1ff3bd478cc476d0 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -498,7 +498,7 @@ pub async fn new_test_thread( current_dir: impl AsRef, cx: &mut TestAppContext, ) -> Entity { - 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)) diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index ebe0e5c1c6dbcee71df010f4702e7567a8c26b2f..da121bb7a486d80f15125d2ecc526b3b01e059d3 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -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?; diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 992e12177abe144b1ba00b7a5e2a9c8806866593..5269b8b0f7a85b63e560b53115bcc8ebf74bb8ab 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -46,7 +46,7 @@ use text::Anchor; use theme::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,6 +288,7 @@ pub struct AcpThreadView { prompt_capabilities: Rc>, available_commands: Rc>>, is_loading_contents: bool, + new_server_version_available: Option, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -416,9 +417,23 @@ 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.thread_state = Self::initial_state( + self.agent.clone(), + None, + self.workspace.clone(), + self.project.clone(), + window, + cx, + ); + self.new_server_version_available.take(); + cx.notify(); + } + fn initial_state( agent: Rc, resume_thread: Option, @@ -451,8 +466,13 @@ impl AcpThreadView { }) .next() .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| { @@ -627,10 +647,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(); @@ -672,15 +705,7 @@ impl AcpThreadView { .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(); } @@ -1443,7 +1468,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; @@ -1472,14 +1496,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() }) @@ -1501,7 +1518,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| { @@ -4800,6 +4817,38 @@ impl AcpThreadView { Some(div().child(content)) } + fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context) -> 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") @@ -5210,6 +5259,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())