diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-03-28 21:13:41 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-03-28 21:13:41 +0300 |
| commit | fa1adf1a26033fc50a2920d96045e76aba93bddb (patch) | |
| tree | 383bd48d0c6bcad7ccf1d5c907f78fd7eee1797f /src/app/actions.rs | |
| parent | d05ba2ea9f1ab2379f5be5ef7fc3898a03f5dce1 (diff) | |
feat: add onboarding and toast notification preferences
Diffstat (limited to 'src/app/actions.rs')
| -rw-r--r-- | src/app/actions.rs | 115 |
1 files changed, 114 insertions, 1 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index 7c323da5..aa767eb3 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -7,7 +7,7 @@ use crate::detect::AgentState; use crate::events::AppEvent; use crate::layout::{find_in_direction, NavDirection, PaneId}; -use super::state::{AppState, Mode}; +use super::state::{AppState, Mode, ToastKind, ToastNotification}; fn notification_sound_for_state_change( is_active_ws: bool, @@ -27,6 +27,38 @@ fn notification_sound_for_state_change( } } +fn notification_toast_for_state_change( + is_active_ws: bool, + prev_state: AgentState, + new_state: AgentState, +) -> Option<ToastKind> { + if is_active_ws || new_state == prev_state { + return None; + } + + match new_state { + AgentState::Waiting => Some(ToastKind::NeedsAttention), + AgentState::Idle if prev_state != AgentState::Idle => Some(ToastKind::Finished), + _ => None, + } +} + +fn agent_label(agent: crate::detect::Agent) -> &'static str { + match agent { + crate::detect::Agent::Pi => "pi", + crate::detect::Agent::Claude => "claude", + crate::detect::Agent::Codex => "codex", + crate::detect::Agent::Gemini => "gemini", + crate::detect::Agent::Cursor => "cursor", + crate::detect::Agent::Cline => "cline", + crate::detect::Agent::OpenCode => "opencode", + crate::detect::Agent::GithubCopilot => "copilot", + crate::detect::Agent::Kimi => "kimi", + crate::detect::Agent::Droid => "droid", + crate::detect::Agent::Amp => "amp", + } +} + // --------------------------------------------------------------------------- // Workspace operations // --------------------------------------------------------------------------- @@ -174,6 +206,7 @@ impl AppState { state, } => { for (ws_idx, ws) in self.workspaces.iter_mut().enumerate() { + let workspace_name = ws.display_name(); if let Some(pane) = ws.panes.get_mut(&pane_id) { let is_active_ws = self.active == Some(ws_idx); let prev_state = pane.state; @@ -195,6 +228,27 @@ impl AppState { } } + if self.toast_config.enabled { + if let (Some(agent), Some(kind)) = ( + agent, + notification_toast_for_state_change( + is_active_ws, + prev_state, + state, + ), + ) { + let event_text = match kind { + ToastKind::NeedsAttention => "needs attention", + ToastKind::Finished => "finished", + }; + self.toast = Some(ToastNotification { + kind, + title: format!("{} {}", agent_label(agent), event_text), + context: format!("{} · {}", workspace_name, ws_idx + 1), + }); + } + } + pane.detected_agent = agent; pane.state = state; break; @@ -434,6 +488,65 @@ mod tests { } #[test] + fn background_waiting_sets_attention_toast() { + let mut state = app_with_workspaces(&["active", "background"]); + state.active = Some(0); + state.toast_config.enabled = true; + let bg_pane_id = *state.workspaces[1].panes.keys().next().unwrap(); + + state.handle_app_event(AppEvent::StateChanged { + pane_id: bg_pane_id, + agent: Some(Agent::Pi), + state: AgentState::Waiting, + }); + + let toast = state.toast.as_ref().unwrap(); + assert_eq!(toast.kind, ToastKind::NeedsAttention); + assert_eq!(toast.title, "pi needs attention"); + assert_eq!(toast.context, "background · 2"); + } + + #[test] + fn background_idle_sets_finished_toast() { + let mut state = app_with_workspaces(&["active", "background"]); + state.active = Some(0); + state.toast_config.enabled = true; + let bg_pane_id = *state.workspaces[1].panes.keys().next().unwrap(); + state.workspaces[1] + .panes + .get_mut(&bg_pane_id) + .unwrap() + .state = AgentState::Busy; + + state.handle_app_event(AppEvent::StateChanged { + pane_id: bg_pane_id, + agent: Some(Agent::Droid), + state: AgentState::Idle, + }); + + let toast = state.toast.as_ref().unwrap(); + assert_eq!(toast.kind, ToastKind::Finished); + assert_eq!(toast.title, "droid finished"); + assert_eq!(toast.context, "background · 2"); + } + + #[test] + fn active_workspace_does_not_set_toast() { + let mut state = app_with_workspaces(&["active"]); + state.active = Some(0); + state.toast_config.enabled = true; + let pane_id = *state.workspaces[0].panes.keys().next().unwrap(); + + state.handle_app_event(AppEvent::StateChanged { + pane_id, + agent: Some(Agent::Pi), + state: AgentState::Waiting, + }); + + assert!(state.toast.is_none()); + } + + #[test] fn toggle_fullscreen_works() { let mut state = app_with_workspaces(&["test"]); state.workspaces[0].test_split(Direction::Horizontal); |
