diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-08 14:53:45 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-08 14:53:45 +0300 |
| commit | c08e51e666f6df9a940393b568943b67940ceedc (patch) | |
| tree | 6a6c4cd3c8ea5ddf9e97a4695c6b0d31eb0d0717 /src/app | |
| parent | b0aa7b2cddc57a55a1c0285a1ea82b59ac9783bd (diff) | |
feat: make agent toasts focus panes
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/actions.rs | 10 | ||||
| -rw-r--r-- | src/app/input/mouse.rs | 81 | ||||
| -rw-r--r-- | src/app/input/navigate.rs | 1 | ||||
| -rw-r--r-- | src/app/mod.rs | 3 | ||||
| -rw-r--r-- | src/app/state.rs | 11 |
5 files changed, 104 insertions, 2 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index 3b904a89..506930ca 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -8,7 +8,7 @@ use crate::events::AppEvent; use crate::layout::{find_in_direction, NavDirection, PaneId}; use crate::pane::EffectiveStateChange; -use super::state::{AppState, Mode, ToastKind, ToastNotification, ViewLayout}; +use super::state::{AppState, Mode, ToastKind, ToastNotification, ToastTarget, ViewLayout}; fn is_background_completion_transition(prev_state: AgentState, new_state: AgentState) -> bool { matches!(new_state, AgentState::Idle) @@ -552,6 +552,7 @@ impl AppState { kind: ToastKind::UpdateInstalled, title: format!("v{version} available"), context: "detach, then run `herdr update`".to_string(), + target: None, }); } Vec::new() @@ -679,6 +680,10 @@ impl AppState { kind, title: format!("{} {}", toast_agent_label(agent_label), event_text), context, + target: Some(ToastTarget { + workspace_id: self.workspaces[ws_idx].id.clone(), + pane_id, + }), }); } } @@ -1086,6 +1091,9 @@ mod tests { assert_eq!(toast.kind, ToastKind::Finished); assert_eq!(toast.title, "droid finished"); assert_eq!(toast.context, "background ยท 2"); + let target = toast.target.as_ref().expect("toast target"); + assert_eq!(&target.workspace_id, &state.workspaces[1].id); + assert_eq!(target.pane_id, bg_pane_id); } #[test] diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs index 185393f0..d0c33138 100644 --- a/src/app/input/mouse.rs +++ b/src/app/input/mouse.rs @@ -31,6 +31,19 @@ impl AppState { return None; } + if self.clickable_toast_at(mouse.column, mouse.row) + && matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) + { + self.focus_toast_target(); + return None; + } + + if self.clickable_toast_at(mouse.column, mouse.row) + && matches!(mouse.kind, MouseEventKind::Up(MouseButton::Left)) + { + return None; + } + if self.mode == Mode::Settings { return self.handle_settings_mouse(mouse); } @@ -977,6 +990,34 @@ impl AppState { } } + fn clickable_toast_at(&self, col: u16, row: u16) -> bool { + self.toast + .as_ref() + .is_some_and(|toast| toast.target.is_some()) + && rect_contains(self.view.toast_hit_area, col, row) + } + + fn focus_toast_target(&mut self) { + let Some(target) = self.toast.as_ref().and_then(|toast| toast.target.clone()) else { + return; + }; + let Some(ws_idx) = self + .workspaces + .iter() + .position(|workspace| workspace.id == target.workspace_id) + else { + return; + }; + let Some(tab_idx) = self.workspaces[ws_idx].find_tab_index_for_pane(target.pane_id) else { + return; + }; + + self.switch_workspace(ws_idx); + self.switch_tab(tab_idx); + self.focus_pane(target.pane_id); + self.mode = Mode::Terminal; + } + pub(super) fn scroll_pane_up(&self, pane_id: crate::layout::PaneId, lines: usize) { if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) { if let Some(rt) = ws.runtimes.get(&pane_id) { @@ -1205,6 +1246,7 @@ mod tests { use super::*; use crate::{ app::state::{ContextMenuKind, ContextMenuState, MenuListState, Mode, ViewLayout}, + detect::{Agent, AgentState}, workspace::Workspace, }; @@ -1226,6 +1268,45 @@ mod tests { } #[test] + fn clicking_agent_toast_focuses_target_pane() { + let mut app = app_for_mouse_test(); + let active = Workspace::test_new("active"); + let mut background = Workspace::test_new("background"); + let first_pane = background.tabs[0].root_pane; + let target_pane = background.test_split(Direction::Horizontal); + background.tabs[0].layout.focus_pane(first_pane); + + app.state.workspaces = vec![active, background]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.toast_config.delivery = crate::config::ToastDelivery::Herdr; + app.state.workspaces[1] + .panes + .get_mut(&target_pane) + .unwrap() + .state = AgentState::Working; + + app.state + .handle_app_event(crate::events::AppEvent::StateChanged { + pane_id: target_pane, + agent: Some(Agent::Pi), + state: AgentState::Idle, + }); + crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20)); + + let hit = app.state.view.toast_hit_area; + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + hit.x + 1, + hit.y + 1, + )); + + assert_eq!(app.state.active, Some(1)); + assert_eq!(app.state.workspaces[1].focused_pane_id(), Some(target_pane)); + assert_eq!(app.state.mode, Mode::Terminal); + } + + #[test] fn clicking_confirm_close_accepts_workspace_close() { let mut app = app_for_mouse_test(); app.state.workspaces = vec![Workspace::test_new("a"), Workspace::test_new("b")]; diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index 546228cc..3bc8da78 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -130,6 +130,7 @@ impl App { kind: crate::app::state::ToastKind::NeedsAttention, title: "custom command failed".to_string(), context: err.to_string(), + target: None, }); self.sync_toast_deadline(previous_toast); } diff --git a/src/app/mod.rs b/src/app/mod.rs index ffef6600..32e89f0e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -318,6 +318,7 @@ impl App { terminal_area: Rect::default(), mobile_header_rect: Rect::default(), mobile_menu_hit_area: Rect::default(), + toast_hit_area: Rect::default(), pane_infos: Vec::new(), split_borders: Vec::new(), }, @@ -684,6 +685,7 @@ impl App { kind: crate::app::state::ToastKind::UpdateInstalled, title: "reloaded config".to_string(), context: "using config.toml".to_string(), + target: None, }); } } else { @@ -694,6 +696,7 @@ impl App { kind: crate::app::state::ToastKind::UpdateInstalled, title: "reloaded config".to_string(), context: "with warnings".to_string(), + target: None, }); } } diff --git a/src/app/state.rs b/src/app/state.rs index 95caa3fe..315872f4 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -3,7 +3,7 @@ use crossterm::event::{KeyCode, KeyModifiers}; use ratatui::layout::{Direction, Rect}; use ratatui::style::Color; -use crate::layout::{PaneInfo, SplitBorder}; +use crate::layout::{PaneId, PaneInfo, SplitBorder}; use crate::selection::Selection; use crate::terminal_theme::TerminalTheme; use crate::workspace::Workspace; @@ -370,6 +370,7 @@ pub struct ViewState { pub terminal_area: Rect, pub mobile_header_rect: Rect, pub mobile_menu_hit_area: Rect, + pub toast_hit_area: Rect, pub pane_infos: Vec<PaneInfo>, pub split_borders: Vec<SplitBorder>, } @@ -594,10 +595,17 @@ pub enum ToastKind { } #[derive(Debug, Clone, PartialEq, Eq)] +pub struct ToastTarget { + pub workspace_id: String, + pub pane_id: PaneId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ToastNotification { pub kind: ToastKind, pub title: String, pub context: String, + pub target: Option<ToastTarget>, } pub struct ReleaseNotesState { @@ -793,6 +801,7 @@ impl AppState { terminal_area: Rect::default(), mobile_header_rect: Rect::default(), mobile_menu_hit_area: Rect::default(), + toast_hit_area: Rect::default(), pane_infos: Vec::new(), split_borders: Vec::new(), }, |
