Detailed changes
@@ -727,6 +727,14 @@ mod test_support {
}
}
+ fn set_title(
+ &self,
+ _session_id: &acp::SessionId,
+ _cx: &App,
+ ) -> Option<Rc<dyn AgentSessionSetTitle>> {
+ 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<Result<()>> {
+ Task::ready(Ok(()))
+ }
+ }
+
struct StubAgentSessionEditor;
impl AgentSessionTruncate for StubAgentSessionEditor {
@@ -1395,12 +1395,19 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
fn set_title(
&self,
session_id: &acp::SessionId,
- _cx: &App,
+ cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
- 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<Rc<dyn AgentSessionList>> {
@@ -1559,17 +1566,13 @@ impl acp_thread::AgentSessionRetry for NativeAgentSessionRetry {
}
struct NativeAgentSessionSetTitle {
- connection: NativeAgentConnection,
- session_id: acp::SessionId,
+ thread: Entity<Thread>,
}
impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle {
fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>> {
- 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(()))
}
}
@@ -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<Rc<agent::NativeAgentConnection>> =
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<Editor>,
- event: &EditorEvent,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- 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"
+ );
+ });
+ }
}
@@ -176,7 +176,7 @@ pub struct AcpThreadView {
pub focus_handle: FocusHandle,
pub workspace: WeakEntity<Workspace>,
pub entry_view_state: Entity<EntryViewState>,
- pub title_editor: Option<Entity<Editor>>,
+ pub title_editor: Entity<Editor>,
pub config_options_view: Option<Entity<ConfigOptionsView>>,
pub mode_selector: Option<Entity<ModeSelector>>,
pub model_selector: Option<Entity<AcpModelSelectorPopover>>,
@@ -266,7 +266,6 @@ impl AcpThreadView {
agent_display_name: SharedString,
workspace: WeakEntity<Workspace>,
entry_view_state: Entity<EntryViewState>,
- title_editor: Option<Entity<Editor>>,
config_options_view: Option<Entity<ConfigOptionsView>>,
mode_selector: Option<Entity<ModeSelector>>,
model_selector: Option<Entity<AcpModelSelectorPopover>>,
@@ -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))
}),
@@ -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()