Skip to content

Commit 99db990

Browse files
agent_ui: Fix thread title being overridden even when manually edited (zed-industries#49028)
This PR actually fixes two issues: - the thread title being overridden even after it's been manually edited; as you go and come back from the thread history view, the edited title would get swapped for the auto-summarized one - the parent thread title sometimes displaying the title of a subagent thread; this would also override the manual edit - - - - [x] Tests - [x] Code Reviewed - [x] Manual QA Release Notes: - Agent: Fixed thread titles being overridden even when manually edited. --------- Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
1 parent 85736ec commit 99db990

5 files changed

Lines changed: 97 additions & 49 deletions

File tree

‎crates/acp_thread/src/connection.rs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,14 @@ mod test_support {
727727
}
728728
}
729729

730+
fn set_title(
731+
&self,
732+
_session_id: &acp::SessionId,
733+
_cx: &App,
734+
) -> Option<Rc<dyn AgentSessionSetTitle>> {
735+
Some(Rc::new(StubAgentSessionSetTitle))
736+
}
737+
730738
fn truncate(
731739
&self,
732740
_session_id: &agent_client_protocol::SessionId,
@@ -740,6 +748,14 @@ mod test_support {
740748
}
741749
}
742750

751+
struct StubAgentSessionSetTitle;
752+
753+
impl AgentSessionSetTitle for StubAgentSessionSetTitle {
754+
fn run(&self, _title: SharedString, _cx: &mut App) -> Task<Result<()>> {
755+
Task::ready(Ok(()))
756+
}
757+
}
758+
743759
struct StubAgentSessionEditor;
744760

745761
impl AgentSessionTruncate for StubAgentSessionEditor {

‎crates/agent/src/agent.rs‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,12 +1395,19 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
13951395
fn set_title(
13961396
&self,
13971397
session_id: &acp::SessionId,
1398-
_cx: &App,
1398+
cx: &App,
13991399
) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
1400-
Some(Rc::new(NativeAgentSessionSetTitle {
1401-
connection: self.clone(),
1402-
session_id: session_id.clone(),
1403-
}) as _)
1400+
self.0.read_with(cx, |agent, _cx| {
1401+
agent
1402+
.sessions
1403+
.get(session_id)
1404+
.filter(|s| !s.thread.read(cx).is_subagent())
1405+
.map(|session| {
1406+
Rc::new(NativeAgentSessionSetTitle {
1407+
thread: session.thread.clone(),
1408+
}) as _
1409+
})
1410+
})
14041411
}
14051412

14061413
fn session_list(&self, cx: &mut App) -> Option<Rc<dyn AgentSessionList>> {
@@ -1559,17 +1566,13 @@ impl acp_thread::AgentSessionRetry for NativeAgentSessionRetry {
15591566
}
15601567

15611568
struct NativeAgentSessionSetTitle {
1562-
connection: NativeAgentConnection,
1563-
session_id: acp::SessionId,
1569+
thread: Entity<Thread>,
15641570
}
15651571

15661572
impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle {
15671573
fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>> {
1568-
let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else {
1569-
return Task::ready(Err(anyhow!("session not found")));
1570-
};
1571-
let thread = session.thread.clone();
1572-
thread.update(cx, |thread, cx| thread.set_title(title, cx));
1574+
self.thread
1575+
.update(cx, |thread, cx| thread.set_title(title, cx));
15731576
Task::ready(Ok(()))
15741577
}
15751578
}

‎crates/agent_ui/src/acp/thread_view.rs‎

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ impl AcpServerView {
723723
});
724724
}
725725

