diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 58252eaddca553eb1da4c960a829a88afb9eb497..c98af5f2c7df2ef197cc0e7cb4bdbc517163249e 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1453,6 +1453,14 @@ impl AcpThread { /// (e.g. first 20 chars of the user message) that should be shown /// immediately but replaced once the LLM generates a proper title via /// `set_title`. + pub fn has_provisional_title(&self) -> bool { + self.provisional_title.is_some() + } + + pub fn provisional_title(&self) -> Option<&SharedString> { + self.provisional_title.as_ref() + } + pub fn set_provisional_title(&mut self, title: SharedString, cx: &mut Context) { self.provisional_title = Some(title); cx.emit(AcpThreadEvent::TitleUpdated); diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index a62e219b2d075e10e074b55859fc6c366c25523d..ba7eb0b259bb792f8d90bd6ac2a2321a90406a2c 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -332,6 +332,7 @@ impl NativeAgent { let parent_session_id = thread.parent_thread_id(); let title = thread.title(); let draft_prompt = thread.draft_prompt().map(Vec::from); + let provisional_title = thread.provisional_title().cloned(); let scroll_position = thread.ui_scroll_position(); let token_usage = thread.latest_token_usage(); let project = thread.project.clone(); @@ -355,6 +356,12 @@ impl NativeAgent { acp_thread }); + if let Some(provisional_title) = provisional_title { + acp_thread.update(cx, |acp_thread, cx| { + acp_thread.set_provisional_title(provisional_title, cx); + }); + } + let registry = LanguageModelRegistry::read_global(cx); let summarization_model = registry.thread_summary_model().map(|c| c.model); @@ -980,9 +987,11 @@ impl NativeAgent { ); let draft_prompt = session.acp_thread.read(cx).draft_prompt().map(Vec::from); + let provisional_title = session.acp_thread.read(cx).provisional_title().cloned(); let database_future = ThreadsDatabase::connect(cx); let db_thread = thread.update(cx, |thread, cx| { thread.set_draft_prompt(draft_prompt); + thread.set_provisional_title(provisional_title); thread.to_db(cx) }); let thread_store = self.thread_store.clone(); diff --git a/crates/agent/src/db.rs b/crates/agent/src/db.rs index 43ab9c3c1826ea7d81fed2c934b96f3bb05dd519..486c69f0cd47437ece5a910027438e9af48f158e 100644 --- a/crates/agent/src/db.rs +++ b/crates/agent/src/db.rs @@ -81,6 +81,8 @@ pub struct DbThread { #[serde(default)] pub draft_prompt: Option>, #[serde(default)] + pub provisional_title: Option, + #[serde(default)] pub ui_scroll_position: Option, } @@ -130,6 +132,7 @@ impl SharedThread { thinking_enabled: false, thinking_effort: None, draft_prompt: None, + provisional_title: None, ui_scroll_position: None, } } @@ -309,6 +312,7 @@ impl DbThread { thinking_enabled: false, thinking_effort: None, draft_prompt: None, + provisional_title: None, ui_scroll_position: None, }) } @@ -694,6 +698,7 @@ mod tests { thinking_enabled: false, thinking_effort: None, draft_prompt: None, + provisional_title: None, ui_scroll_position: None, } } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 02ffac47f120ee3ec4694b3a3be085af053c5909..a18402a03c9307061fdfe006b11a3bddc8696c1e 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -920,6 +920,7 @@ pub struct Thread { subagent_context: Option, /// The user's unsent prompt text, persisted so it can be restored when reloading the thread. draft_prompt: Option>, + provisional_title: Option, ui_scroll_position: Option, /// Weak references to running subagent threads for cancellation propagation running_subagents: Vec>, @@ -1036,6 +1037,7 @@ impl Thread { imported: false, subagent_context: None, draft_prompt: None, + provisional_title: None, ui_scroll_position: None, running_subagents: Vec::new(), } @@ -1252,6 +1254,7 @@ impl Thread { imported: db_thread.imported, subagent_context: db_thread.subagent_context, draft_prompt: db_thread.draft_prompt, + provisional_title: db_thread.provisional_title.clone(), ui_scroll_position: db_thread.ui_scroll_position.map(|sp| gpui::ListOffset { item_ix: sp.item_ix, offset_in_item: gpui::px(sp.offset_in_item), @@ -1263,7 +1266,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.clone().unwrap_or_default(), messages: self.messages.clone(), updated_at: self.updated_at, detailed_summary: self.summary.clone(), @@ -1281,6 +1284,7 @@ impl Thread { thinking_enabled: self.thinking_enabled, thinking_effort: self.thinking_effort.clone(), draft_prompt: self.draft_prompt.clone(), + provisional_title: self.provisional_title.clone(), ui_scroll_position: self.ui_scroll_position.map(|lo| { crate::db::SerializedScrollPosition { item_ix: lo.item_ix, @@ -1336,6 +1340,14 @@ impl Thread { self.draft_prompt = prompt; } + pub fn provisional_title(&self) -> Option<&SharedString> { + self.provisional_title.as_ref() + } + + pub fn set_provisional_title(&mut self, title: Option) { + self.provisional_title = title; + } + pub fn ui_scroll_position(&self) -> Option { self.ui_scroll_position } diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index dd1f650de2f59a0e681e15e7eae3fad1a49ccc41..f093cb00e68f60d554692e9be50aaeb0e2131923 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -162,6 +162,7 @@ mod tests { thinking_enabled: false, thinking_effort: None, draft_prompt: None, + provisional_title: None, ui_scroll_position: None, } } diff --git a/crates/agent_ui/src/connection_view/thread_view.rs b/crates/agent_ui/src/connection_view/thread_view.rs index 806b2c9c397de1c729164b5f859ceae4b7f6231f..87b981753c6cea339c770dea0ca8a38c658a51e7 100644 --- a/crates/agent_ui/src/connection_view/thread_view.rs +++ b/crates/agent_ui/src/connection_view/thread_view.rs @@ -1010,7 +1010,7 @@ impl ThreadView { .join(" "); let text = text.lines().next().unwrap_or("").trim(); if !text.is_empty() { - let title: SharedString = util::truncate_and_trailoff(text, 20).into(); + let title: SharedString = text.chars().take(100).collect::().into(); thread.update(cx, |thread, cx| { thread.set_provisional_title(title, cx); })?; diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index d5cf352665a8cd59bdd6a6b601248bce4a214e3b..e8a131f037648531d73c05ca95aab36fd050d74e 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -50,6 +50,7 @@ const DEFAULT_THREADS_SHOWN: usize = 5; struct ActiveThreadInfo { session_id: acp::SessionId, title: SharedString, + is_provisional: bool, status: AgentThreadStatus, icon: IconName, icon_from_external_svg: Option, @@ -78,6 +79,7 @@ struct ThreadEntry { workspace: Entity, is_live: bool, is_background: bool, + is_provisional: bool, highlight_positions: Vec, } @@ -388,6 +390,7 @@ impl Sidebar { let title = thread.title(); let session_id = thread.session_id().clone(); let is_background = agent_panel_ref.is_background_thread(&session_id); + let is_provisional = thread.has_provisional_title(); let status = if thread.is_waiting_for_confirmation() { AgentThreadStatus::WaitingForConfirmation @@ -403,6 +406,7 @@ impl Sidebar { ActiveThreadInfo { session_id, title, + is_provisional, status, icon, icon_from_external_svg, @@ -463,6 +467,7 @@ impl Sidebar { workspace: workspace.clone(), is_live: false, is_background: false, + is_provisional: false, highlight_positions: Vec::new(), }); } @@ -489,6 +494,7 @@ impl Sidebar { thread.icon_from_external_svg = info.icon_from_external_svg.clone(); thread.is_live = true; thread.is_background = info.is_background; + thread.is_provisional = info.is_provisional; } } @@ -1173,6 +1179,7 @@ impl Sidebar { .when_some(thread.icon_from_external_svg.clone(), |this, svg| { this.custom_icon_from_external_svg(svg) }) + .provisional(thread.is_provisional) .highlight_positions(thread.highlight_positions.to_vec()) .status(thread.status) .notified(has_notification) @@ -1562,6 +1569,7 @@ mod tests { thinking_effort: None, draft_prompt: None, ui_scroll_position: None, + provisional_title: None, } } @@ -2032,6 +2040,7 @@ mod tests { workspace: workspace.clone(), is_live: false, is_background: false, + is_provisional: false, highlight_positions: Vec::new(), }), // Active thread with Running status @@ -2050,6 +2059,7 @@ mod tests { workspace: workspace.clone(), is_live: true, is_background: false, + is_provisional: false, highlight_positions: Vec::new(), }), // Active thread with Error status @@ -2068,6 +2078,7 @@ mod tests { workspace: workspace.clone(), is_live: true, is_background: false, + is_provisional: false, highlight_positions: Vec::new(), }), // Thread with WaitingForConfirmation status, not active @@ -2086,6 +2097,7 @@ mod tests { workspace: workspace.clone(), is_live: false, is_background: false, + is_provisional: false, highlight_positions: Vec::new(), }), // Background thread that completed (should show notification) @@ -2104,6 +2116,7 @@ mod tests { workspace: workspace.clone(), is_live: true, is_background: true, + is_provisional: false, highlight_positions: Vec::new(), }), // View More entry diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs index 3c08bd946710f76ccf49f933b82091a3bcb06e08..6f980165384cd947c510b04c4f44f502b9eba8f2 100644 --- a/crates/ui/src/components/ai/thread_item.rs +++ b/crates/ui/src/components/ai/thread_item.rs @@ -29,6 +29,7 @@ pub struct ThreadItem { added: Option, removed: Option, worktree: Option, + provisional: bool, highlight_positions: Vec, worktree_highlight_positions: Vec, on_click: Option>, @@ -50,6 +51,7 @@ impl ThreadItem { selected: false, focused: false, hovered: false, + provisional: false, added: None, removed: None, worktree: None, @@ -127,6 +129,11 @@ impl ThreadItem { self } + pub fn provisional(mut self, provisional: bool) -> Self { + self.provisional = provisional; + self + } + pub fn on_click( mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, @@ -209,9 +216,13 @@ impl RenderOnce for ThreadItem { let title = self.title; let highlight_positions = self.highlight_positions; let title_label = if highlight_positions.is_empty() { - Label::new(title).into_any_element() + Label::new(title) + .when(self.provisional, |label| label.italic()) + .into_any_element() } else { - HighlightedLabel::new(title, highlight_positions).into_any_element() + HighlightedLabel::new(title, highlight_positions) + .when(self.provisional, |label| label.italic()) + .into_any_element() }; let base_bg = if self.selected { diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index ead16b911e3ccf9ebd1b9f54113cb01dca849e9d..96bcd504792fc9f663f5bdd0cec74173328d6ad3 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -2719,6 +2719,7 @@ fn run_multi_workspace_sidebar_visual_tests( thinking_effort: None, ui_scroll_position: None, draft_prompt: None, + provisional_title: None, }, path_list, cx,