agent_ui: Add timestamp to thread item in the sidebar (#51327)

Danilo Leal created

Release Notes:

- N/A

Change summary

crates/agent_ui/src/sidebar.rs             |  26 ++++
crates/ui/src/components/ai/thread_item.rs | 140 ++++++++++++++++++++---
2 files changed, 143 insertions(+), 23 deletions(-)

Detailed changes

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)

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(