Rename agent threads from the sidebar (#57656)

Summary:

- Added a rename action for agent threads in the sidebar.
- Persisted renamed thread titles and kept open thread views in sync.

Release Notes:

- Improved agent threads by allowing them to be renamed directly from
the sidebar.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
authored and GitHub committed 2026-05-26 17:55:09 +00:00
1 parent 169b07f7ea
commit 031085584f
9 files changed
+503 -55

No files matched your search

+1
View File
@@ -729,6 +729,7 @@
"use_key_equivalents": true,
"bindings": {
"space": "menu::Confirm",
"shift-r": "agent::RenameSelectedThread",
},
},
{
+1
View File
@@ -785,6 +785,7 @@
"use_key_equivalents": true,
"bindings": {
"space": "menu::Confirm",
"shift-r": "agent::RenameSelectedThread",
},
},
{
+1
View File
@@ -732,6 +732,7 @@
"use_key_equivalents": true,
"bindings": {
"space": "menu::Confirm",
"shift-r": "agent::RenameSelectedThread",
},
},
{
+16
View File
@@ -3646,6 +3646,22 @@ impl AgentPanel {
}
}
pub fn conversation_view_for_id(
&self,
thread_id: &ThreadId,
cx: &App,
) -> Option<&Entity<ConversationView>> {
self.retained_threads.get(thread_id).or_else(|| {
if let Some(view) = self.active_conversation_view()
&& view.read(cx).thread_id == *thread_id
{
Some(view)
} else {
None
}
})
}
pub fn conversation_views(&self) -> Vec<Entity<ConversationView>> {
self.active_conversation_view()
.into_iter()
+2
View File
@@ -199,6 +199,8 @@ actions!(
ArchiveSelectedThread,
/// Removes the currently selected thread.
RemoveSelectedThread,
/// Renames the currently selected thread.
RenameSelectedThread,
/// Starts a chat conversation with follow-up enabled.
ChatWithFollow,
/// Cycles to the next inline assist suggestion.
@@ -2008,8 +2008,6 @@ impl ThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) {
let thread = &self.thread;
match event {
EditorEvent::BufferEdited => {
// We only want to set the title if the user has actively edited
@@ -2023,20 +2021,7 @@ impl ThreadView {
if new_title.is_empty() {
return;
}
let title = SharedString::from(new_title);
if let Some(store) = ThreadMetadataStore::try_global(cx)
&& !self.is_subagent()
{
let thread_id = self.root_thread_id;
store.update(cx, |store, cx| {
store.set_title_override(thread_id, title.clone(), cx);
});
}
thread.update(cx, |thread, cx| {
if thread.can_set_title(cx) {
thread.set_title(title, cx).detach_and_log_err(cx);
}
});
self.apply_renamed_title(SharedString::from(new_title), cx);
}
EditorEvent::Blurred => {
if title_editor.read(cx).text(cx).is_empty() {
@@ -2049,6 +2034,35 @@ impl ThreadView {
}
}
/// Renames the thread, mirroring the editor text and persisting the new
/// title. Used by callers outside of the title editor (e.g. the sidebar's
/// inline rename) so that they go through the same persistence path as
/// the in-thread title editor.
pub fn rename(&mut self, title: SharedString, window: &mut Window, cx: &mut Context<Self>) {
if self.title_editor.read(cx).text(cx) != title.as_ref() {
self.title_editor.update(cx, |editor, cx| {
editor.set_text(title.clone(), window, cx);
});
}
self.apply_renamed_title(title, cx);
}
fn apply_renamed_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
if let Some(store) = ThreadMetadataStore::try_global(cx)
&& !self.is_subagent()
{
let thread_id = self.root_thread_id;
store.update(cx, |store, cx| {
store.set_title_override(thread_id, title.clone(), cx);
});
}
self.thread.update(cx, |thread, cx| {
if thread.can_set_title(cx) {
thread.set_title(title, cx).detach_and_log_err(cx);
}
});
}
pub fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
if let Some(index) = self.editing_message.take()
&& let Some(editor) = &self
+236 -37
View File
@@ -18,8 +18,8 @@ use agent_ui::threads_archive_view::{
use agent_ui::{
AcpThreadImportOnboarding, Agent, AgentPanel, AgentPanelEvent, AgentThreadSource,
ArchiveSelectedThread, CrossChannelImportOnboarding, DEFAULT_THREAD_TITLE, NewTerminalThread,
NewThread, TerminalId, ThreadId, ThreadImportModal, channels_with_threads,
import_threads_from_other_channels,
NewThread, RenameSelectedThread, TerminalId, ThreadId, ThreadImportModal,
channels_with_threads, import_threads_from_other_channels,
};
use chrono::{DateTime, Utc};
use editor::Editor;
@@ -654,6 +654,7 @@ pub struct Sidebar {
width: Pixels,
focus_handle: FocusHandle,
filter_editor: Entity<Editor>,
thread_rename_editor: Entity<Editor>,
list_state: ListState,
contents: SidebarContents,
/// The index of the list item that currently has the keyboard focus
@@ -663,6 +664,10 @@ pub struct Sidebar {
/// Tracks which sidebar entry is currently active (highlighted).
active_entry: Option<ActiveEntry>,
hovered_thread_index: Option<usize>,
renaming_thread_id: Option<ThreadId>,
/// start_renaming_thread must seed current title into the title editor
/// so this prevents that BufferEdited event from being interpreted as user input.
suppress_next_rename_edit: bool,
/// Updated only in response to explicit user actions (clicking a
/// thread, confirming in the thread switcher, etc.) — never from
@@ -708,6 +713,7 @@ impl Sidebar {
editor.set_placeholder_text("Search threads…", window, cx);
editor
});
let thread_rename_editor = cx.new(|cx| Editor::single_line(window, cx));
cx.subscribe_in(
&multi_workspace,
@@ -744,6 +750,15 @@ impl Sidebar {
})
.detach();
cx.subscribe_in(
&thread_rename_editor,
window,
|this, title_editor, event, window, cx| {
this.handle_thread_rename_editor_event(title_editor, event, window, cx);
},
)
.detach();
cx.observe(&ThreadMetadataStore::global(cx), |this, _store, cx| {
this.update_entries(cx);
})
@@ -773,11 +788,14 @@ impl Sidebar {
width: DEFAULT_WIDTH,
focus_handle,
filter_editor,
thread_rename_editor,
list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
contents: SidebarContents::default(),
selection: None,
active_entry: None,
hovered_thread_index: None,
renaming_thread_id: None,
suppress_next_rename_edit: false,
thread_last_accessed: HashMap::new(),
terminal_last_accessed: HashMap::new(),
@@ -1692,12 +1710,8 @@ impl Sidebar {
let mut matched_threads: Vec<ThreadEntry> = Vec::new();
for mut thread in threads {
let title: &str = thread
.metadata
.title
.as_ref()
.map_or(DEFAULT_THREAD_TITLE, |t| t.as_ref());
if let Some(positions) = fuzzy_match_positions(&query, title) {
let title = thread.metadata.display_title();
if let Some(positions) = fuzzy_match_positions(&query, title.as_ref()) {
thread.highlight_positions = positions;
}
let mut worktree_matched = false;
@@ -2800,10 +2814,17 @@ impl Sidebar {
let is_archived_search_focused = matches!(&self.view, SidebarView::Archive(archive) if archive.read(cx).is_filter_editor_focused(window, cx));
let is_renaming_thread = self
.thread_rename_editor
.focus_handle(cx)
.is_focused(window);
let identifier = if self.filter_editor.focus_handle(cx).is_focused(window)
|| is_archived_search_focused
{
"searching"
} else if is_renaming_thread {
"editing"
} else {
"not_searching"
};
@@ -2828,6 +2849,11 @@ impl Sidebar {
}
fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
if self.renaming_thread_id.is_some() {
self.finish_thread_rename(window, cx);
return;
}
if self.filter_editor.read(cx).is_focused(window) {
if self.reset_filter_editor_text(window, cx) {
self.selection = None;
@@ -2888,6 +2914,104 @@ impl Sidebar {
!self.filter_editor.read(cx).text(cx).is_empty()
}
fn start_renaming_thread(
&mut self,
ix: usize,
thread_id: ThreadId,
title: SharedString,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.renaming_thread_id.is_some() && self.renaming_thread_id != Some(thread_id) {
self.finish_thread_rename(window, cx);
}
self.selection = Some(ix);
self.renaming_thread_id = Some(thread_id);
self.suppress_next_rename_edit = true;
self.list_state.scroll_to_reveal_item(ix);
self.thread_rename_editor.update(cx, |editor, cx| {
editor.set_text(title, window, cx);
editor.select_all(&editor::actions::SelectAll, window, cx);
editor.focus_handle(cx).focus(window, cx);
});
cx.notify();
}
fn handle_thread_rename_editor_event(
&mut self,
title_editor: &Entity<Editor>,
event: &editor::EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
editor::EditorEvent::BufferEdited => {
if self.suppress_next_rename_edit {
self.suppress_next_rename_edit = false;
return;
}
if !title_editor.read(cx).is_focused(window) {
return;
}
let new_title = title_editor.read(cx).text(cx);
if new_title.is_empty() {
return;
}
let Some(thread_id) = self.renaming_thread_id else {
return;
};
self.apply_thread_rename(thread_id, SharedString::from(new_title), window, cx);
}
editor::EditorEvent::Blurred => {
self.finish_thread_rename(window, cx);
}
_ => {}
}
}
fn apply_thread_rename(
&mut self,
thread_id: ThreadId,
title: SharedString,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut found = false;
if let Some(multi_workspace) = self.multi_workspace.upgrade() {
let workspaces: Vec<_> = multi_workspace.read(cx).workspaces().cloned().collect();
for workspace in workspaces {
if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
if let Some(view) = agent_panel
.read(cx)
.conversation_view_for_id(&thread_id, cx)
&& let Some(thread_view) = view.read(cx).root_thread_view()
{
thread_view.update(cx, |thread_view, cx| {
thread_view.rename(title.clone(), window, cx);
});
found = true;
}
}
}
}
if !found {
ThreadMetadataStore::global(cx).update(cx, |store, cx| {
store.set_title_override(thread_id, title, cx);
});
}
}
fn finish_thread_rename(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if self.renaming_thread_id.take().is_none() {
return false;
}
self.focus_handle.focus(window, cx);
self.update_entries(cx);
true
}
fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
self.select_next(&SelectNext, window, cx);
if self.selection.is_some() {
@@ -2962,6 +3086,10 @@ impl Sidebar {
}
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
if self.finish_thread_rename(window, cx) {
return;
}
let Some(ix) = self.selection else { return };
let Some(entry) = self.contents.entries.get(ix) else {
return;
@@ -5062,6 +5190,23 @@ impl Sidebar {
}
}
fn rename_selected_thread(
&mut self,
_: &RenameSelectedThread,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(ix) = self.selection else {
return;
};
let Some(ListEntry::Thread(thread)) = self.contents.entries.get(ix) else {
return;
};
let thread_id = thread.metadata.thread_id;
let title = thread.metadata.display_title();
self.start_renaming_thread(ix, thread_id, title, window, cx);
}
fn record_thread_access(&mut self, id: &ThreadId) {
self.thread_last_accessed.insert(*id, Utc::now());
}
@@ -5492,10 +5637,12 @@ impl Sidebar {
thread.status,
AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation
);
let is_renaming = self.renaming_thread_id == Some(thread.metadata.thread_id);
let thread_id_for_actions = thread.metadata.thread_id;
let session_id_for_delete = thread.metadata.session_id.clone();
let focus_handle = self.focus_handle.clone();
let title_editor = self.thread_rename_editor.clone();
let id = SharedString::from(format!("thread-entry-{}", ix));
@@ -5519,7 +5666,7 @@ impl Sidebar {
(thread.icon, thread.icon_from_external_svg.clone())
};
ThreadItem::new(id, title)
ThreadItem::new(id, title.clone())
.base_bg(sidebar_bg)
.icon(icon)
.when(is_draft, |this| {
@@ -5552,22 +5699,84 @@ impl Sidebar {
}
cx.notify();
}))
.when(is_hovered && is_running, |this| {
this.action_slot(
.when(is_renaming, |this| {
this.is_truncated(false).title_slot(
div()
.h_full()
.min_w_0()
.flex_1()
.capture_action(cx.listener(
|this, _: &editor::actions::Newline, window, cx| {
this.finish_thread_rename(window, cx);
},
))
.on_action(cx.listener(|this, _: &Confirm, window, cx| {
this.finish_thread_rename(window, cx);
}))
.on_action(
cx.listener(|this, _: &editor::actions::Cancel, window, cx| {
this.finish_thread_rename(window, cx);
}),
)
.child(title_editor),
)
})
.when(is_hovered && !is_renaming, |this| {
let rename_button = IconButton::new(("rename-thread", ix), IconName::Pencil)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Rename Thread",
&RenameSelectedThread,
&focus_handle,
cx,
)
}
})
.on_click({
let title = title.clone();
cx.listener(move |this, _, window, cx| {
this.start_renaming_thread(
ix,
thread_id_for_actions,
title.clone(),
window,
cx,
);
})
});
let contextual_action = if is_running {
IconButton::new("stop-thread", IconName::Stop)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.style(ButtonStyle::Tinted(TintColor::Error))
.tooltip(Tooltip::text("Stop Generation"))
.on_click(cx.listener(move |this, _, _window, cx| {
this.stop_thread(&thread_id_for_actions, cx);
}))
.into_any_element()
} else if is_draft {
IconButton::new("discard_thread", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Discard Draft"))
.on_click({
cx.listener(move |this, _, _window, cx| {
this.stop_thread(&thread_id_for_actions, cx);
let thread_workspace = thread_workspace.clone();
cx.listener(move |this, _, window, cx| {
this.remove_draft(
thread_id_for_actions,
&thread_workspace,
window,
cx,
);
})
}),
)
})
.when(is_hovered && !is_running && !is_draft, |this| {
this.action_slot(
})
.into_any_element()
} else {
IconButton::new("archive-thread", IconName::Archive)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
@@ -5589,26 +5798,15 @@ impl Sidebar {
this.archive_thread(session_id, window, cx);
}
})
}),
)
})
.when(is_hovered && !is_running && is_draft, |this| {
})
.into_any_element()
};
this.action_slot(
IconButton::new("discard_thread", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Discard Draft"))
.on_click({
let thread_workspace = thread_workspace.clone();
cx.listener(move |this, _, window, cx| {
this.remove_draft(
thread_id_for_actions,
&thread_workspace,
window,
cx,
);
})
}),
h_flex()
.gap_0p5()
.child(rename_button)
.child(contextual_action),
)
})
.on_click({
@@ -7035,6 +7233,7 @@ impl Render for Sidebar {
.on_action(cx.listener(Self::unfold_all))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::archive_selected_thread))
.on_action(cx.listener(Self::rename_selected_thread))
.on_action(cx.listener(Self::new_thread_in_group))
.on_action(cx.listener(Self::new_terminal_thread))
.on_action(cx.listener(Self::toggle_archive))
+197
View File
@@ -4886,6 +4886,203 @@ async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext)
);
}
#[gpui::test]
async fn test_rename_thread_from_sidebar_updates_title_override(cx: &mut TestAppContext) {
let project = init_test_project_with_agent_panel("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Hi there!".into()),
)]);
open_thread_with_connection(&panel, connection, cx);
send_message(&panel, cx);
let session_id = active_session_id(&panel, cx);
save_test_thread_metadata(&session_id, &project, cx).await;
cx.run_until_parked();
let (entry_ix, thread_id, title) = sidebar.read_with(cx, |sidebar, _cx| {
sidebar
.contents
.entries
.iter()
.enumerate()
.find_map(|(ix, entry)| match entry {
ListEntry::Thread(thread) => Some((
ix,
thread.metadata.thread_id,
thread.metadata.display_title(),
)),
ListEntry::ProjectHeader { .. } | ListEntry::Terminal(_) => None,
})
.expect("sidebar should have a thread entry")
});
let renamed_title = "abcdefghijklmnopqrstuvwxyé renamed";
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.start_renaming_thread(entry_ix, thread_id, title, window, cx);
});
cx.run_until_parked();
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.thread_rename_editor.update(cx, |editor, cx| {
editor.set_text(renamed_title, window, cx);
});
});
cx.run_until_parked();
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.finish_thread_rename(window, cx);
});
cx.run_until_parked();
let metadata = cx.update(|_, cx| {
ThreadMetadataStore::global(cx)
.read(cx)
.entry(thread_id)
.cloned()
.expect("thread metadata should exist")
});
assert_eq!(metadata.title_override.as_deref(), Some(renamed_title));
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
//
"v [my-project]",
" abcdefghijklmnopqrstuvwxyé renamed * <== selected",
]
);
let active_thread = panel.read_with(cx, |panel, cx| panel.active_agent_thread(cx).unwrap());
assert_eq!(
active_thread.read_with(cx, |thread, _| thread.title()),
Some(renamed_title.into())
);
let active_thread_view = panel.read_with(cx, |panel, cx| panel.active_thread_view(cx).unwrap());
let title_editor_text =
active_thread_view.read_with(cx, |view, cx| view.title_editor.read(cx).text(cx));
assert_eq!(title_editor_text, renamed_title);
active_thread.update(cx, |thread, cx| {
thread
.set_title("abcdefghijklmnopqrstuvwxyz0".into(), cx)
.detach();
});
cx.run_until_parked();
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
//
"v [my-project]",
" abcdefghijklmnopqrstuvwxyé renamed * <== selected",
]
);
type_in_search(&sidebar, "0", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
Vec::<String>::new()
);
type_in_search(&sidebar, "é", cx);
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec![
//
"v [my-project]",
" abcdefghijklmnopqrstuvwxyé renamed * <== selected",
]
);
sidebar.read_with(cx, |sidebar, _cx| {
let thread = sidebar
.contents
.entries
.iter()
.find_map(|entry| match entry {
ListEntry::Thread(thread) => Some(thread),
ListEntry::ProjectHeader { .. } | ListEntry::Terminal(_) => None,
})
.expect("renamed thread should match the search");
let title = thread.metadata.display_title();
assert!(
thread
.highlight_positions
.iter()
.all(|position| { title.is_char_boundary(*position) })
);
});
}
#[gpui::test]
async fn test_rename_selected_thread_action_renames_selected_thread(cx: &mut TestAppContext) {
let project = init_test_project_with_agent_panel("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Hi there!".into()),
)]);
open_thread_with_connection(&panel, connection, cx);
send_message(&panel, cx);
let session_id = active_session_id(&panel, cx);
save_test_thread_metadata(&session_id, &project, cx).await;
cx.run_until_parked();
let (entry_ix, thread_id) = sidebar.read_with(cx, |sidebar, _cx| {
sidebar
.contents
.entries
.iter()
.enumerate()
.find_map(|(ix, entry)| match entry {
ListEntry::Thread(thread) => Some((ix, thread.metadata.thread_id)),
ListEntry::ProjectHeader { .. } | ListEntry::Terminal(_) => None,
})
.expect("sidebar should have a thread entry")
});
focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(entry_ix);
});
cx.dispatch_action(RenameSelectedThread);
cx.run_until_parked();
sidebar.read_with(cx, |sidebar, _cx| {
assert_eq!(
sidebar.renaming_thread_id,
Some(thread_id),
"dispatching RenameSelectedThread should start renaming the selected thread"
);
});
let renamed_title = "Renamed via action";
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.thread_rename_editor.update(cx, |editor, cx| {
editor.set_text(renamed_title, window, cx);
});
});
cx.run_until_parked();
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.finish_thread_rename(window, cx);
});
cx.run_until_parked();
let metadata = cx.update(|_, cx| {
ThreadMetadataStore::global(cx)
.read(cx)
.entry(thread_id)
.cloned()
.expect("thread metadata should exist")
});
assert_eq!(metadata.title_override.as_deref(), Some(renamed_title));
}
#[gpui::test]
async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
+19 -2
View File
@@ -39,6 +39,7 @@ pub struct ThreadItem {
icon_visible: bool,
custom_icon_from_external_svg: Option<SharedString>,
title: SharedString,
title_slot: Option<AnyElement>,
title_label_color: Option<Color>,
title_generating: bool,
highlight_positions: Vec<usize>,
@@ -49,6 +50,7 @@ pub struct ThreadItem {
focused: bool,
hovered: bool,
rounded: bool,
is_truncated: bool,
added: Option<usize>,
removed: Option<usize>,
project_paths: Option<Arc<[PathBuf]>>,
@@ -71,6 +73,7 @@ impl ThreadItem {
icon_visible: true,
custom_icon_from_external_svg: None,
title: title.into(),
title_slot: None,
title_label_color: None,
title_generating: false,
highlight_positions: Vec::new(),
@@ -81,6 +84,7 @@ impl ThreadItem {
focused: false,
hovered: false,
rounded: false,
is_truncated: true,
added: None,
removed: None,
project_paths: None,
@@ -140,6 +144,11 @@ impl ThreadItem {
self
}
pub fn title_slot(mut self, element: impl IntoElement) -> Self {
self.title_slot = Some(element.into_any_element());
self
}
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
self.highlight_positions = positions;
self
@@ -200,6 +209,11 @@ impl ThreadItem {
self
}
pub fn is_truncated(mut self, is_truncated: bool) -> Self {
self.is_truncated = is_truncated;
self
}
pub fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
@@ -317,7 +331,9 @@ impl RenderOnce for ThreadItem {
let title = self.title;
let highlight_positions = self.highlight_positions;
let title_label = if self.title_generating {
let title_label = if let Some(title_slot) = self.title_slot {
title_slot
} else if self.title_generating {
Label::new(title)
.color(Color::Muted)
.with_animation(
@@ -403,6 +419,7 @@ impl RenderOnce for ThreadItem {
h_flex()
.min_w_0()
.w_full()
.h_6()
.gap_2()
.justify_between()
.child(
@@ -414,7 +431,7 @@ impl RenderOnce for ThreadItem {
.child(icon)
.child(title_label),
)
.child(gradient_overlay)
.when(self.is_truncated, |this| this.child(gradient_overlay))
.when(self.hovered, |this| {
this.when_some(self.action_slot, |this, slot| {
let overlay = GradientFade::new(base_bg, hover_bg, hover_bg)