diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 3bba53847b7bf9910ef5fb286cc41694ec9aef07..6a63239fcfbee5f97cb820d7b3e7ce0dfbc2e785 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -727,6 +727,14 @@ mod test_support { } } + fn set_title( + &self, + _session_id: &acp::SessionId, + _cx: &App, + ) -> Option> { + Some(Rc::new(StubAgentSessionSetTitle)) + } + fn truncate( &self, _session_id: &agent_client_protocol::SessionId, @@ -740,6 +748,14 @@ mod test_support { } } + struct StubAgentSessionSetTitle; + + impl AgentSessionSetTitle for StubAgentSessionSetTitle { + fn run(&self, _title: SharedString, _cx: &mut App) -> Task> { + Task::ready(Ok(())) + } + } + struct StubAgentSessionEditor; impl AgentSessionTruncate for StubAgentSessionEditor { diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 3c4428034950fe1f9c4db17127fbb7be37622bf6..a663494a1bdeecea8d2d164fe4a210cbb0bd5534 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -1395,12 +1395,19 @@ impl acp_thread::AgentConnection for NativeAgentConnection { fn set_title( &self, session_id: &acp::SessionId, - _cx: &App, + cx: &App, ) -> Option> { - Some(Rc::new(NativeAgentSessionSetTitle { - connection: self.clone(), - session_id: session_id.clone(), - }) as _) + self.0.read_with(cx, |agent, _cx| { + agent + .sessions + .get(session_id) + .filter(|s| !s.thread.read(cx).is_subagent()) + .map(|session| { + Rc::new(NativeAgentSessionSetTitle { + thread: session.thread.clone(), + }) as _ + }) + }) } fn session_list(&self, cx: &mut App) -> Option> { @@ -1559,17 +1566,13 @@ impl acp_thread::AgentSessionRetry for NativeAgentSessionRetry { } struct NativeAgentSessionSetTitle { - connection: NativeAgentConnection, - session_id: acp::SessionId, + thread: Entity, } impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle { fn run(&self, title: SharedString, cx: &mut App) -> Task> { - let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else { - return Task::ready(Err(anyhow!("session not found"))); - }; - let thread = session.thread.clone(); - thread.update(cx, |thread, cx| thread.set_title(title, cx)); + self.thread + .update(cx, |thread, cx| thread.set_title(title, cx)); Task::ready(Ok(())) } } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index cffc90ea278e24fb81aba287c2668b2ac9a6655a..13a454cdeaa5ccf0a99253ab896075ff0bce9007 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -723,7 +723,7 @@ impl AcpServerView { }); } - let mut subscriptions = vec![ + let subscriptions = vec![ cx.subscribe_in(&thread, window, Self::handle_thread_event), cx.observe(&action_log, |_, _, cx| cx.notify()), ]; @@ -755,18 +755,6 @@ impl AcpServerView { .detach(); } - let title_editor = if thread.update(cx, |thread, cx| thread.can_set_title(cx)) { - let editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_text(thread.read(cx).title(), window, cx); - editor - }); - subscriptions.push(cx.subscribe_in(&editor, window, Self::handle_title_editor_event)); - Some(editor) - } else { - None - }; - let profile_selector: Option> = connection.clone().downcast(); let profile_selector = profile_selector @@ -802,7 +790,6 @@ impl AcpServerView { agent_display_name, self.workspace.clone(), entry_view_state, - title_editor, config_options_view, mode_selector, model_selector, @@ -984,20 +971,6 @@ impl AcpServerView { } } - pub fn handle_title_editor_event( - &mut self, - title_editor: &Entity, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(active) = self.active_thread() { - active.update(cx, |active, cx| { - active.handle_title_editor_event(title_editor, event, window, cx); - }); - } - } - pub fn is_loading(&self) -> bool { matches!(self.server_state, ServerState::Loading { .. }) } @@ -1181,10 +1154,8 @@ impl AcpServerView { } AcpThreadEvent::TitleUpdated => { let title = thread.read(cx).title(); - if let Some(title_editor) = self - .thread_view(&thread_id) - .and_then(|active| active.read(cx).title_editor.clone()) - { + if let Some(active_thread) = self.thread_view(&thread_id) { + let title_editor = active_thread.read(cx).title_editor.clone(); title_editor.update(cx, |editor, cx| { if editor.text(cx) != title { editor.set_text(title, window, cx); @@ -5799,4 +5770,49 @@ pub(crate) mod tests { "Missing deny pattern option" ); } + + #[gpui::test] + async fn test_manually_editing_title_updates_acp_thread_title(cx: &mut TestAppContext) { + init_test(cx); + + let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; + + let active = active_thread(&thread_view, cx); + let title_editor = cx.read(|cx| active.read(cx).title_editor.clone()); + let thread = cx.read(|cx| active.read(cx).thread.clone()); + + title_editor.read_with(cx, |editor, cx| { + assert!(!editor.read_only(cx)); + }); + + title_editor.update_in(cx, |editor, window, cx| { + editor.set_text("My Custom Title", window, cx); + }); + cx.run_until_parked(); + + title_editor.read_with(cx, |editor, cx| { + assert_eq!(editor.text(cx), "My Custom Title"); + }); + thread.read_with(cx, |thread, _cx| { + assert_eq!(thread.title().as_ref(), "My Custom Title"); + }); + } + + #[gpui::test] + async fn test_title_editor_is_read_only_when_set_title_unsupported(cx: &mut TestAppContext) { + init_test(cx); + + let (thread_view, cx) = + setup_thread_view(StubAgentServer::new(ResumeOnlyAgentConnection), cx).await; + + let active = active_thread(&thread_view, cx); + let title_editor = cx.read(|cx| active.read(cx).title_editor.clone()); + + title_editor.read_with(cx, |editor, cx| { + assert!( + editor.read_only(cx), + "Title editor should be read-only when the connection does not support set_title" + ); + }); + } } diff --git a/crates/agent_ui/src/acp/thread_view/active_thread.rs b/crates/agent_ui/src/acp/thread_view/active_thread.rs index 1f377b345026547046044825903ba0c9a55fa412..73b2408c02f2a9950a8c38b57bebc1fc1b3a51bc 100644 --- a/crates/agent_ui/src/acp/thread_view/active_thread.rs +++ b/crates/agent_ui/src/acp/thread_view/active_thread.rs @@ -176,7 +176,7 @@ pub struct AcpThreadView { pub focus_handle: FocusHandle, pub workspace: WeakEntity, pub entry_view_state: Entity, - pub title_editor: Option>, + pub title_editor: Entity, pub config_options_view: Option>, pub mode_selector: Option>, pub model_selector: Option>, @@ -266,7 +266,6 @@ impl AcpThreadView { agent_display_name: SharedString, workspace: WeakEntity, entry_view_state: Entity, - title_editor: Option>, config_options_view: Option>, mode_selector: Option>, model_selector: Option>, @@ -332,6 +331,18 @@ impl AcpThreadView { && project.upgrade().is_some_and(|p| p.read(cx).is_local()) && agent_name == "Codex"; + let title_editor = { + let can_edit = thread.update(cx, |thread, cx| thread.can_set_title(cx)); + let editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text(thread.read(cx).title(), window, cx); + editor.set_read_only(!can_edit); + editor + }); + subscriptions.push(cx.subscribe_in(&editor, window, Self::handle_title_editor_event)); + editor + }; + subscriptions.push(cx.subscribe_in( &entry_view_state, window, @@ -2303,7 +2314,6 @@ impl AcpThreadView { return None; }; - let title = self.thread.read(cx).title(); let server_view = self.server_view.clone(); let is_done = self.thread.read(cx).status() == ThreadStatus::Idle; @@ -2315,17 +2325,20 @@ impl AcpThreadView { .pr_1p5() .w_full() .justify_between() + .gap_1() .border_b_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background.opacity(0.2)) .child( h_flex() + .flex_1() + .gap_2() .child( Icon::new(IconName::ForwardArrowUp) .size(IconSize::Small) .color(Color::Muted), ) - .child(Label::new(title).color(Color::Muted).ml_2().mr_1()) + .child(self.title_editor.clone()) .when(is_done, |this| { this.child(Icon::new(IconName::Check).color(Color::Success)) }), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 9338cde0da066bea295ea7bb0e68fb5844288852..33b5acb9f376bf21744646075d172c32872c9346 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1954,7 +1954,7 @@ impl AgentPanel { if let Some(title_editor) = thread_view .read(cx) .parent_thread(cx) - .and_then(|r| r.read(cx).title_editor.clone()) + .map(|r| r.read(cx).title_editor.clone()) { let container = div() .w_full()