notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit f0ca3da5f46c4d9fab450f1d3d3a98523f219913
parent da48bf7b7ec3e34411a8e24569f259c9f7a7d3f4
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 26 Feb 2026 12:03:54 -0800

dave: show last activity timestamp in status bar

Display a relative timestamp ("just now", "3m ago", etc.) in the
status bar for agentic sessions so users can tell at a glance when
the last AI response token was received and detect stuck sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_dave/src/session.rs | 5+++++
Mcrates/notedeck_dave/src/ui/dave.rs | 38++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_dave/src/ui/mod.rs | 3++-
3 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::mpsc::Receiver; +use std::time::Instant; use crate::agent_status::AgentStatus; use crate::backend::BackendType; @@ -383,6 +384,8 @@ pub struct ChatSession { pub details: SessionDetails, /// Which backend this session uses (Claude, Codex, etc.) pub backend_type: BackendType, + /// When the last AI response token was received (for "5m ago" display) + pub last_activity: Option<Instant>, } impl Drop for ChatSession { @@ -428,6 +431,7 @@ impl ChatSession { .unwrap_or_default(), }, backend_type, + last_activity: None, } } @@ -905,6 +909,7 @@ impl ChatSession { pub fn append_token(&mut self, token: &str) { // Content arrived — transition AwaitingResponse → Streaming. self.dispatch_state.backend_responded(); + self.last_activity = Some(Instant::now()); // Fast path: last message is the active assistant response if let Some(Message::Assistant(msg)) = self.chat.last_mut() { diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -74,6 +74,8 @@ pub struct DaveUi<'a> { backend_type: BackendType, /// Current permission mode (Default, Plan, AcceptEdits) permission_mode: PermissionMode, + /// When the last AI response token was received + last_activity: Option<std::time::Instant>, } /// The response the app generates. The response contains an optional @@ -191,9 +193,15 @@ impl<'a> DaveUi<'a> { dispatch_state: crate::session::DispatchState::default(), backend_type: BackendType::Remote, permission_mode: PermissionMode::Default, + last_activity: None, } } + pub fn last_activity(mut self, instant: Option<std::time::Instant>) -> Self { + self.last_activity = instant; + self + } + pub fn backend_type(mut self, bt: BackendType) -> Self { self.backend_type = bt; self @@ -384,6 +392,7 @@ impl<'a> DaveUi<'a> { auto_steal_focus, self.usage, self.context_window, + self.last_activity, ui, ) }) @@ -1410,6 +1419,20 @@ fn add_msg_link(ui: &mut egui::Ui, shift_held: bool, action: &mut Option<DaveAct } } +/// Format an Instant as a relative time string (e.g. "just now", "3m ago"). +fn format_relative_time(instant: std::time::Instant) -> String { + let elapsed = instant.elapsed().as_secs(); + if elapsed < 60 { + "just now".to_string() + } else if elapsed < 3600 { + format!("{}m ago", elapsed / 60) + } else if elapsed < 86400 { + format!("{}h ago", elapsed / 3600) + } else { + format!("{}d ago", elapsed / 86400) + } +} + /// Renders the status bar containing git status and toggle badges. fn status_bar_ui( mut git_status: Option<&mut GitStatusCache>, @@ -1418,6 +1441,7 @@ fn status_bar_ui( auto_steal_focus: bool, usage: Option<&crate::messages::UsageInfo>, context_window: u64, + last_activity: Option<std::time::Instant>, ui: &mut egui::Ui, ) -> Option<DaveAction> { let snapshot = git_status @@ -1440,6 +1464,13 @@ fn status_bar_ui( None }; if is_agentic { + if let Some(instant) = last_activity { + ui.label( + egui::RichText::new(format_relative_time(instant)) + .size(10.0) + .color(ui.visuals().weak_text_color()), + ); + } usage_bar_ui(usage, context_window, ui); } badge_action @@ -1449,6 +1480,13 @@ fn status_bar_ui( // No git status (remote session) - just show badges and usage ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let badge_action = toggle_badges_ui(ui, permission_mode, auto_steal_focus); + if let Some(instant) = last_activity { + ui.label( + egui::RichText::new(format_relative_time(instant)) + .size(10.0) + .color(ui.visuals().weak_text_color()), + ); + } usage_bar_ui(usage, context_window, ui); badge_action }) diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -69,7 +69,8 @@ fn build_dave_ui<'a>( .is_remote(is_remote) .dispatch_state(session.dispatch_state) .details(&session.details) - .backend_type(session.backend_type); + .backend_type(session.backend_type) + .last_activity(session.last_activity); if let Some(agentic) = &mut session.agentic { let model = agentic