use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::mpsc;
use super::events::AppEvent;
use super::state::{App, AppClients};
use crate::app::service::{AppServiceDeps, AppServices};
use crate::app::session::SessionManager;
use crate::app::setting::SettingsManager;
use crate::app::startup::{AppStartup, StartupProjectContext, StartupSessionLoadContext};
use crate::app::{AppError, review, sync, task};
use crate::domain::agent::AgentKind;
use crate::infra::clock::{Clock, RealClock};
use crate::infra::db;
use crate::infra::db::AppRepositories;
use crate::infra::fs::FsClient;
use crate::infra::git::GitClient;
#[cfg(test)]
use crate::infra::project_discovery::ProjectDiscoveryClient;
impl App {
pub async fn new(
auto_update: bool,
base_path: PathBuf,
working_dir: PathBuf,
git_branch: Option<String>,
repositories: impl Into<AppRepositories>,
) -> Result<Self, AppError> {
let clients = AppClients::new();
Self::new_with_options(
auto_update,
base_path,
working_dir,
git_branch,
repositories,
clients,
)
.await
}
#[cfg(test)]
pub(crate) async fn new_with_clients(
base_path: PathBuf,
working_dir: PathBuf,
git_branch: Option<String>,
repositories: impl Into<AppRepositories>,
clients: AppClients,
) -> Result<Self, AppError> {
Self::new_with_options(
false,
base_path,
working_dir,
git_branch,
repositories,
clients,
)
.await
}
async fn new_with_options(
auto_update: bool,
base_path: PathBuf,
working_dir: PathBuf,
git_branch: Option<String>,
repositories: impl Into<AppRepositories>,
clients: AppClients,
) -> Result<Self, AppError> {
let repositories = repositories.into();
let startup_project_context = Self::load_startup_project_state(
working_dir.as_path(),
git_branch,
&repositories,
&clients,
)
.await?;
let StartupProjectContext {
active_project_id,
active_project_name,
project_items,
startup_git_branch,
startup_git_upstream_ref,
startup_working_dir,
} = startup_project_context;
let clock: Arc<dyn Clock> = Arc::new(RealClock);
let (event_tx, event_rx) = mpsc::unbounded_channel();
let services = Self::build_services(
base_path.clone(),
Arc::clone(&clock),
event_tx.clone(),
repositories.clone(),
&clients,
)
.await?;
SessionManager::fail_unfinished_operations_from_previous_run(
repositories.clone(),
Arc::clone(&clock),
)
.await;
let projects = crate::app::project::ProjectManager::new(
active_project_id,
active_project_name,
startup_git_branch,
startup_git_upstream_ref,
project_items,
startup_working_dir.clone(),
);
let settings = SettingsManager::new(&services, active_project_id).await;
let default_session_model = SessionManager::load_default_session_model(
&services,
Some(active_project_id),
AgentKind::Gemini.default_model(),
)
.await;
let sessions = AppStartup::load_startup_sessions(
&services,
StartupSessionLoadContext {
active_project_id,
default_session_model,
startup_working_dir: startup_working_dir.as_path(),
},
)
.await;
let review_cache = review::review_cache_from_rows(
repositories
.sessions()
.load_session_focused_reviews_for_project(active_project_id)
.await
.unwrap_or_default(),
);
let sync_context = Self::sync_context_for(&projects, &services, &sessions);
let sync_handle = sync::SyncHandle::spawn(event_tx.clone(), sync_context);
let sync_main_runner = clients
.sync_main_runner
.unwrap_or_else(|| sync_handle.sync_main_runner());
AppStartup::spawn_background_tasks(auto_update, &event_tx);
Ok(Self {
mode: crate::ui::state::app_mode::AppMode::List,
needs_redraw: true,
settings,
tabs: crate::app::tab::TabManager::default(),
projects,
services,
sessions,
requested_review_generation: 0,
requested_review_selected_index: None,
requested_review_table_state: ratatui::widgets::TableState::default(),
requested_reviews: crate::app::RequestedReviewState::default(),
event_rx,
review_cache,
latest_available_version: None,
last_seen_session_update_versions: std::collections::HashMap::new(),
markdown_render_cache: crate::ui::markdown::MarkdownRenderCache::default(),
merge_queue: crate::app::merge_queue::MergeQueue::default(),
session_output_layout_cache:
crate::ui::component::session_output::SessionOutputLayoutCache::default(),
session_progress_messages: std::collections::HashMap::new(),
update_status: None,
sync_handle,
sync_main_runner,
tmux_client: clients.tmux_client,
})
}
async fn load_startup_project_state(
working_dir: &Path,
git_branch: Option<String>,
repositories: &AppRepositories,
clients: &AppClients,
) -> Result<StartupProjectContext, AppError> {
let current_project_id =
AppStartup::persist_startup_project(repositories, working_dir, git_branch.as_deref())
.await?;
let startup_project_context = AppStartup::load_startup_project_context(
repositories,
clients.fs_client.as_ref(),
&clients.git_client,
clients.project_discovery_client.as_ref(),
working_dir,
git_branch,
current_project_id,
)
.await?;
Ok(startup_project_context)
}
async fn build_services(
base_path: PathBuf,
clock: Arc<dyn Clock>,
event_tx: mpsc::UnboundedSender<AppEvent>,
repositories: AppRepositories,
clients: &AppClients,
) -> Result<AppServices, AppError> {
let available_agent_kinds = task::TaskService::load_agent_availability(Arc::clone(
&clients.agent_availability_probe,
))
.await;
AppStartup::validate_startup_agent_availability(&available_agent_kinds)?;
Ok(AppServices::new(
base_path,
clock,
event_tx,
AppServiceDeps {
app_server_client_override: clients
.app_server_client_override
.as_ref()
.map(Arc::clone),
available_agent_kinds,
fs_client: Arc::clone(&clients.fs_client),
git_client: Arc::clone(&clients.git_client),
repositories,
review_request_client: Arc::clone(&clients.review_request_client),
},
))
}
pub(super) async fn load_git_upstream_ref(
git_client: &dyn GitClient,
working_dir: &Path,
git_branch: Option<&str>,
) -> Option<String> {
AppStartup::load_git_upstream_ref(git_client, working_dir, git_branch).await
}
#[cfg(test)]
pub(super) async fn resolve_startup_active_project_id(
db: &AppRepositories,
fs_client: &dyn FsClient,
current_project_id: i64,
) -> i64 {
AppStartup::resolve_startup_active_project_id(db, fs_client, current_project_id).await
}
pub(super) async fn load_project_items(
db: &AppRepositories,
fs_client: &dyn FsClient,
) -> Vec<crate::domain::project::ProjectListItem> {
AppStartup::load_project_items(db, fs_client).await
}
#[cfg(test)]
pub(super) async fn load_project_items_with_session_worktree_root(
db: &AppRepositories,
fs_client: &dyn FsClient,
session_worktree_root: &Path,
) -> Vec<crate::domain::project::ProjectListItem> {
AppStartup::load_project_items_with_session_worktree_root(
db,
fs_client,
session_worktree_root,
)
.await
}
#[cfg(test)]
pub(super) async fn load_projects_from_home_directory(
db: &AppRepositories,
project_discovery_client: &dyn ProjectDiscoveryClient,
session_worktree_root: &Path,
home_directory: Option<&Path>,
) {
AppStartup::load_projects_from_home_directory(
db,
project_discovery_client,
session_worktree_root,
home_directory,
)
.await;
}
#[cfg(test)]
pub(super) fn discover_home_project_paths(
home_directory: &Path,
session_worktree_root: &Path,
) -> Vec<PathBuf> {
AppStartup::discover_home_project_paths(home_directory, session_worktree_root)
}
#[cfg(test)]
pub(super) fn is_session_worktree_project_path(
project_path: &str,
session_worktree_root: &Path,
) -> bool {
AppStartup::is_session_worktree_project_path(project_path, session_worktree_root)
}
#[cfg(test)]
pub(super) fn visible_project_rows(
project_rows: Vec<db::ProjectListRow>,
fs_client: &dyn FsClient,
session_worktree_root: &Path,
) -> Vec<db::ProjectListRow> {
AppStartup::visible_project_rows(project_rows, fs_client, session_worktree_root)
}
#[cfg(test)]
pub(super) fn is_existing_project_path(fs_client: &dyn FsClient, project_path: &str) -> bool {
AppStartup::is_existing_project_path(fs_client, project_path)
}
pub(super) fn project_from_row(project_row: db::ProjectRow) -> crate::domain::project::Project {
AppStartup::project_from_row(project_row)
}
}