From 3dff4c57877c8a1b6bc2f6e2444b3b58ab9e637d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:15:43 -0300 Subject: [PATCH] agent_ui: Add timestamp to thread item in the sidebar (#51327) Release Notes: - N/A --- crates/agent_ui/src/sidebar.rs | 26 ++++ crates/ui/src/components/ai/thread_item.rs | 140 +++++++++++++++++---- 2 files changed, 143 insertions(+), 23 deletions(-) diff --git a/crates/agent_ui/src/sidebar.rs b/crates/agent_ui/src/sidebar.rs index 595366dd0484254ed641e69713b519199547e8e3..3804e3f63678bcf771b27b2f05929a958531ab39 100644 --- a/crates/agent_ui/src/sidebar.rs +++ b/crates/agent_ui/src/sidebar.rs @@ -1180,11 +1180,37 @@ impl Sidebar { let id = SharedString::from(format!("thread-entry-{}", ix)); + let timestamp = thread + .session_info + .created_at + .or(thread.session_info.updated_at) + .map(|entry_time| { + let now = Utc::now(); + let duration = now.signed_duration_since(entry_time); + + let minutes = duration.num_minutes(); + let hours = duration.num_hours(); + let days = duration.num_days(); + let weeks = days / 7; + let months = days / 30; + + if minutes < 60 { + format!("{}m", minutes.max(1)) + } else if hours < 24 { + format!("{}h", hours) + } else if weeks < 4 { + format!("{}w", weeks.max(1)) + } else { + format!("{}mo", months.max(1)) + } + }); + ThreadItem::new(id, title) .icon(thread.icon) .when_some(thread.icon_from_external_svg.clone(), |this, svg| { this.custom_icon_from_external_svg(svg) }) + .when_some(timestamp, |this, ts| this.timestamp(ts)) .highlight_positions(thread.highlight_positions.to_vec()) .status(thread.status) .notified(has_notification) diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs index 1ab516b0cbbcb20c98bf61525779d2bd760ef260..5be91e9d98a1219dcfbbba70a5541ba7b827cfc5 100644 --- a/crates/ui/src/components/ai/thread_item.rs +++ b/crates/ui/src/components/ai/thread_item.rs @@ -245,6 +245,8 @@ impl RenderOnce for ThreadItem { let removed_count = self.removed.unwrap_or(0); let diff_stat_id = self.id.clone(); let has_worktree = self.worktree.is_some(); + let has_timestamp = !self.timestamp.is_empty(); + let timestamp = self.timestamp; v_flex() .id(self.id.clone()) @@ -253,13 +255,7 @@ impl RenderOnce for ThreadItem { .overflow_hidden() .cursor_pointer() .w_full() - .map(|this| { - if has_worktree || has_diff_stats { - this.p_2() - } else { - this.p_1() - } - }) + .p_1() .when(self.selected, |s| s.bg(color.element_active)) .border_1() .border_color(gpui::transparent_black()) @@ -310,23 +306,47 @@ impl RenderOnce for ThreadItem { .gap_1p5() .child(icon_container()) // Icon Spacing .child(worktree_label) - .child(dot_separator()) + .when(has_diff_stats || has_timestamp, |this| { + this.child(dot_separator()) + }) .when(has_diff_stats, |this| { this.child(DiffStat::new( diff_stat_id.clone(), added_count, removed_count, )) + }) + .when(has_diff_stats && has_timestamp, |this| { + this.child(dot_separator()) + }) + .when(has_timestamp, |this| { + this.child( + Label::new(timestamp.clone()) + .size(LabelSize::Small) + .color(Color::Muted), + ) }), ) }) - .when(!has_worktree && has_diff_stats, |this| { + .when(!has_worktree && (has_diff_stats || has_timestamp), |this| { this.child( h_flex() .min_w_0() .gap_1p5() .child(icon_container()) // Icon Spacing - .child(DiffStat::new(diff_stat_id, added_count, removed_count)), + .when(has_diff_stats, |this| { + this.child(DiffStat::new(diff_stat_id, added_count, removed_count)) + }) + .when(has_diff_stats && has_timestamp, |this| { + this.child(dot_separator()) + }) + .when(has_timestamp, |this| { + this.child( + Label::new(timestamp.clone()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }), ) }) .when_some(self.on_click, |this, on_click| this.on_click(on_click)) @@ -349,21 +369,31 @@ impl Component for ThreadItem { let thread_item_examples = vec![ single_example( - "Default", + "Default (minutes)", container() .child( ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings") .icon(IconName::AiOpenAi) - .timestamp("1:33 AM"), + .timestamp("15m"), + ) + .into_any_element(), + ), + single_example( + "Timestamp Only (hours)", + container() + .child( + ThreadItem::new("ti-1b", "Thread with just a timestamp") + .icon(IconName::AiClaude) + .timestamp("3h"), ) .into_any_element(), ), single_example( - "Notified", + "Notified (weeks)", container() .child( ThreadItem::new("ti-2", "Refine thread view scrolling behavior") - .timestamp("12:12 AM") + .timestamp("1w") .notified(true), ) .into_any_element(), @@ -373,7 +403,7 @@ impl Component for ThreadItem { container() .child( ThreadItem::new("ti-2b", "Execute shell command in terminal") - .timestamp("12:15 AM") + .timestamp("2h") .status(AgentThreadStatus::WaitingForConfirmation), ) .into_any_element(), @@ -383,7 +413,7 @@ impl Component for ThreadItem { container() .child( ThreadItem::new("ti-2c", "Failed to connect to language server") - .timestamp("12:20 AM") + .timestamp("5h") .status(AgentThreadStatus::Error), ) .into_any_element(), @@ -394,7 +424,7 @@ impl Component for ThreadItem { .child( ThreadItem::new("ti-3", "Add line numbers option to FileEditBlock") .icon(IconName::AiClaude) - .timestamp("7:30 PM") + .timestamp("23h") .status(AgentThreadStatus::Running), ) .into_any_element(), @@ -405,30 +435,43 @@ impl Component for ThreadItem { .child( ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock") .icon(IconName::AiClaude) - .timestamp("7:37 PM") + .timestamp("2w") .worktree("link-agent-panel"), ) .into_any_element(), ), single_example( - "With Changes", + "With Changes (months)", container() .child( ThreadItem::new("ti-5", "Managing user and project settings interactions") .icon(IconName::AiClaude) - .timestamp("7:37 PM") + .timestamp("1mo") .added(10) .removed(3), ) .into_any_element(), ), + single_example( + "Worktree + Changes + Timestamp", + container() + .child( + ThreadItem::new("ti-5b", "Full metadata example") + .icon(IconName::AiClaude) + .worktree("my-project") + .added(42) + .removed(17) + .timestamp("3w"), + ) + .into_any_element(), + ), single_example( "Selected Item", container() .child( ThreadItem::new("ti-6", "Refine textarea interaction behavior") .icon(IconName::AiGemini) - .timestamp("3:00 PM") + .timestamp("45m") .selected(true), ) .into_any_element(), @@ -439,23 +482,74 @@ impl Component for ThreadItem { .child( ThreadItem::new("ti-7", "Implement keyboard navigation") .icon(IconName::AiClaude) - .timestamp("4:00 PM") + .timestamp("12h") .focused(true), ) .into_any_element(), ), + single_example( + "Focused + Docked Right", + container() + .child( + ThreadItem::new("ti-7b", "Focused with right dock border") + .icon(IconName::AiClaude) + .timestamp("1w") + .focused(true) + .docked_right(true), + ) + .into_any_element(), + ), single_example( "Selected + Focused", container() .child( ThreadItem::new("ti-8", "Active and keyboard-focused thread") .icon(IconName::AiGemini) - .timestamp("5:00 PM") + .timestamp("2mo") .selected(true) .focused(true), ) .into_any_element(), ), + single_example( + "Hovered with Action Slot", + container() + .child( + ThreadItem::new("ti-9", "Hover to see action button") + .icon(IconName::AiClaude) + .timestamp("6h") + .hovered(true) + .action_slot( + IconButton::new("delete", IconName::Trash) + .icon_size(IconSize::Small) + .icon_color(Color::Muted), + ), + ) + .into_any_element(), + ), + single_example( + "Search Highlight", + container() + .child( + ThreadItem::new("ti-10", "Implement keyboard navigation") + .icon(IconName::AiClaude) + .timestamp("4w") + .highlight_positions(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), + ) + .into_any_element(), + ), + single_example( + "Worktree Search Highlight", + container() + .child( + ThreadItem::new("ti-11", "Search in worktree name") + .icon(IconName::AiClaude) + .timestamp("3mo") + .worktree("my-project-name") + .worktree_highlight_positions(vec![3, 4, 5, 6, 7, 8, 9, 10, 11]), + ) + .into_any_element(), + ), ]; Some(