Detailed changes
@@ -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>) {
self.provisional_title = Some(title);
cx.emit(AcpThreadEvent::TitleUpdated);
@@ -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();
@@ -81,6 +81,8 @@ pub struct DbThread {
#[serde(default)]
pub draft_prompt: Option<Vec<acp::ContentBlock>>,
#[serde(default)]
+ pub provisional_title: Option<SharedString>,
+ #[serde(default)]
pub ui_scroll_position: Option<SerializedScrollPosition>,
}
@@ -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,
}
}
@@ -920,6 +920,7 @@ pub struct Thread {
subagent_context: Option<SubagentContext>,
/// The user's unsent prompt text, persisted so it can be restored when reloading the thread.
draft_prompt: Option<Vec<acp::ContentBlock>>,
+ provisional_title: Option<SharedString>,
ui_scroll_position: Option<gpui::ListOffset>,
/// Weak references to running subagent threads for cancellation propagation
running_subagents: Vec<WeakEntity<Thread>>,
@@ -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<DbThread> {
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<SharedString>) {
+ self.provisional_title = title;
+ }
+
pub fn ui_scroll_position(&self) -> Option<gpui::ListOffset> {
self.ui_scroll_position
}
@@ -162,6 +162,7 @@ mod tests {
thinking_enabled: false,
thinking_effort: None,
draft_prompt: None,
+ provisional_title: None,
ui_scroll_position: None,
}
}
@@ -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::<String>().into();
thread.update(cx, |thread, cx| {
thread.set_provisional_title(title, cx);
})?;
@@ -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<SharedString>,
@@ -78,6 +79,7 @@ struct ThreadEntry {
workspace: Entity<Workspace>,
is_live: bool,
is_background: bool,
+ is_provisional: bool,
highlight_positions: Vec<usize>,
}
@@ -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
@@ -29,6 +29,7 @@ pub struct ThreadItem {
added: Option<usize>,
removed: Option<usize>,
worktree: Option<SharedString>,
+ provisional: bool,
highlight_positions: Vec<usize>,
worktree_highlight_positions: Vec<usize>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
@@ -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 {
@@ -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,