use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use ag_forge::ReviewRequestClient;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tracing::{debug, warn};
use crate::app::AppEvent;
use crate::db::AppRepositories;
use crate::domain::agent::{AgentCliInfo, AgentKind};
use crate::domain::session::SessionId;
use crate::infra::app_server::AppServerClient;
use crate::infra::clipboard_image::{ClipboardImageClient, RealClipboardImageClient};
use crate::infra::clock::Clock;
use crate::infra::fs::FsClient;
use crate::infra::git::GitClient;
use crate::infra::review_comment_cache::ReviewCommentCache;
pub(crate) type SessionUpdateVersionMap = Arc<Mutex<HashMap<SessionId, u64>>>;
pub(crate) struct AppServiceDeps {
pub(crate) app_server_client_override: Option<Arc<dyn AppServerClient>>,
pub(crate) available_agent_kinds: Vec<AgentKind>,
pub(crate) clipboard_image_client_override: Option<Arc<dyn ClipboardImageClient>>,
pub(crate) fs_client: Arc<dyn FsClient>,
pub(crate) git_client: Arc<dyn GitClient>,
pub(crate) repositories: AppRepositories,
pub(crate) review_request_client: Arc<dyn ReviewRequestClient>,
}
pub struct AppServices {
available_agent_clis: Arc<Mutex<Vec<AgentCliInfo>>>,
available_agent_kinds: Arc<[AgentKind]>,
app_server_client_override: Option<Arc<dyn AppServerClient>>,
base_path: PathBuf,
cleanup_task_handles: Arc<Mutex<Vec<JoinHandle<()>>>>,
clipboard_image_client: Arc<dyn ClipboardImageClient>,
clock: Arc<dyn Clock>,
event_tx: mpsc::UnboundedSender<AppEvent>,
fs_client: Arc<dyn FsClient>,
git_client: Arc<dyn GitClient>,
repositories: AppRepositories,
review_comment_cache: ReviewCommentCache,
review_request_client: Arc<dyn ReviewRequestClient>,
session_update_versions: SessionUpdateVersionMap,
}
impl AppServices {
pub(crate) fn new_with_agent_clis(
base_path: PathBuf,
clock: Arc<dyn Clock>,
event_tx: mpsc::UnboundedSender<AppEvent>,
deps: AppServiceDeps,
available_agent_clis: Vec<AgentCliInfo>,
) -> Self {
let AppServiceDeps {
app_server_client_override,
available_agent_kinds,
clipboard_image_client_override,
fs_client,
git_client,
repositories,
review_request_client,
} = deps;
let clipboard_image_client = clipboard_image_client_override.unwrap_or_else(|| {
Arc::new(RealClipboardImageClient::new(
Arc::clone(&clock),
Arc::clone(&fs_client),
))
});
Self {
available_agent_clis: Arc::new(Mutex::new(available_agent_clis)),
available_agent_kinds: Arc::<[AgentKind]>::from(available_agent_kinds),
app_server_client_override,
base_path,
cleanup_task_handles: Arc::default(),
clipboard_image_client,
clock,
event_tx,
fs_client,
git_client,
repositories,
review_comment_cache: ReviewCommentCache::default(),
review_request_client,
session_update_versions: Arc::default(),
}
}
pub(crate) fn base_path(&self) -> &Path {
self.base_path.as_path()
}
pub(crate) fn available_agent_kinds(&self) -> Vec<AgentKind> {
self.available_agent_kinds.as_ref().to_vec()
}
pub(crate) fn available_agent_clis(&self) -> Vec<AgentCliInfo> {
self.available_agent_clis
.lock()
.map(|agent_clis| agent_clis.clone())
.unwrap_or_default()
}
pub(crate) fn replace_available_agent_clis(&self, available_agent_clis: Vec<AgentCliInfo>) {
if let Ok(mut agent_clis) = self.available_agent_clis.lock() {
*agent_clis = available_agent_clis;
}
}
pub(crate) fn db(&self) -> &AppRepositories {
&self.repositories
}
pub(crate) fn clock(&self) -> Arc<dyn Clock> {
Arc::clone(&self.clock)
}
pub(crate) fn clipboard_image_client(&self) -> Arc<dyn ClipboardImageClient> {
Arc::clone(&self.clipboard_image_client)
}
pub(crate) fn emit_app_event(&self, event: AppEvent) {
let event_label = app_event_label(&event);
debug!(
event = event_label,
"enqueueing app event through app services"
);
if self.event_tx.send(event).is_err() {
warn!(
event = event_label,
"failed to send app event because the receiver is closed"
);
}
}
pub(crate) fn emit_session_and_project_refresh_events(&self) {
self.emit_app_event(AppEvent::RefreshSessions);
self.emit_app_event(AppEvent::RefreshProjects);
}
pub(crate) fn track_cleanup_task(&self, join_handle: JoinHandle<()>) {
if let Ok(mut cleanup_task_handles) = self.cleanup_task_handles.lock() {
cleanup_task_handles.push(join_handle);
}
}
pub(crate) async fn wait_for_cleanup_tasks(&self) {
loop {
let cleanup_task_handles = self
.cleanup_task_handles
.lock()
.map(|mut task_handles| task_handles.drain(..).collect::<Vec<_>>())
.unwrap_or_default();
if cleanup_task_handles.is_empty() {
break;
}
for cleanup_task_handle in cleanup_task_handles {
if let Err(error) = cleanup_task_handle.await {
warn!(
error = %error,
"background cleanup task failed during shutdown"
);
}
}
}
}
pub(crate) fn event_sender(&self) -> mpsc::UnboundedSender<AppEvent> {
self.event_tx.clone()
}
pub(crate) fn fs_client(&self) -> Arc<dyn FsClient> {
Arc::clone(&self.fs_client)
}
pub(crate) fn git_client(&self) -> Arc<dyn GitClient> {
Arc::clone(&self.git_client)
}
pub(crate) fn review_request_client(&self) -> Arc<dyn ReviewRequestClient> {
Arc::clone(&self.review_request_client)
}
pub(crate) fn session_update_versions(&self) -> SessionUpdateVersionMap {
Arc::clone(&self.session_update_versions)
}
pub(crate) fn review_comment_cache(&self) -> ReviewCommentCache {
self.review_comment_cache.clone()
}
pub(crate) fn app_server_client_override(&self) -> Option<Arc<dyn AppServerClient>> {
self.app_server_client_override.as_ref().map(Arc::clone)
}
}
fn app_event_label(event: &AppEvent) -> &'static str {
match event {
AppEvent::AtMentionEntriesLoaded { .. } => "AtMentionEntriesLoaded",
AppEvent::GitStatusUpdated { .. } => "GitStatusUpdated",
AppEvent::VersionAvailabilityUpdated { .. } => "VersionAvailabilityUpdated",
AppEvent::AgentCliVersionsUpdated { .. } => "AgentCliVersionsUpdated",
AppEvent::UpdateStatusChanged { .. } => "UpdateStatusChanged",
AppEvent::SystemLog { .. } => "SystemLog",
AppEvent::SessionModelUpdated { .. } => "SessionModelUpdated",
AppEvent::SessionReasoningLevelUpdated { .. } => "SessionReasoningLevelUpdated",
AppEvent::RefreshSessions => "RefreshSessions",
AppEvent::RefreshProjects => "RefreshProjects",
AppEvent::RefreshGitStatus => "RefreshGitStatus",
AppEvent::RequestedReviewsLoaded { .. } => "RequestedReviewsLoaded",
AppEvent::RequestedReviewCommentSnapshotLoaded { .. } => {
"RequestedReviewCommentSnapshotLoaded"
}
AppEvent::SessionProgressUpdated { .. } => "SessionProgressUpdated",
AppEvent::SyncMainCompleted { .. } => "SyncMainCompleted",
AppEvent::SessionSizeUpdated { .. } => "SessionSizeUpdated",
AppEvent::SessionTitleGenerationFinished { .. } => "SessionTitleGenerationFinished",
AppEvent::BranchPublishActionCompleted { .. } => "BranchPublishActionCompleted",
AppEvent::ReviewPrepared { .. } => "ReviewPrepared",
AppEvent::ReviewPreparationFailed { .. } => "ReviewPreparationFailed",
AppEvent::SessionUpdated { .. } => "SessionUpdated",
AppEvent::AgentResponseReceived { .. } => "AgentResponseReceived",
AppEvent::StackedParentTurnCompleted { .. } => "StackedParentTurnCompleted",
AppEvent::StackedParentSyncCompleted { .. } => "StackedParentSyncCompleted",
AppEvent::StackedParentMergeCompleted { .. } => "StackedParentMergeCompleted",
AppEvent::SessionWorkflowNoticeUpdated { .. } => "SessionWorkflowNoticeUpdated",
AppEvent::PublishedBranchSyncUpdated { .. } => "PublishedBranchSyncUpdated",
AppEvent::ReviewRequestStatusUpdated { .. } => "ReviewRequestStatusUpdated",
AppEvent::ReviewCommentsUpdated { .. } => "ReviewCommentsUpdated",
}
}