diff options
Diffstat (limited to 'src/app/api.rs')
| -rw-r--r-- | src/app/api.rs | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/src/app/api.rs b/src/app/api.rs index 562bc449..e69ab0eb 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -103,6 +103,7 @@ impl App { let previous_toast = self.state.toast.clone(); let pane_updates = self.state.handle_app_event(ev); for update in &pane_updates { + self.refresh_new_herdr_toast_context_for_update(update, &previous_toast); self.emit_pane_state_update(update); } self.sync_agent_metadata_deadline(); @@ -180,10 +181,13 @@ impl App { ToastKind::Finished => "finished", ToastKind::UpdateInstalled => "updated", }; + let workspace_label = + ws.display_name_from(&self.state.terminals, &self.terminal_runtimes); let _ = notify( &format!("{} {}", agent_label, event_text), Some(&crate::app::actions::notification_context( ws, + &workspace_label, update.ws_idx, update.pane_id, )), @@ -196,6 +200,49 @@ impl App { self.shutdown_detached_terminal_runtimes(); } + pub(crate) fn refresh_new_herdr_toast_context_for_update( + &mut self, + update: &crate::app::actions::PaneStateUpdate, + previous_toast: &Option<crate::app::state::ToastNotification>, + ) { + if !matches!( + self.state.toast_config.delivery, + crate::config::ToastDelivery::Herdr + ) || self.state.toast == *previous_toast + { + return; + } + + let Some(target) = self + .state + .toast + .as_ref() + .and_then(|toast| toast.target.as_ref()) + else { + return; + }; + if target.pane_id != update.pane_id { + return; + } + let Some(ws) = self.state.workspaces.get(update.ws_idx) else { + return; + }; + if ws.id != target.workspace_id { + return; + } + + let workspace_label = ws.display_name_from(&self.state.terminals, &self.terminal_runtimes); + let context = crate::app::actions::notification_context( + ws, + &workspace_label, + update.ws_idx, + update.pane_id, + ); + if let Some(toast) = self.state.toast.as_mut() { + toast.context = context; + } + } + pub(crate) fn show_clipboard_feedback(&mut self) { self.state.copy_feedback = Some(crate::app::state::CopyFeedback { message: "copied to clipboard".to_string(), @@ -537,3 +584,172 @@ impl App { serde_json::to_string(&response).unwrap() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::detect::{Agent, AgentState}; + + fn init_repo(path: &std::path::Path) { + let status = std::process::Command::new("git") + .args(["init", "-q"]) + .current_dir(path) + .status() + .unwrap(); + assert!(status.success(), "git init failed for {}", path.display()); + } + + #[tokio::test] + async fn herdr_toast_context_uses_live_root_runtime_cwd_label() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &crate::config::Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + + let mut workspace = crate::workspace::Workspace::test_new("stale"); + workspace.custom_name = None; + let root = workspace.tabs[0].root_pane; + let terminal_id = workspace.terminal_id(root).cloned().unwrap(); + let temp_root = std::env::temp_dir().join(format!( + "herdr-toast-context-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let stale_cwd = temp_root.join("__herdr_original__"); + let live_cwd = temp_root.join("__herdr_projects__"); + std::fs::create_dir_all(&stale_cwd).unwrap(); + std::fs::create_dir_all(&live_cwd).unwrap(); + init_repo(&stale_cwd); + init_repo(&live_cwd); + + workspace.identity_cwd = stale_cwd.clone(); + app.state.workspaces = vec![workspace]; + app.state.ensure_test_terminals(); + app.state.terminals.get_mut(&terminal_id).unwrap().cwd = stale_cwd; + app.state.active = None; + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.toast_config.delivery = crate::config::ToastDelivery::Herdr; + + let (events, _) = tokio::sync::mpsc::channel(4); + let runtime = crate::terminal::TerminalRuntime::spawn( + root, + 24, + 80, + live_cwd.clone(), + 0, + crate::terminal_theme::TerminalTheme::default(), + "/bin/sh", + events, + std::sync::Arc::new(tokio::sync::Notify::new()), + std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), + ) + .unwrap(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); + while runtime.cwd() != Some(live_cwd.clone()) && std::time::Instant::now() < deadline { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + app.terminal_runtimes.insert(terminal_id, runtime); + + app.handle_internal_event(AppEvent::StateChanged { + pane_id: root, + agent: Some(Agent::Codex), + state: AgentState::Working, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), + }); + app.handle_internal_event(AppEvent::StateChanged { + pane_id: root, + agent: Some(Agent::Codex), + state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), + }); + + assert_eq!( + app.state.toast.as_ref().map(|toast| toast.context.as_str()), + Some("__herdr_projects__ · 1") + ); + + for (_, runtime) in app.terminal_runtimes.drain() { + runtime.shutdown(); + } + let _ = std::fs::remove_dir_all(temp_root); + } + + #[test] + fn terminal_delivery_does_not_refresh_existing_targeted_toast() { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &crate::config::Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.local_terminal_notifications = false; + + let mut workspace = crate::workspace::Workspace::test_new("stale"); + workspace.custom_name = None; + workspace.identity_cwd = "/__herdr_original__".into(); + let root = workspace.tabs[0].root_pane; + let terminal_id = workspace.terminal_id(root).cloned().unwrap(); + let workspace_id = workspace.id.clone(); + app.state.workspaces = vec![workspace]; + app.state.ensure_test_terminals(); + app.state.terminals.get_mut(&terminal_id).unwrap().cwd = "/__herdr_projects__".into(); + app.state.active = None; + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.toast_config.delivery = crate::config::ToastDelivery::Terminal; + + app.handle_internal_event(AppEvent::StateChanged { + pane_id: root, + agent: Some(Agent::Codex), + state: AgentState::Working, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), + }); + app.state.toast = Some(crate::app::state::ToastNotification { + kind: ToastKind::Finished, + title: "codex finished".into(), + context: "__herdr_original__ · 1".into(), + target: Some(crate::app::state::ToastTarget { + workspace_id, + pane_id: root, + }), + }); + + app.handle_internal_event(AppEvent::StateChanged { + pane_id: root, + agent: Some(Agent::Codex), + state: AgentState::Idle, + visible_blocker: false, + visible_idle: false, + visible_working: false, + process_exited: false, + observed_at: std::time::Instant::now(), + }); + + assert_eq!( + app.state.toast.as_ref().map(|toast| toast.context.as_str()), + Some("__herdr_original__ · 1") + ); + } +} |
