diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index a16a4a7895b28b281d5a1d8d883206252b33c412..3e3cbc19ab40ee483d5c9667d7a3cc132befde00 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1013,7 +1013,7 @@ pub struct AcpThread { session_id: acp::SessionId, work_dirs: Option, parent_session_id: Option, - title: SharedString, + title: Option, provisional_title: Option, entries: Vec, plan: Plan, @@ -1176,7 +1176,7 @@ impl Error for LoadError {} impl AcpThread { pub fn new( parent_session_id: Option, - title: impl Into, + title: Option, work_dirs: Option, connection: Rc, project: Entity, @@ -1203,7 +1203,7 @@ impl AcpThread { shared_buffers: Default::default(), entries: Default::default(), plan: Default::default(), - title: title.into(), + title, provisional_title: None, project, running_turn: None, @@ -1259,10 +1259,10 @@ impl AcpThread { &self.project } - pub fn title(&self) -> SharedString { - self.provisional_title + pub fn title(&self) -> Option { + self.title .clone() - .unwrap_or_else(|| self.title.clone()) + .or_else(|| self.provisional_title.clone()) } pub fn has_provisional_title(&self) -> bool { @@ -1387,8 +1387,8 @@ impl AcpThread { if let acp::MaybeUndefined::Value(title) = info_update.title { let had_provisional = self.provisional_title.take().is_some(); let title: SharedString = title.into(); - if title != self.title { - self.title = title; + if self.title.as_ref() != Some(&title) { + self.title = Some(title); cx.emit(AcpThreadEvent::TitleUpdated); } else if had_provisional { cx.emit(AcpThreadEvent::TitleUpdated); @@ -1676,8 +1676,8 @@ impl AcpThread { pub fn set_title(&mut self, title: SharedString, cx: &mut Context) -> Task> { let had_provisional = self.provisional_title.take().is_some(); - if title != self.title { - self.title = title.clone(); + if self.title.as_ref() != Some(&title) { + self.title = Some(title.clone()); cx.emit(AcpThreadEvent::TitleUpdated); if let Some(set_title) = self.connection.set_title(&self.session_id, cx) { return set_title.run(title, cx); @@ -4297,7 +4297,7 @@ mod tests { let thread = cx.new(|cx| { AcpThread::new( None, - "Test", + None, Some(work_dirs), self.clone(), project, @@ -4999,7 +4999,7 @@ mod tests { // Initial title is the default. thread.read_with(cx, |thread, _| { - assert_eq!(thread.title().as_ref(), "Test"); + assert_eq!(thread.title(), None); }); // Setting a provisional title updates the display title. @@ -5007,7 +5007,10 @@ mod tests { thread.set_provisional_title("Hello, can you help…".into(), cx); }); thread.read_with(cx, |thread, _| { - assert_eq!(thread.title().as_ref(), "Hello, can you help…"); + assert_eq!( + thread.title().as_ref().map(|s| s.as_str()), + Some("Hello, can you help…") + ); }); // The provisional title should NOT have propagated to the connection. @@ -5024,7 +5027,10 @@ mod tests { }); task.await.expect("set_title should succeed"); thread.read_with(cx, |thread, _| { - assert_eq!(thread.title().as_ref(), "Helping with Rust question"); + assert_eq!( + thread.title().as_ref().map(|s| s.as_str()), + Some("Helping with Rust question") + ); }); assert_eq!( set_title_calls.borrow().as_slice(), @@ -5088,7 +5094,10 @@ mod tests { result.expect("session info update should succeed"); thread.read_with(cx, |thread, _| { - assert_eq!(thread.title().as_ref(), "Helping with Rust question"); + assert_eq!( + thread.title().as_ref().map(|s| s.as_str()), + Some("Helping with Rust question") + ); assert!( !thread.has_provisional_title(), "session info title update should clear provisional title" diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index fd47c77e7d6ff7245dd4e98f1b87cce80e1cbba6..1c6f2a49f18b4b6c664f49d1a732de9a53c75aab 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -665,11 +665,10 @@ mod test_support { cx: &mut gpui::App, ) -> Entity { let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread_title = title.unwrap_or_else(|| SharedString::new_static("Test")); let thread = cx.new(|cx| { AcpThread::new( None, - thread_title, + title, Some(work_dirs), self.clone(), project, diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 37dee2d97f44f7290ad9a084fccb3fc226f6de52..77c326feec60514d459e6026a39f1bcd5ed8a896 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -662,14 +662,16 @@ impl NativeAgent { let Some(session) = self.sessions.get(session_id) else { return; }; - let thread = thread.downgrade(); - let acp_thread = session.acp_thread.downgrade(); - cx.spawn(async move |_, cx| { - let title = thread.read_with(cx, |thread, _| thread.title())?; - let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?; - task.await - }) - .detach_and_log_err(cx); + + if let Some(title) = thread.read(cx).title() { + let acp_thread = session.acp_thread.downgrade(); + cx.spawn(async move |_, cx| { + let task = + acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?; + task.await + }) + .detach_and_log_err(cx); + } } fn handle_thread_token_usage_updated( diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index d486f4f667a91fd18e5f5cade1933f2527e8048f..5cb2c99bfae222b13fd978e1bc3eba2fe98ca1d6 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -3122,7 +3122,7 @@ async fn test_title_generation(cx: &mut TestAppContext) { fake_model.send_last_completion_stream_text_chunk("Hey!"); fake_model.end_last_completion_stream(); cx.run_until_parked(); - thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "New Thread")); + thread.read_with(cx, |thread, _| assert_eq!(thread.title(), None)); // Ensure the summary model has been invoked to generate a title. summary_model.send_last_completion_stream_text_chunk("Hello "); @@ -3131,7 +3131,9 @@ async fn test_title_generation(cx: &mut TestAppContext) { summary_model.end_last_completion_stream(); send.collect::>().await; cx.run_until_parked(); - thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); + thread.read_with(cx, |thread, _| { + assert_eq!(thread.title(), Some("Hello world".into())) + }); // Send another message, ensuring no title is generated this time. let send = thread @@ -3145,7 +3147,9 @@ async fn test_title_generation(cx: &mut TestAppContext) { cx.run_until_parked(); assert_eq!(summary_model.pending_completions(), Vec::new()); send.collect::>().await; - thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); + thread.read_with(cx, |thread, _| { + assert_eq!(thread.title(), Some("Hello world".into())) + }); } #[gpui::test] diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 5e1de6783953a53a92196823e79b168ee9f08319..39f5a9df902744875a9faaa1651d65842c1dbf11 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1312,7 +1312,7 @@ impl Thread { pub fn to_db(&self, cx: &App) -> Task { let initial_project_snapshot = self.initial_project_snapshot.clone(); let mut thread = DbThread { - title: self.title(), + title: self.title().unwrap_or_default(), messages: self.messages.clone(), updated_at: self.updated_at, detailed_summary: self.summary.clone(), @@ -2491,8 +2491,8 @@ impl Thread { } } - pub fn title(&self) -> SharedString { - self.title.clone().unwrap_or("New Thread".into()) + pub fn title(&self) -> Option { + self.title.clone() } pub fn is_generating_summary(&self) -> bool { diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 14b4628c764b76e58a2d2be1637f760a46d7bfad..f7b6a59a63b02028a8b30c905c92b82805a52b33 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -42,7 +42,6 @@ pub struct UnsupportedVersion; pub struct AcpConnection { id: AgentId, - display_name: SharedString, telemetry_id: SharedString, connection: Rc, sessions: Rc>>, @@ -167,7 +166,6 @@ impl AgentSessionList for AcpSessionList { pub async fn connect( agent_id: AgentId, project: Entity, - display_name: SharedString, command: AgentServerCommand, default_mode: Option, default_model: Option, @@ -177,7 +175,6 @@ pub async fn connect( let conn = AcpConnection::stdio( agent_id, project, - display_name, command.clone(), default_mode, default_model, @@ -194,7 +191,6 @@ impl AcpConnection { pub async fn stdio( agent_id: AgentId, project: Entity, - display_name: SharedString, command: AgentServerCommand, default_mode: Option, default_model: Option, @@ -364,7 +360,6 @@ impl AcpConnection { auth_methods, command, connection, - display_name, telemetry_id, sessions, agent_capabilities: response.agent_capabilities, @@ -660,7 +655,7 @@ impl AgentConnection for AcpConnection { let thread: Entity = cx.new(|cx| { AcpThread::new( None, - self.display_name.clone(), + None, Some(work_dirs), self.clone(), project, @@ -718,7 +713,6 @@ impl AgentConnection for AcpConnection { let mcp_servers = mcp_servers_for_project(&project, cx); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let title = title.unwrap_or_else(|| self.display_name.clone()); let thread: Entity = cx.new(|cx| { AcpThread::new( None, @@ -801,7 +795,6 @@ impl AgentConnection for AcpConnection { let mcp_servers = mcp_servers_for_project(&project, cx); let action_log = cx.new(|_| ActionLog::new(project.clone())); - let title = title.unwrap_or_else(|| self.display_name.clone()); let thread: Entity = cx.new(|cx| { AcpThread::new( None, diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 2506608432ffa7a1eaf82bb3dfd15259a5dd53e5..0dcd2240d6ecf6dc052cdd55953cff8ec1442eae 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -296,11 +296,6 @@ impl AgentServer for CustomAgentServer { cx: &mut App, ) -> Task>> { let agent_id = self.agent_id(); - let display_name = delegate - .store - .read(cx) - .agent_display_name(&agent_id) - .unwrap_or_else(|| agent_id.0.clone()); let default_mode = self.default_mode(cx); let default_model = self.default_model(cx); let is_registry_agent = is_registry_agent(agent_id.clone(), cx); @@ -376,7 +371,6 @@ impl AgentServer for CustomAgentServer { let connection = crate::acp::connect( agent_id, project, - display_name, command, default_mode, default_model, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index a44546fb2bfdfe4800d8087a6370635c6e96de9e..44b706bbe705ea9368c79fb774bd171f6220c70b 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -44,7 +44,6 @@ pub struct AgentDiffPane { thread: Entity, focus_handle: FocusHandle, workspace: WeakEntity, - title: SharedString, _subscriptions: Vec, } @@ -113,7 +112,6 @@ impl AgentDiffPane { this.handle_acp_thread_event(event, cx) }), ], - title: SharedString::default(), multibuffer, editor, thread, @@ -121,7 +119,6 @@ impl AgentDiffPane { workspace, }; this.update_excerpts(window, cx); - this.update_title(cx); this } @@ -231,17 +228,9 @@ impl AgentDiffPane { } } - fn update_title(&mut self, cx: &mut Context) { - let new_title = self.thread.read(cx).title(); - if new_title != self.title { - self.title = new_title; - cx.emit(EditorEvent::TitleChanged); - } - } - fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context) { if let AcpThreadEvent::TitleUpdated = event { - self.update_title(cx) + cx.emit(EditorEvent::TitleChanged); } } @@ -534,13 +523,17 @@ impl Item for AgentDiffPane { fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { let title = self.thread.read(cx).title(); - Label::new(format!("Review: {}", title)) - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() + Label::new(if let Some(title) = title { + format!("Review: {}", title) + } else { + "Review".to_string() + }) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 15dffbae160779508f7aa2a7c2bd79b7fa6a2226..9fed388cb8596096cf5b6dc64cceef31de6397fd 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -26,7 +26,6 @@ use zed_actions::agent::{ ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff, }; -use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal, HoldForDefault}; use crate::{ AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CycleStartThreadIn, Follow, InlineAssistant, LoadThreadFromClipboard, NewTextThread, NewThread, @@ -42,6 +41,10 @@ use crate::{ Agent, AgentInitialContent, ExternalSourcePrompt, NewExternalAgentThread, NewNativeAgentThreadFromSummary, }; +use crate::{ + DEFAULT_THREAD_TITLE, + ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal, HoldForDefault}, +}; use crate::{ ExpandMessageEditor, ThreadHistoryView, text_thread_history::{TextThreadHistory, TextThreadHistoryEvent}, @@ -92,7 +95,6 @@ use zed_actions::{ const AGENT_PANEL_KEY: &str = "agent_panel"; const RECENTLY_UPDATED_MENU_LIMIT: usize = 6; -const DEFAULT_THREAD_TITLE: &str = "New Thread"; fn read_serialized_panel( workspace_id: workspace::WorkspaceId, @@ -775,11 +777,7 @@ impl AgentPanel { SerializedActiveThread { session_id: thread.session_id().0.to_string(), agent_type: self.selected_agent_type.clone(), - title: if title.as_ref() != DEFAULT_THREAD_TITLE { - Some(title.to_string()) - } else { - None - }, + title: title.map(|t| t.to_string()), work_dirs: work_dirs.map(|dirs| dirs.serialize()), } }); @@ -3221,7 +3219,7 @@ impl AgentPanel { .map(|r| r.read(cx).title_editor.clone()) { if is_generating_title { - Label::new("New Thread…") + Label::new(DEFAULT_THREAD_TITLE) .color(Color::Muted) .truncate() .with_animation( diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index f4b346d2ec86e65e75a64453421276ebcae8be38..db3b2526e45a4aade6d03a8d8ff87fd26106b76d 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -80,6 +80,8 @@ pub(crate) use thread_history::ThreadHistory; pub(crate) use thread_history_view::*; use zed_actions; +pub const DEFAULT_THREAD_TITLE: &str = "New Thread"; + actions!( agent, [ diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index 96660ee0e58598cea423c0416719f3fe174f58cc..1c81ae85845e2c8a92f9c5dff9eb7d2d35ccd98b 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicBool; +use crate::DEFAULT_THREAD_TITLE; use crate::ThreadHistory; use acp_thread::MentionUri; use agent_client_protocol as acp; @@ -192,7 +193,7 @@ pub struct EntryMatch { fn session_title(title: Option) -> SharedString { title .filter(|title| !title.is_empty()) - .unwrap_or_else(|| SharedString::new_static("New Thread")) + .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE)) } #[derive(Debug, Clone)] @@ -1098,11 +1099,11 @@ impl PromptCompletionProvider { if let Some(agent_panel) = workspace.panel::(cx) && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx) + && let Some(title) = thread.read(cx).title() { - let thread = thread.read(cx); mentions.insert(MentionUri::Thread { - id: thread.session_id().clone(), - name: thread.title().into(), + id: thread.read(cx).session_id().clone(), + name: title.to_string(), }); } diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index d0ccf2dd0116074cbcdfff3162d585e5b3222cbf..28c823430c4cf92e24075fb56a526a75244b45c9 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -40,6 +40,7 @@ use parking_lot::RwLock; use project::{AgentId, AgentServerStore, Project, ProjectEntryId}; use prompt_store::{PromptId, PromptStore}; +use crate::DEFAULT_THREAD_TITLE; use crate::message_editor::SessionCapabilities; use rope::Point; use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore}; @@ -551,7 +552,7 @@ impl ConversationView { ( Some(thread.session_id().clone()), thread.work_dirs().cloned(), - Some(thread.title()), + thread.title(), ) }) .unwrap_or((None, None, None)); @@ -1106,9 +1107,12 @@ impl ConversationView { &self.workspace } - pub fn title(&self, _cx: &App) -> SharedString { + pub fn title(&self, cx: &App) -> SharedString { match &self.server_state { - ServerState::Connected(_) => "New Thread".into(), + ServerState::Connected(view) => view + .active_view() + .and_then(|v| v.read(cx).thread.read(cx).title()) + .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()), ServerState::Loading(_) => "Loading…".into(), ServerState::LoadError { error, .. } => match error { LoadError::Unsupported { .. } => { @@ -1350,8 +1354,9 @@ impl ConversationView { ); } AcpThreadEvent::TitleUpdated => { - let title = thread.read(cx).title(); - if let Some(active_thread) = self.thread_view(&thread_id) { + if let Some(title) = thread.read(cx).title() + && 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 { @@ -3708,7 +3713,7 @@ pub(crate) mod tests { cx.new(|cx| { AcpThread::new( None, - name, + Some(name.into()), None, connection, project, @@ -3908,7 +3913,7 @@ pub(crate) mod tests { Task::ready(Ok(cx.new(|cx| { AcpThread::new( None, - "AuthGatedAgent", + None, Some(work_dirs), self, project, @@ -3982,7 +3987,7 @@ pub(crate) mod tests { let action_log = cx.new(|_| ActionLog::new(project.clone())); AcpThread::new( None, - "SaboteurAgentConnection", + None, Some(work_dirs), self, project, @@ -4052,7 +4057,7 @@ pub(crate) mod tests { let action_log = cx.new(|_| ActionLog::new(project.clone())); AcpThread::new( None, - "RefusalAgentConnection", + None, Some(work_dirs), self, project, @@ -4132,7 +4137,7 @@ pub(crate) mod tests { let thread = cx.new(|cx| { AcpThread::new( None, - "CwdCapturingConnection", + None, Some(work_dirs), self.clone(), project, @@ -4167,7 +4172,7 @@ pub(crate) mod tests { let thread = cx.new(|cx| { AcpThread::new( None, - "CwdCapturingConnection", + None, Some(work_dirs), self.clone(), project, @@ -6109,7 +6114,7 @@ pub(crate) mod tests { assert_eq!(editor.text(cx), "My Custom Title"); }); thread.read_with(cx, |thread, _cx| { - assert_eq!(thread.title().as_ref(), "My Custom Title"); + assert_eq!(thread.title(), Some("My Custom Title".into())); }); } @@ -6195,7 +6200,7 @@ pub(crate) mod tests { cx.new(|cx| { AcpThread::new( parent_session_id, - "Test Thread", + None, None, connection, project, @@ -6703,7 +6708,7 @@ pub(crate) mod tests { let thread = cx.new(|cx| { AcpThread::new( None, - "CloseCapableConnection", + Some("CloseCapableConnection".into()), Some(work_dirs), self, project, diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 8c8157a834cee5481013246ae4c71f84ae77f04c..cafc2b9f369e6854d887ff63073831e48bff6116 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -1,4 +1,4 @@ -use crate::SelectPermissionGranularity; +use crate::{DEFAULT_THREAD_TITLE, SelectPermissionGranularity}; use std::cell::RefCell; use acp_thread::ContentBlock; @@ -405,7 +405,11 @@ impl ThreadView { 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); + if let Some(title) = thread.read(cx).title() { + editor.set_text(title, window, cx); + } else { + editor.set_text(DEFAULT_THREAD_TITLE, window, cx); + } editor.set_read_only(!can_edit); editor }); @@ -1052,7 +1056,7 @@ impl ThreadView { .ok(); } }); - if is_first_message { + if is_first_message && thread.read_with(cx, |thread, _cx| thread.title().is_none())? { let text: String = contents .iter() .filter_map(|block| match block { @@ -1537,7 +1541,7 @@ impl ThreadView { EditorEvent::Blurred => { if title_editor.read(cx).text(cx).is_empty() { title_editor.update(cx, |editor, cx| { - editor.set_text("New Thread", window, cx); + editor.set_text(DEFAULT_THREAD_TITLE, window, cx); }); } } @@ -4656,7 +4660,10 @@ impl ThreadView { .language_for_name("Markdown"); let thread = self.thread.read(cx); - let thread_title = thread.title().to_string(); + let thread_title = thread + .title() + .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()) + .to_string(); let markdown = thread.to_markdown(cx); let project = workspace.read(cx).project().clone(); @@ -7068,7 +7075,7 @@ impl ThreadView { let thread_title = thread .as_ref() - .map(|t| t.read(cx).title()) + .and_then(|t| t.read(cx).title()) .filter(|t| !t.is_empty()); let tool_call_label = tool_call.label.read(cx).source().to_string(); let has_tool_call_label = !tool_call_label.is_empty(); diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 993d52640d1449b623dc47f2af4b3155d202448e..a4f444cfe7364dad64098eeb33b40078055a66d6 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1,3 +1,4 @@ +use crate::DEFAULT_THREAD_TITLE; use crate::SendImmediately; use crate::ThreadHistory; use crate::{ @@ -387,7 +388,7 @@ impl MessageEditor { }; let thread_title = title .filter(|title| !title.is_empty()) - .unwrap_or_else(|| SharedString::new_static("New Thread")); + .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE)); let uri = MentionUri::Thread { id: session_id, name: thread_title.to_string(), diff --git a/crates/agent_ui/src/thread_history_view.rs b/crates/agent_ui/src/thread_history_view.rs index bfb01d74d534934cbe731bd80403d04f4e454457..2def2c69d1bebdc150f15c93baa5f7f41bc188e2 100644 --- a/crates/agent_ui/src/thread_history_view.rs +++ b/crates/agent_ui/src/thread_history_view.rs @@ -1,5 +1,7 @@ use crate::thread_history::ThreadHistory; -use crate::{AgentPanel, ConversationView, RemoveHistory, RemoveSelectedThread}; +use crate::{ + AgentPanel, ConversationView, DEFAULT_THREAD_TITLE, RemoveHistory, RemoveSelectedThread, +}; use acp_thread::AgentSessionInfo; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc}; use editor::{Editor, EditorEvent}; @@ -16,14 +18,12 @@ use ui::{ WithScrollbar, prelude::*, }; -const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread"); - -pub(crate) fn thread_title(entry: &AgentSessionInfo) -> &SharedString { +pub(crate) fn thread_title(entry: &AgentSessionInfo) -> SharedString { entry .title - .as_ref() + .clone() .filter(|title| !title.is_empty()) - .unwrap_or(DEFAULT_TITLE) + .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()) } pub struct ThreadHistoryView { @@ -203,7 +203,7 @@ impl ThreadHistoryView { let mut candidates = Vec::with_capacity(entries.len()); for (idx, entry) in entries.iter().enumerate() { - candidates.push(StringMatchCandidate::new(idx, thread_title(entry))); + candidates.push(StringMatchCandidate::new(idx, &thread_title(entry))); } const MAX_MATCHES: usize = 100; @@ -429,7 +429,7 @@ impl ThreadHistoryView { (_, None) => "—".to_string(), }; - let title = thread_title(entry).clone(); + let title = thread_title(entry); let full_date = entry_time .map(|time| { EntryTimeFormat::DateAndTime.format_timestamp(time.timestamp(), self.local_timezone) @@ -678,7 +678,7 @@ impl HistoryEntryElement { impl RenderOnce for HistoryEntryElement { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let id = ElementId::Name(self.entry.session_id.0.clone().into()); - let title = thread_title(&self.entry).clone(); + let title = thread_title(&self.entry); let formatted_time = self .entry .updated_at diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index 339d3f48772cc21ad501ce3016919e96eb58ec6a..ee34305e946284629b72f979a3967956ffe0af96 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -21,6 +21,8 @@ use ui::{App, Context, SharedString}; use util::ResultExt as _; use workspace::PathList; +use crate::DEFAULT_THREAD_TITLE; + pub fn init(cx: &mut App) { SidebarThreadMetadataStore::init_global(cx); @@ -134,7 +136,9 @@ impl ThreadMetadata { pub fn from_thread(thread: &Entity, cx: &App) -> Self { let thread_ref = thread.read(cx); let session_id = thread_ref.session_id().clone(); - let title = thread_ref.title(); + let title = thread_ref + .title() + .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()); let updated_at = Utc::now(); let agent_id = thread_ref.connection().agent_id(); @@ -987,7 +991,7 @@ mod tests { cx.new(|cx| { acp_thread::AcpThread::new( Some(regular_session_id.clone()), - "Subagent Thread", + Some("Subagent Thread".into()), None, connection.clone(), project.clone(), diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 62c032406cdd9d741c58e480f97dbfe3e445781d..9b9a24b03f099d675cb35c440ff8b8afd98985ce 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -5,7 +5,9 @@ use agent_ui::thread_metadata_store::{SidebarThreadMetadataStore, ThreadMetadata use agent_ui::threads_archive_view::{ ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp, }; -use agent_ui::{Agent, AgentPanel, AgentPanelEvent, NewThread, RemoveSelectedThread}; +use agent_ui::{ + Agent, AgentPanel, AgentPanelEvent, DEFAULT_THREAD_TITLE, NewThread, RemoveSelectedThread, +}; use chrono::Utc; use editor::Editor; use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _}; @@ -559,7 +561,9 @@ impl Sidebar { let icon = thread_view_ref.agent_icon; let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone(); - let title = thread.title(); + let title = thread + .title() + .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()); let is_native = thread_view_ref.as_native_thread(cx).is_some(); let is_title_generating = is_native && thread.has_provisional_title(); let session_id = thread.session_id().clone(); @@ -2709,9 +2713,9 @@ impl Sidebar { let label: SharedString = if is_active { self.active_draft_text(cx) - .unwrap_or_else(|| "New Thread".into()) + .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into()) } else { - "New Thread".into() + DEFAULT_THREAD_TITLE.into() }; let workspace = workspace.clone(); @@ -5122,7 +5126,7 @@ mod tests { let connection_b2 = StubAgentConnection::new(); connection_b2.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("New thread".into()), + acp::ContentChunk::new(DEFAULT_THREAD_TITLE.into()), )]); open_thread_with_connection(&panel_b, connection_b2, cx); send_message(&panel_b, cx);