diff options
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/actions.rs | 70 | ||||
| -rw-r--r-- | src/app/agents.rs | 1 | ||||
| -rw-r--r-- | src/app/api.rs | 344 | ||||
| -rw-r--r-- | src/app/api/agents.rs | 63 | ||||
| -rw-r--r-- | src/app/api/panes.rs | 2 | ||||
| -rw-r--r-- | src/app/input/settings.rs | 6 | ||||
| -rw-r--r-- | src/app/mod.rs | 20 | ||||
| -rw-r--r-- | src/app/runtime.rs | 20 | ||||
| -rw-r--r-- | src/app/state.rs | 11 |
9 files changed, 525 insertions, 12 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index 249d0e64..b218e53c 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -2312,6 +2312,36 @@ impl AppState { } Vec::new() } + AppEvent::AgentDetectionManifestsUpdated { updated, status } => { + self.agent_manifest_update_status = status; + self.refresh_agent_manifest_summaries(); + if !updated.is_empty() + && matches!( + self.toast_config.delivery, + crate::config::ToastDelivery::Herdr + ) + { + let agent_list = updated + .iter() + .map(|item| { + format!( + "{} {}", + crate::detect::agent_label(item.agent), + item.version + ) + }) + .collect::<Vec<_>>() + .join(", "); + self.toast = Some(ToastNotification { + kind: ToastKind::UpdateInstalled, + title: "Agent detection rules updated".to_string(), + context: agent_list, + position: None, + target: None, + }); + } + Vec::new() + } AppEvent::StateChanged { pane_id, agent, @@ -2520,14 +2550,15 @@ impl AppState { ) -> Option<PaneStateUpdate> { let observed_at = std::time::Instant::now(); self.update_terminal_state(pane_id, |terminal| { - let agent = terminal - .effective_known_agent() - .or(terminal.detected_agent)?; + let agent = terminal.effective_known_agent().or(terminal.detected_agent); + if agent.is_none() && !terminal.full_lifecycle_hook_authority_active() { + return None; + } Some(terminal.set_detected_state_with_screen_signals_at( - Some(agent), + agent, AgentState::Idle, false, - false, + true, false, true, observed_at, @@ -4684,6 +4715,35 @@ mod tests { } #[test] + fn agent_detection_manifest_update_event_updates_status_and_toast() { + let mut state = AppState::test_new(); + state.toast_config.delivery = crate::config::ToastDelivery::Herdr; + let status = crate::detect::manifest_update::ManifestUpdateStatus { + last_result: Some("checked".to_string()), + ..Default::default() + }; + + let updates = state.handle_app_event(AppEvent::AgentDetectionManifestsUpdated { + updated: vec![crate::detect::manifest_update::ManifestUpdateCommit { + agent: Agent::Codex, + version: crate::detect::manifest_update::ManifestVersion::parse("2026.06.10.1") + .unwrap(), + }], + status, + }); + + assert!(updates.is_empty()); + assert_eq!( + state.agent_manifest_update_status.last_result.as_deref(), + Some("checked") + ); + let toast = state.toast.as_ref().expect("manifest update toast"); + assert_eq!(toast.kind, ToastKind::UpdateInstalled); + assert_eq!(toast.title, "Agent detection rules updated"); + assert_eq!(toast.context, "codex 2026.06.10.1"); + } + + #[test] fn toggle_zoom_works() { let mut state = app_with_workspaces(&["test"]); state.workspaces[0].test_split(Direction::Horizontal); diff --git a/src/app/agents.rs b/src/app/agents.rs index 6d463b28..ea828eae 100644 --- a/src/app/agents.rs +++ b/src/app/agents.rs @@ -415,6 +415,7 @@ impl App { title: pane.title, display_agent: pane.display_agent, agent_status: pane.agent_status, + screen_detection_skipped: terminal.full_lifecycle_hook_authority_active(), custom_status: pane.custom_status, state_labels: pane.state_labels, agent_session: pane.agent_session, diff --git a/src/app/api.rs b/src/app/api.rs index 76d9c537..ea4ea378 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -69,6 +69,7 @@ impl App { if let AppEvent::PaneDied { pane_id } = &ev { let previous_toast = self.state.toast.clone(); if let Some(update) = self.state.publish_pane_process_exit_if_agent(*pane_id) { + self.sync_full_lifecycle_authority_detection_pauses(); self.refresh_new_herdr_toast_context_for_update(&update, &previous_toast); self.emit_pane_state_update(&update); self.emit_terminal_or_system_agent_notifications(std::slice::from_ref(&update)); @@ -123,17 +124,18 @@ impl App { } else { None }; + let manifest_update_agents = + if let AppEvent::AgentDetectionManifestsUpdated { updated, .. } = &ev { + Some(updated.iter().map(|item| item.agent).collect::<Vec<_>>()) + } else { + None + }; let terminal_cwd_reported = matches!(ev, AppEvent::TerminalCwdReported { .. }); let previous_toast = self.state.toast.clone(); let pane_updates = self.state.handle_app_event(ev); - if terminal_cwd_reported { - self.mark_git_status_refresh_due(Instant::now()); - } - for update in &pane_updates { - self.refresh_new_herdr_toast_context_for_update(update, &previous_toast); - self.emit_pane_state_update(update); + if let Some(agents) = manifest_update_agents { + self.reset_agent_detection_for_agents(&agents); } - self.sync_agent_metadata_deadline(); if let Some((pane_id, agent)) = released_agent { if pane_updates.iter().any(|update| update.pane_id == pane_id) { if let Some((ws_idx, _)) = self.find_pane(pane_id) { @@ -147,6 +149,15 @@ impl App { } } } + self.sync_full_lifecycle_authority_detection_pauses(); + if terminal_cwd_reported { + self.mark_git_status_refresh_due(Instant::now()); + } + 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(); if let Some(overlay) = overlay_state { self.restore_overlay_after_exit(overlay); } @@ -175,6 +186,29 @@ impl App { self.shutdown_detached_terminal_runtimes(); } + fn reset_agent_detection_for_agents(&self, agents: &[crate::detect::Agent]) { + if agents.is_empty() { + return; + } + for (terminal_id, terminal) in &self.state.terminals { + let Some(agent) = terminal.effective_known_agent().or(terminal.detected_agent) else { + continue; + }; + if !agents.contains(&agent) { + continue; + } + if let Some(runtime) = self.terminal_runtimes.get(terminal_id) { + runtime.reset_agent_detection(); + } + } + } + + fn reset_all_agent_detection_runtimes(&self) { + for runtime in self.terminal_runtimes.values() { + runtime.reset_agent_detection(); + } + } + pub(crate) fn refresh_new_herdr_toast_context_for_update( &mut self, update: &crate::app::actions::PaneStateUpdate, @@ -218,6 +252,26 @@ impl App { } } + fn sync_full_lifecycle_authority_detection_pauses(&self) { + for workspace in &self.state.workspaces { + for tab in &workspace.tabs { + for pane in tab.panes.values() { + let Some(terminal) = self.state.terminals.get(&pane.attached_terminal_id) + else { + continue; + }; + let Some(runtime) = self.terminal_runtimes.get(&pane.attached_terminal_id) + else { + continue; + }; + runtime.set_full_lifecycle_authority_active( + terminal.full_lifecycle_hook_authority_active(), + ); + } + } + } + } + pub(crate) fn show_clipboard_feedback(&mut self) { if !self.state.toast_config.clipboard.enabled { self.state.copy_feedback = None; @@ -624,6 +678,39 @@ impl App { }, } } + Method::ServerAgentManifests(_) => { + self.state.refresh_agent_manifest_summaries(); + let update_status = crate::detect::manifest_update::load_status(); + SuccessResponse { + id: request.id, + result: ResponseResult::AgentManifestStatus { + last_check_unix: update_status.last_check_unix, + last_result: update_status.last_result.clone(), + manifests: self + .state + .agent_manifest_summaries + .clone() + .into_iter() + .map(|summary| agent_manifest_info(summary, &update_status)) + .collect(), + }, + } + } + Method::ServerReloadAgentManifests(_) => { + let summaries = crate::detect::manifest::reload_manifests(); + self.state.agent_manifest_summaries = summaries.clone(); + let update_status = crate::detect::manifest_update::load_status(); + self.reset_all_agent_detection_runtimes(); + SuccessResponse { + id: request.id, + result: ResponseResult::AgentManifestReload { + manifests: summaries + .into_iter() + .map(|summary| agent_manifest_info(summary, &update_status)) + .collect(), + }, + } + } Method::NotificationShow(params) => { return self.handle_notification_show(request.id, params); } @@ -661,6 +748,7 @@ impl App { Method::AgentRename(params) => return self.handle_agent_rename(request.id, params), Method::AgentStart(params) => return self.handle_agent_start(request.id, params), Method::AgentRead(params) => return self.handle_agent_read(request.id, params), + Method::AgentExplain(target) => return self.handle_agent_explain(request.id, target), Method::AgentSend(params) => return self.handle_agent_send(request.id, params), Method::PaneSplit(params) => return self.handle_pane_split(request.id, params), Method::PaneSwap(params) => return self.handle_pane_swap(request.id, params), @@ -838,6 +926,25 @@ fn sanitized_notification_text(value: &str, max_chars: usize) -> Option<String> (!sanitized.is_empty()).then_some(sanitized) } +fn agent_manifest_info( + summary: crate::detect::manifest::AgentManifestSummary, + update_status: &crate::detect::manifest_update::ManifestUpdateStatus, +) -> crate::api::schema::AgentManifestInfo { + let remote = update_status.agent_status(summary.agent); + crate::api::schema::AgentManifestInfo { + agent: crate::detect::agent_label(summary.agent).to_string(), + source: summary.active_source.label(), + source_kind: summary.active_source.kind().to_string(), + active_version: summary.active_version, + cached_remote_version: summary.cached_remote_version, + local_override_shadowing_remote: summary.local_override_shadowing_remote, + remote_update_result: remote.as_ref().map(|status| status.last_result.clone()), + remote_update_error: remote.as_ref().and_then(|status| status.last_error.clone()), + remote_last_checked_unix: remote.and_then(|status| status.last_checked_unix), + warning: summary.warning, + } +} + #[cfg(test)] mod tests { use super::*; @@ -853,6 +960,229 @@ mod tests { assert!(status.success(), "git init failed for {}", path.display()); } + #[tokio::test] + async fn manifest_update_event_resets_matching_agent_detection_runtime() { + 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.state.workspaces = vec![crate::workspace::Workspace::test_new("manifest-reset")]; + app.state.ensure_test_terminals(); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + let terminal_id = app.state.workspaces[0].tabs[0].panes[&pane_id] + .attached_terminal_id + .clone(); + app.state + .terminals + .get_mut(&terminal_id) + .unwrap() + .detected_agent = Some(Agent::Codex); + let (runtime, _rx) = crate::terminal::TerminalRuntime::test_with_channel(80, 24); + let reset_notify = runtime.agent_detection_reset_notify_for_test(); + app.terminal_runtimes.insert(terminal_id, runtime); + + app.handle_internal_event(AppEvent::AgentDetectionManifestsUpdated { + updated: vec![crate::detect::manifest_update::ManifestUpdateCommit { + agent: Agent::Codex, + version: crate::detect::manifest_update::ManifestVersion::parse("2026.06.10.1") + .unwrap(), + }], + status: crate::detect::manifest_update::ManifestUpdateStatus::default(), + }); + + tokio::time::timeout( + std::time::Duration::from_millis(50), + reset_notify.notified(), + ) + .await + .expect("matching agent detection runtime should be reset"); + } + + #[tokio::test] + async fn server_reload_agent_manifests_resets_detection_runtimes() { + 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.state.workspaces = vec![crate::workspace::Workspace::test_new("manifest-reload")]; + app.state.ensure_test_terminals(); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + let terminal_id = app.state.workspaces[0].tabs[0].panes[&pane_id] + .attached_terminal_id + .clone(); + let (runtime, _rx) = crate::terminal::TerminalRuntime::test_with_channel(80, 24); + let reset_notify = runtime.agent_detection_reset_notify_for_test(); + app.terminal_runtimes.insert(terminal_id, runtime); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "reload_manifests".into(), + method: crate::api::schema::Method::ServerReloadAgentManifests( + crate::api::schema::EmptyParams::default(), + ), + }); + let response: serde_json::Value = serde_json::from_str(&response).unwrap(); + assert_eq!(response["result"]["type"], "agent_manifest_reload"); + assert!(!response["result"]["manifests"] + .as_array() + .unwrap() + .is_empty()); + + tokio::time::timeout( + std::time::Duration::from_millis(50), + reset_notify.notified(), + ) + .await + .expect("manual manifest reload should reset detection runtimes"); + } + + #[tokio::test] + async fn server_agent_manifests_reports_status_without_resetting_runtimes() { + 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.state.workspaces = vec![crate::workspace::Workspace::test_new("manifest-status")]; + app.state.ensure_test_terminals(); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + let terminal_id = app.state.workspaces[0].tabs[0].panes[&pane_id] + .attached_terminal_id + .clone(); + let (runtime, _rx) = crate::terminal::TerminalRuntime::test_with_channel(80, 24); + let reset_notify = runtime.agent_detection_reset_notify_for_test(); + app.terminal_runtimes.insert(terminal_id, runtime); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "manifest_status".into(), + method: crate::api::schema::Method::ServerAgentManifests( + crate::api::schema::EmptyParams::default(), + ), + }); + let response: serde_json::Value = serde_json::from_str(&response).unwrap(); + assert_eq!(response["result"]["type"], "agent_manifest_status"); + assert!(!response["result"]["manifests"] + .as_array() + .unwrap() + .is_empty()); + assert!( + tokio::time::timeout( + std::time::Duration::from_millis(10), + reset_notify.notified(), + ) + .await + .is_err(), + "status request should not reset detection runtimes" + ); + } + + #[tokio::test] + async fn agent_explain_evaluates_with_server_manifest_cache() { + 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.state.workspaces = vec![crate::workspace::Workspace::test_new("agent-explain")]; + app.state.ensure_test_terminals(); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + let terminal_id = app.state.workspaces[0].tabs[0].panes[&pane_id] + .attached_terminal_id + .clone(); + app.state + .terminals + .get_mut(&terminal_id) + .unwrap() + .detected_agent = Some(Agent::Codex); + let runtime = crate::terminal::TerminalRuntime::test_with_screen_bytes( + 80, + 24, + b"press enter to confirm or esc to cancel", + ); + app.terminal_runtimes.insert(terminal_id, runtime); + let target = app.public_pane_id(0, pane_id).unwrap(); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "agent_explain".into(), + method: crate::api::schema::Method::AgentExplain(crate::api::schema::AgentTarget { + target, + }), + }); + let response: serde_json::Value = serde_json::from_str(&response).unwrap(); + + assert_eq!(response["result"]["type"], "agent_explain"); + assert_eq!(response["result"]["explain"]["state"], "blocked"); + assert_eq!( + response["result"]["explain"]["matched_rule"]["id"], + "live_strong_blocker" + ); + } + + #[tokio::test] + async fn agent_explain_reports_hook_only_full_lifecycle_authority() { + 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.state.workspaces = vec![crate::workspace::Workspace::test_new("agent-explain-omp")]; + app.state.ensure_test_terminals(); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + let terminal_id = app.state.workspaces[0].tabs[0].panes[&pane_id] + .attached_terminal_id + .clone(); + app.state + .terminals + .get_mut(&terminal_id) + .unwrap() + .set_hook_authority( + "herdr:omp".to_string(), + "omp".to_string(), + AgentState::Working, + None, + Some(1), + ); + let runtime = crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b""); + app.terminal_runtimes.insert(terminal_id, runtime); + let target = app.public_pane_id(0, pane_id).unwrap(); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "agent_explain_omp".into(), + method: crate::api::schema::Method::AgentExplain(crate::api::schema::AgentTarget { + target, + }), + }); + let response: serde_json::Value = serde_json::from_str(&response).unwrap(); + + assert_eq!(response["result"]["type"], "agent_explain"); + assert_eq!(response["result"]["explain"]["agent"], "omp"); + assert_eq!(response["result"]["explain"]["state"], "working"); + assert_eq!( + response["result"]["explain"]["screen_detection_skip_reason"], + "full_lifecycle_hook_authority" + ); + assert_eq!( + response["result"]["explain"]["matched_rule"], + serde_json::Value::Null + ); + } + #[cfg(unix)] #[tokio::test] async fn herdr_toast_context_uses_live_root_runtime_cwd_label() { diff --git a/src/app/api/agents.rs b/src/app/api/agents.rs index 4077b38c..f89be0ce 100644 --- a/src/app/api/agents.rs +++ b/src/app/api/agents.rs @@ -73,11 +73,13 @@ impl App { ReadSource::Visible => pane.visible_text(), ReadSource::Recent => pane.recent_text(requested_lines), ReadSource::RecentUnwrapped => pane.recent_unwrapped_text(requested_lines), + ReadSource::Detection => pane.detection_text(), }, ReadFormat::Ansi => match params.source { ReadSource::Visible => pane.visible_ansi(), ReadSource::Recent => pane.recent_ansi(requested_lines), ReadSource::RecentUnwrapped => pane.recent_unwrapped_ansi(requested_lines), + ReadSource::Detection => pane.detection_text(), }, }; @@ -102,6 +104,67 @@ impl App { ) } + pub(super) fn handle_agent_explain(&mut self, id: String, target: AgentTarget) -> String { + let resolved = match self.resolve_terminal_target(&target.target) { + Ok(resolved) => resolved, + Err(err) => return encode_error_body(id, self.agent_target_error_body(err)), + }; + let Some((pane, _workspace_id)) = self.lookup_runtime(resolved.ws_idx, resolved.pane_id) + else { + return agent_not_found(id, &target.target); + }; + let Some(terminal_id) = self + .state + .workspaces + .get(resolved.ws_idx) + .and_then(|workspace| workspace.terminal_id(resolved.pane_id)) + else { + return agent_not_found(id, &target.target); + }; + let Some(terminal) = self.state.terminals.get(terminal_id) else { + return agent_not_found(id, &target.target); + }; + if terminal.full_lifecycle_hook_authority_active() { + let explain = serde_json::json!({ + "agent": terminal.effective_agent_label().unwrap_or("unknown"), + "state": crate::detect::manifest::agent_state_label(terminal.state), + "manifest_source": null, + "manifest_version": null, + "cached_remote_version": null, + "local_override_shadowing_remote": false, + "remote_update_status": null, + "remote_update_error": null, + "matched_rule": null, + "visible_idle": false, + "visible_blocker": false, + "visible_working": false, + "screen_detection_skipped": true, + "screen_detection_skip_reason": "full_lifecycle_hook_authority", + "skip_state_update": false, + "skipped_update_reason": null, + "fallback_reason": null, + "warning": null, + "evaluated_rules": [], + }); + return encode_success(id, ResponseResult::AgentExplain { explain }); + } + let Some(agent) = terminal.effective_known_agent().or(terminal.detected_agent) else { + return encode_error( + id, + "agent_explain_unavailable", + format!( + "agent target {} does not have a detected agent label", + target.target + ), + ); + }; + + let explain = crate::detect::manifest::explain(agent, &pane.detection_text()); + let value = crate::detect::manifest::explain_to_json_value(&explain); + + encode_success(id, ResponseResult::AgentExplain { explain: value }) + } + pub(super) fn handle_agent_send(&mut self, id: String, params: AgentSendParams) -> String { let resolved = match self.resolve_terminal_target(¶ms.target) { Ok(resolved) => resolved, diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs index d824d26b..5afac850 100644 --- a/src/app/api/panes.rs +++ b/src/app/api/panes.rs @@ -616,11 +616,13 @@ impl App { ReadSource::Visible => pane.visible_text(), ReadSource::Recent => pane.recent_text(requested_lines), ReadSource::RecentUnwrapped => pane.recent_unwrapped_text(requested_lines), + ReadSource::Detection => pane.detection_text(), }, ReadFormat::Ansi => match params.source { ReadSource::Visible => pane.visible_ansi(), ReadSource::Recent => pane.recent_ansi(requested_lines), ReadSource::RecentUnwrapped => pane.recent_unwrapped_ansi(requested_lines), + ReadSource::Detection => pane.detection_text(), }, }; diff --git a/src/app/input/settings.rs b/src/app/input/settings.rs index 66ec3226..0e7c0d81 100644 --- a/src/app/input/settings.rs +++ b/src/app/input/settings.rs @@ -603,6 +603,12 @@ mod tests { KeyEvent::new(KeyCode::BackTab, KeyModifiers::empty()), ); assert_eq!(state.settings.section, SettingsSection::Integrations); + + update_settings_state( + &mut state, + KeyEvent::new(KeyCode::BackTab, KeyModifiers::empty()), + ); + assert_eq!(state.settings.section, SettingsSection::PaneLabels); } #[test] diff --git a/src/app/mod.rs b/src/app/mod.rs index 35ab92b1..c0af6754 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -107,6 +107,7 @@ pub struct App { pub(crate) next_resize_poll: Instant, pub(crate) next_animation_tick: Option<Instant>, pub(crate) next_auto_update_check: Option<Instant>, + pub(crate) next_agent_manifest_update_check: Option<Instant>, pub(crate) agent_metadata_deadline: Option<Instant>, pub(crate) pending_agent_resume_deadline: Option<Instant>, pub(crate) selection_autoscroll_deadline: Option<Instant>, @@ -398,6 +399,8 @@ impl App { state::Mode::Navigate }; + let agent_manifest_summaries = crate::detect::manifest::reload_manifests(); + let mut state = AppState { terminals: std::collections::HashMap::new(), direct_attach_resize_locks: std::collections::HashSet::new(), @@ -535,6 +538,8 @@ impl App { original_theme: None, }, integration_recommendations: crate::integration::integration_recommendations(), + agent_manifest_summaries, + agent_manifest_update_status: crate::detect::manifest_update::load_status(), integration_install_messages: Vec::new(), global_menu: state::MenuListState::new(0), host_terminal_theme: crate::terminal_theme::TerminalTheme::default(), @@ -557,6 +562,10 @@ impl App { if auto_updates_enabled(no_session) { let update_tx = event_tx.clone(); std::thread::spawn(move || crate::update::auto_update(update_tx)); + let manifest_update_tx = event_tx.clone(); + std::thread::spawn(move || { + crate::detect::manifest_update::auto_update(manifest_update_tx) + }); } let last_focus = state.active.and_then(|idx| { @@ -585,6 +594,8 @@ impl App { next_animation_tick: None, next_auto_update_check: auto_updates_enabled(no_session) .then_some(Instant::now() + AUTO_UPDATE_CHECK_INTERVAL), + next_agent_manifest_update_check: auto_updates_enabled(no_session) + .then_some(Instant::now() + AUTO_UPDATE_CHECK_INTERVAL), agent_metadata_deadline: None, pending_agent_resume_deadline: None, session_save_deadline: None, @@ -635,6 +646,15 @@ impl App { let pane_id_aliases = crate::persist::handoff_pane_aliases(snapshot, &workspaces); app.no_session = false; + if auto_updates_enabled(app.no_session) { + let now = Instant::now(); + app.next_auto_update_check = app + .state + .update_available + .is_none() + .then_some(now + AUTO_UPDATE_CHECK_INTERVAL); + app.next_agent_manifest_update_check = Some(now + AUTO_UPDATE_CHECK_INTERVAL); + } app.state.detach_exits = false; app.state.pane_id_aliases = pane_id_aliases; app.state.workspaces = workspaces; diff --git a/src/app/runtime.rs b/src/app/runtime.rs index ca780360..cb2e5712 100644 --- a/src/app/runtime.rs +++ b/src/app/runtime.rs @@ -257,6 +257,13 @@ impl App { } if self + .next_agent_manifest_update_check + .is_some_and(|deadline| now >= deadline) + { + self.run_agent_manifest_update_check(); + } + + if self .session_save_deadline .is_some_and(|deadline| now >= deadline) { @@ -448,6 +455,18 @@ impl App { std::thread::spawn(move || crate::update::auto_update(update_tx)); } + pub(crate) fn run_agent_manifest_update_check(&mut self) { + if !auto_updates_enabled(self.no_session) { + self.next_agent_manifest_update_check = None; + return; + } + + self.next_agent_manifest_update_check = Some(Instant::now() + AUTO_UPDATE_CHECK_INTERVAL); + + let manifest_update_tx = self.event_tx.clone(); + std::thread::spawn(move || crate::detect::manifest_update::auto_update(manifest_update_tx)); + } + pub(crate) fn start_git_status_refresh_if_due(&mut self, now: Instant) { let Some(deadline) = self.git_refresh_deadline() else { return; @@ -531,6 +550,7 @@ impl App { .then(|| self.git_refresh_deadline()) .flatten(), self.next_auto_update_check, + self.next_agent_manifest_update_check, self.agent_metadata_deadline, self.pending_agent_resume_deadline, self.session_save_deadline, diff --git a/src/app/state.rs b/src/app/state.rs index 01f3d9c8..f37ccb22 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1377,6 +1377,10 @@ pub struct AppState { pub settings: SettingsState, /// Cached integration recommendations for onboarding/settings UI. pub integration_recommendations: Vec<crate::integration::IntegrationRecommendation>, + /// Cached detection manifest source/version summaries for runtime/API status. + pub agent_manifest_summaries: Vec<crate::detect::manifest::AgentManifestSummary>, + /// Cached remote detection manifest update diagnostics for runtime/API status. + pub agent_manifest_update_status: crate::detect::manifest_update::ManifestUpdateStatus, /// Result messages from the latest integration install action. pub integration_install_messages: Vec<String>, /// Highlight state for the bottom-right global launcher menu. @@ -1433,6 +1437,10 @@ impl AppState { .any(|item| item.state == crate::integration::IntegrationStatusKind::Outdated) } + pub(crate) fn refresh_agent_manifest_summaries(&mut self) { + self.agent_manifest_summaries = crate::detect::manifest::manifest_summaries(); + } + pub(crate) fn global_menu_attention_badge_visible(&self) -> bool { self.update_available.is_some() || self.integration_updates_available() } @@ -1700,6 +1708,9 @@ impl AppState { original_theme: None, }, integration_recommendations: Vec::new(), + agent_manifest_summaries: Vec::new(), + agent_manifest_update_status: + crate::detect::manifest_update::ManifestUpdateStatus::default(), integration_install_messages: Vec::new(), global_menu: MenuListState::new(0), host_terminal_theme: TerminalTheme::default(), |
