notedeck

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

commit d885c5b503656bc611c22391f3ce3ba4115dfdbf
parent eacaa41b34c2eb19b2a617b53882c2860c1b69a9
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 26 Jan 2026 12:31:06 -0800

dave: fix scene panel overflow with StripBuilder and clipping

- Replace manual rect allocation with StripBuilder for proper layout
- Add explicit clip_rect to panel cell to contain content
- Skip top_buttons_ui in compact mode (scene has its own toolbar)
- Reduce bottom margin in compact mode to prevent overflow
- Fix clippy warning: collapse nested if statement

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

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 160+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/notedeck_dave/src/ui/dave.rs | 20++++++++++++++++----
Mcrates/notedeck_dave/src/ui/scene.rs | 5++---
3 files changed, 96 insertions(+), 89 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -303,22 +303,12 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr /// Scene view with RTS-style agent visualization and chat side panel fn scene_ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse { + use egui_extras::{Size, StripBuilder}; + let mut dave_response = DaveResponse::default(); - let available = ui.available_rect_before_wrap(); + let mut scene_response: Option<SceneResponse> = None; let panel_width = 400.0; - // Scene area (main) - let scene_rect = egui::Rect::from_min_size( - available.min, - egui::vec2(available.width() - panel_width, available.height()), - ); - - // Chat panel area (right side) - let panel_rect = egui::Rect::from_min_size( - egui::pos2(available.max.x - panel_width, available.min.y), - egui::vec2(panel_width, available.height()), - ); - // Update all session statuses self.session_manager.update_all_statuses(); @@ -328,86 +318,92 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr self.session_manager.switch_to(attention_id); } - // Render scene - let scene_response = ui - .allocate_new_ui(egui::UiBuilder::new().max_rect(scene_rect), |ui| { - // Scene toolbar at top - ui.horizontal(|ui| { - if ui.button("+ New Agent").clicked() { - dave_response = DaveResponse::new(DaveAction::NewChat); - } + StripBuilder::new(ui) + .size(Size::remainder()) // Scene area takes remaining space + .size(Size::exact(panel_width)) // Chat panel fixed width + .clip(true) // Clip content to cell bounds + .horizontal(|mut strip| { + // Scene area (main) + strip.cell(|ui| { + // Scene toolbar at top + ui.horizontal(|ui| { + if ui.button("+ New Agent").clicked() { + dave_response = DaveResponse::new(DaveAction::NewChat); + } + ui.separator(); + if ui.button("Classic View").clicked() { + self.show_scene = false; + } + }); ui.separator(); - if ui.button("Classic View").clicked() { - self.show_scene = false; - } + + // Render the scene + scene_response = Some(self.scene.ui(&self.session_manager, ui)); }); - ui.separator(); - // Render the scene - self.scene.ui(&self.session_manager, ui) - }) - .inner; + // Chat side panel + strip.cell(|ui| { + egui::Frame::new() + .fill(ui.visuals().faint_bg_color) + .inner_margin(egui::Margin::symmetric(8, 12)) + .show(ui, |ui| { + if let Some(selected_id) = self.scene.primary_selection() { + if let Some(session) = self.session_manager.get_mut(selected_id) { + // Show title + ui.heading(&session.title); + ui.separator(); + + // Render chat UI for selected session + let response = DaveUi::new( + self.model_config.trial, + &session.chat, + &mut session.input, + ) + .compact(true) + .ui(app_ctx, ui); + + if response.action.is_some() { + dave_response = response; + } + } + } else { + // No selection + ui.centered_and_justified(|ui| { + ui.label("Select an agent to view chat"); + }); + } + }); + }); + }); - // Handle scene actions - if let Some(action) = scene_response.action { - match action { - SceneAction::SelectionChanged(ids) => { - // Selection updated, sync with session manager's active - if let Some(id) = ids.first() { - self.session_manager.switch_to(*id); + // Handle scene actions after strip rendering + if let Some(response) = scene_response { + if let Some(action) = response.action { + match action { + SceneAction::SelectionChanged(ids) => { + // Selection updated, sync with session manager's active + if let Some(id) = ids.first() { + self.session_manager.switch_to(*id); + } } - } - SceneAction::SpawnAgent => { - dave_response = DaveResponse::new(DaveAction::NewChat); - } - SceneAction::DeleteSelected => { - for id in self.scene.selected.clone() { - self.session_manager.delete_session(id); + SceneAction::SpawnAgent => { + dave_response = DaveResponse::new(DaveAction::NewChat); } - self.scene.clear_selection(); - } - SceneAction::AgentMoved { id, position } => { - if let Some(session) = self.session_manager.get_mut(id) { - session.scene_position = position; + SceneAction::DeleteSelected => { + for id in self.scene.selected.clone() { + self.session_manager.delete_session(id); + } + self.scene.clear_selection(); + } + SceneAction::AgentMoved { id, position } => { + if let Some(session) = self.session_manager.get_mut(id) { + session.scene_position = position; + } } } } } - // Render chat side panel - ui.allocate_new_ui(egui::UiBuilder::new().max_rect(panel_rect), |ui| { - egui::Frame::new() - .fill(ui.visuals().faint_bg_color) - .inner_margin(egui::Margin::symmetric(8, 12)) - .show(ui, |ui| { - if let Some(selected_id) = self.scene.primary_selection() { - if let Some(session) = self.session_manager.get_mut(selected_id) { - // Show title - ui.heading(&session.title); - ui.separator(); - - // Render chat UI for selected session - let response = DaveUi::new( - self.model_config.trial, - &session.chat, - &mut session.input, - ) - .compact(true) - .ui(app_ctx, ui); - - if response.action.is_some() { - dave_response = response; - } - } - } else { - // No selection - ui.centered_and_justified(|ui| { - ui.label("Select an agent to view chat"); - }); - } - }); - }); - dave_response } diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -113,19 +113,27 @@ impl<'a> DaveUi<'a> { /// The main render function. Call this to render Dave pub fn ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse { - let action = top_buttons_ui(app_ctx, ui); + // Skip top buttons in compact mode (scene panel has its own controls) + let action = if self.compact { + None + } else { + top_buttons_ui(app_ctx, ui) + }; egui::Frame::NONE .show(ui, |ui| { ui.with_layout(Layout::bottom_up(Align::Min), |ui| { let margin = self.chat_margin(ui.ctx()); + // Reduce bottom margin in compact mode to prevent overflow + let bottom_margin = if self.compact { 20 } else { 100 }; + let r = egui::Frame::new() .outer_margin(egui::Margin { left: margin, right: margin, top: 0, - bottom: 100, + bottom: bottom_margin, }) .inner_margin(egui::Margin::same(8)) .fill(ui.visuals().extreme_bg_color) @@ -565,14 +573,18 @@ impl<'a> DaveUi<'a> { .corner_radius(10.0) .fill(ui.visuals().widgets.inactive.weak_bg_fill) .show(ui, |ui| { - ui.label(msg); + ui.add(egui::Label::new(msg).selectable(true)); }) }); } fn assistant_chat(&self, msg: &str, ui: &mut egui::Ui) { ui.horizontal_wrapped(|ui| { - ui.add(egui::Label::new(msg).wrap_mode(egui::TextWrapMode::Wrap)); + ui.add( + egui::Label::new(msg) + .wrap_mode(egui::TextWrapMode::Wrap) + .selectable(true), + ); }); } } diff --git a/crates/notedeck_dave/src/ui/scene.rs b/crates/notedeck_dave/src/ui/scene.rs @@ -259,10 +259,9 @@ impl AgentScene { // Handle keyboard input (only when no text input has focus) if !ui.ctx().wants_keyboard_input() { if ui.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) + && !self.selected.is_empty() { - if !self.selected.is_empty() { - response = SceneResponse::new(SceneAction::DeleteSelected); - } + response = SceneResponse::new(SceneAction::DeleteSelected); } // Handle 'n' key to spawn new agent