notedeck

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

commit e79bbd92ef7f51c858de6618c73bbbbdbda121c4
parent 2c6302fe32e61cb76a4ddf7f56b9dccdf7cca3bb
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 24 Jan 2026 16:39:12 -0800

dave: restyle session list sidebar with ChatGPT-like design

- Add darker background to sidebar using faint_bg_color
- Add padding (8px horizontal, 12px vertical) to sidebar frame
- Simplify session items to single-line (title only, no preview)
- Add hover highlight effect on session items
- Add pointer cursor on hover
- Remove separator between header and list
- Use manual painting for cleaner item rendering

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

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 13+++++++++++--
Mcrates/notedeck_dave/src/ui/session_list.rs | 94++++++++++++++++++++++---------------------------------------------------------
2 files changed, 37 insertions(+), 70 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -222,7 +222,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr // Render sidebar first - borrow released after this let session_action = ui .allocate_new_ui(egui::UiBuilder::new().max_rect(sidebar_rect), |ui| { - SessionListUi::new(&self.session_manager).ui(ui) + egui::Frame::new() + .fill(ui.visuals().faint_bg_color) + .inner_margin(egui::Margin::symmetric(8, 12)) + .show(ui, |ui| SessionListUi::new(&self.session_manager).ui(ui)) + .inner }) .inner; @@ -258,7 +262,12 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr fn narrow_ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse { if self.show_session_list { // Show session list - if let Some(action) = SessionListUi::new(&self.session_manager).ui(ui) { + let session_action = egui::Frame::new() + .fill(ui.visuals().faint_bg_color) + .inner_margin(egui::Margin::symmetric(8, 12)) + .show(ui, |ui| SessionListUi::new(&self.session_manager).ui(ui)) + .inner; + if let Some(action) = session_action { match action { SessionListAction::NewSession => { self.session_manager.new_session(); diff --git a/crates/notedeck_dave/src/ui/session_list.rs b/crates/notedeck_dave/src/ui/session_list.rs @@ -1,7 +1,6 @@ use egui::{Align, Layout, Sense}; use crate::session::{SessionId, SessionManager}; -use crate::Message; /// Actions that can be triggered from the session list UI #[derive(Debug, Clone)] @@ -29,8 +28,6 @@ impl<'a> SessionListUi<'a> { action = self.header_ui(ui); ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); // Scrollable list of sessions egui::ScrollArea::vertical() @@ -49,13 +46,13 @@ impl<'a> SessionListUi<'a> { let mut action = None; ui.horizontal(|ui| { - ui.add_space(12.0); - ui.heading("Chats"); + ui.add_space(4.0); + ui.label(egui::RichText::new("Chats").size(18.0).strong()); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add_space(12.0); + ui.add_space(4.0); if ui - .button("+") + .button(egui::RichText::new("+").size(18.0)) .on_hover_text("New Chat") .clicked() { @@ -74,7 +71,7 @@ impl<'a> SessionListUi<'a> { for session in self.session_manager.sessions_ordered() { let is_active = Some(session.id) == active_id; - let response = self.session_item_ui(ui, &session.title, Self::get_preview(&session.chat), is_active); + let response = self.session_item_ui(ui, &session.title, is_active); if response.clicked() { action = Some(SessionListAction::SwitchTo(session.id)); @@ -92,72 +89,33 @@ impl<'a> SessionListUi<'a> { action } - fn session_item_ui( - &self, - ui: &mut egui::Ui, - title: &str, - preview: Option<String>, - is_active: bool, - ) -> egui::Response { + fn session_item_ui(&self, ui: &mut egui::Ui, title: &str, is_active: bool) -> egui::Response { + let desired_size = egui::vec2(ui.available_width(), 36.0); + let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); + let response = response.on_hover_cursor(egui::CursorIcon::PointingHand); + + // Paint background: active > hovered > transparent let fill = if is_active { ui.visuals().widgets.active.bg_fill + } else if response.hovered() { + ui.visuals().widgets.hovered.weak_bg_fill } else { egui::Color32::TRANSPARENT }; - let frame = egui::Frame::new() - .fill(fill) - .inner_margin(egui::Margin::symmetric(12, 8)) - .corner_radius(8.0); - - frame - .show(ui, |ui| { - ui.set_width(ui.available_width()); - - ui.vertical(|ui| { - // Title - ui.add( - egui::Label::new( - egui::RichText::new(title) - .strong() - .size(14.0), - ) - .truncate(), - ); - - // Preview of last message - if let Some(preview) = preview { - ui.add( - egui::Label::new( - egui::RichText::new(preview) - .weak() - .size(12.0), - ) - .truncate(), - ); - } - }); - }) - .response - .interact(Sense::click()) - } + let corner_radius = 8.0; + ui.painter().rect_filled(rect, corner_radius, fill); - /// Get a preview string from the chat history - fn get_preview(chat: &[Message]) -> Option<String> { - // Find the last user or assistant message - for msg in chat.iter().rev() { - match msg { - Message::User(text) | Message::Assistant(text) => { - let preview: String = text.chars().take(50).collect(); - return Some(if text.len() > 50 { - format!("{}...", preview) - } else { - preview - }); - } - _ => continue, - } - } - None + // Draw title text (left-aligned, vertically centered) + let text_pos = rect.left_center() + egui::vec2(8.0, 0.0); + ui.painter().text( + text_pos, + egui::Align2::LEFT_CENTER, + title, + egui::FontId::proportional(14.0), + ui.visuals().text_color(), + ); + + response } }