726-
let mut subscriptions = vec![
726+
let subscriptions = vec![
727727
cx.subscribe_in(&thread, window, Self::handle_thread_event),
728728
cx.observe(&action_log, |_, _, cx| cx.notify()),
729729
];
@@ -755,18 +755,6 @@ impl AcpServerView {
755755
.detach();
756756
}
757757

758-
let title_editor = if thread.update(cx, |thread, cx| thread.can_set_title(cx)) {
759-
let editor = cx.new(|cx| {
760-
let mut editor = Editor::single_line(window, cx);
761-
editor.set_text(thread.read(cx).title(), window, cx);
762-
editor
763-
});
764-
subscriptions.push(cx.subscribe_in(&editor, window, Self::handle_title_editor_event));
765-
Some(editor)
766-
} else {
767-
None
768-
};
769-
770758
let profile_selector: Option<Rc<agent::NativeAgentConnection>> =
771759
connection.clone().downcast();
772760
let profile_selector = profile_selector
@@ -802,7 +790,6 @@ impl AcpServerView {
802790
agent_display_name,
803791
self.workspace.clone(),
804792
entry_view_state,
805-
title_editor,
806793
config_options_view,
807794
mode_selector,
808795
model_selector,
@@ -984,20 +971,6 @@ impl AcpServerView {
984971
}
985972
}
986973

987-
pub fn handle_title_editor_event(
988-
&mut self,
989-
title_editor: &Entity<Editor>,
990-
event: &EditorEvent,
991-
window: &mut Window,
992-
cx: &mut Context<Self>,
993-
) {
994-
if let Some(active) = self.active_thread() {
995-
active.update(cx, |active, cx| {
996-
active.handle_title_editor_event(title_editor, event, window, cx);
997-
});
998-
}
999-
}
1000-
1001974
pub fn is_loading(&self) -> bool {
1002975
matches!(self.server_state, ServerState::Loading { .. })
1003976
}
@@ -1181,10 +1154,8 @@ impl AcpServerView {
11811154
}
11821155
AcpThreadEvent::TitleUpdated => {
11831156
let title = thread.read(cx).title();
1184-
if let Some(title_editor) = self
1185-
.thread_view(&thread_id)
1186-
.and_then(|active| active.read(cx).title_editor.clone())
1187-
{
1157+
if let Some(active_thread) = self.thread_view(&thread_id) {
1158+
let title_editor = active_thread.read(cx).title_editor.clone();
11881159
title_editor.update(cx, |editor, cx| {
11891160
if editor.text(cx) != title {
11901161
editor.set_text(title, window, cx);
@@ -5799,4 +5770,49 @@ pub(crate) mod tests {
57995770
"Missing deny pattern option"
58005771
);
58015772
}
5773+
5774+
#[gpui::test]
5775+
async fn test_manually_editing_title_updates_acp_thread_title(cx: &mut TestAppContext) {
5776+
init_test(cx);
5777+
5778+
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
5779+
5780+
let active = active_thread(&thread_view, cx);
5781+
let title_editor = cx.read(|cx| active.read(cx).title_editor.clone());
5782+
let thread = cx.read(|cx| active.read(cx).thread.clone());
5783+
5784+
title_editor.read_with(cx, |editor, cx| {
5785+
assert!(!editor.read_only(cx));
5786+
});
5787+
5788+
title_editor.update_in(cx, |editor, window, cx| {
5789+
editor.set_text("My Custom Title", window, cx);
5790+
});
5791+
cx.run_until_parked();
5792+
5793+
title_editor.read_with(cx, |editor, cx| {
5794+
assert_eq!(editor.text(cx), "My Custom Title");
5795+
});
5796+
thread.read_with(cx, |thread, _cx| {
5797+
assert_eq!(thread.title().as_ref(), "My Custom Title");
5798+
});
5799+
}
5800+
5801+
#[gpui::test]
5802+
async fn test_title_editor_is_read_only_when_set_title_unsupported(cx: &mut TestAppContext) {
5803+
init_test(cx);
5804+
5805+
let (thread_view, cx) =
5806+
setup_thread_view(StubAgentServer::new(ResumeOnlyAgentConnection), cx).await;
5807+
5808+
let active = active_thread(&thread_view, cx);
5809+
let title_editor = cx.read(|cx| active.read(cx).title_editor.clone());
5810+
5811+
title_editor.read_with(cx, |editor, cx| {
5812+
assert!(
5813+
editor.read_only(cx),
5814+
"Title editor should be read-only when the connection does not support set_title"
5815+
);
5816+
});
5817+
}
58025818
}

‎crates/agent_ui/src/acp/thread_view/active_thread.rs‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ pub struct AcpThreadView {
176176
pub focus_handle: FocusHandle,
177177
pub workspace: WeakEntity<Workspace>,
178178
pub entry_view_state: Entity<EntryViewState>,
179-
pub title_editor: Option<Entity<Editor>>,
179+
pub title_editor: Entity<Editor>,
180180
pub config_options_view: Option<Entity<ConfigOptionsView>>,
181181
pub mode_selector: Option<Entity<ModeSelector>>,
182182
pub model_selector: Option<Entity<AcpModelSelectorPopover>>,
@@ -266,7 +266,6 @@ impl AcpThreadView {
266266
agent_display_name: SharedString,
267267
workspace: WeakEntity<Workspace>,
268268
entry_view_state: Entity<EntryViewState>,
269-
title_editor: Option<Entity<Editor>>,
270269
config_options_view: Option<Entity<ConfigOptionsView>>,
271270
mode_selector: Option<Entity<ModeSelector>>,
272271
model_selector: Option<Entity<AcpModelSelectorPopover>>,
@@ -332,6 +331,18 @@ impl AcpThreadView {
332331
&& project.upgrade().is_some_and(|p| p.read(cx).is_local())
333332
&& agent_name == "Codex";
334333

334+
let title_editor = {
335+
let can_edit = thread.update(cx, |thread, cx| thread.can_set_title(cx));
336+
let editor = cx.new(|cx| {
337+
let mut editor = Editor::single_line(window, cx);
338+
editor.set_text(thread.read(cx).title(), window, cx);
339+
editor.set_read_only(!can_edit);
340+
editor
341+
});
342+
subscriptions.push(cx.subscribe_in(&editor, window, Self::handle_title_editor_event));
343+
editor
344+
};
345+
335346
subscriptions.push(cx.subscribe_in(
336347
&entry_view_state,
337348
window,
@@ -2303,7 +2314,6 @@ impl AcpThreadView {
23032314
return None;
23042315
};
23052316

2306-
let title = self.thread.read(cx).title();
23072317
let server_view = self.server_view.clone();
23082318

23092319
let is_done = self.thread.read(cx).status() == ThreadStatus::Idle;
@@ -2315,17 +2325,20 @@ impl AcpThreadView {
23152325
.pr_1p5()
23162326
.w_full()
23172327
.justify_between()
2328+
.gap_1()
23182329
.border_b_1()
23192330
.border_color(cx.theme().colors().border)
23202331
.bg(cx.theme().colors().editor_background.opacity(0.2))
23212332
.child(
23222333
h_flex()
2334+
.flex_1()
2335+
.gap_2()
23232336
.child(
23242337
Icon::new(IconName::ForwardArrowUp)
23252338
.size(IconSize::Small)
23262339
.color(Color::Muted),
23272340
)
2328-
.child(Label::new(title).color(Color::Muted).ml_2().mr_1())
2341+
.child(self.title_editor.clone())
23292342
.when(is_done, |this| {
23302343
this.child(Icon::new(IconName::Check).color(Color::Success))
23312344
}),

‎crates/agent_ui/src/agent_panel.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1954,7 +1954,7 @@ impl AgentPanel {
19541954
if let Some(title_editor) = thread_view
19551955
.read(cx)
19561956
.parent_thread(cx)
1957-
.and_then(|r| r.read(cx).title_editor.clone())
1957+
.map(|r| r.read(cx).title_editor.clone())
19581958
{
19591959
let container = div()
19601960
.w_full()

0 commit comments

Comments
 (0)