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:
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