Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use forge_app::{
FileDiscoveryService, ForgeApp, GitApp, GrpcInfra, McpConfigManager, McpService,
ProviderAuthService, ProviderService, Services, User, UserUsage, Walker, WorkspaceService,
};
use forge_config::ForgeConfig;
use forge_domain::{Agent, ConsoleWriter, *};
use forge_infra::ForgeInfra;
use forge_repo::ForgeRepo;
Expand Down Expand Up @@ -42,19 +41,6 @@ impl<A, F> ForgeAPI<A, F> {
}

impl ForgeAPI<ForgeServices<ForgeRepo<ForgeInfra>>, ForgeRepo<ForgeInfra>> {
/// Creates a fully-initialized [`ForgeAPI`] from a pre-read configuration.
///
/// # Arguments
/// * `cwd` - The working directory path for environment and file resolution
/// * `config` - Pre-read application configuration (from startup)
/// * `services_url` - Pre-validated URL for the gRPC workspace server
pub fn init(cwd: PathBuf, config: ForgeConfig) -> Self {
let infra = Arc::new(ForgeInfra::new(cwd, config));
let repo = Arc::new(ForgeRepo::new(infra.clone()));
let app = Arc::new(ForgeServices::new(repo.clone()));
ForgeAPI::new(app, repo)
}

pub async fn get_skills_internal(&self) -> Result<Vec<Skill>> {
use forge_domain::SkillRepository;
self.infra.load_skills().await
Expand Down
3 changes: 3 additions & 0 deletions crates/forge_main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ forge_display.workspace = true
forge_tracker.workspace = true

forge_spinner.workspace = true
forge_repo.workspace = true
forge_infra.workspace = true
forge_services.workspace = true
forge_select.workspace = true

merge.workspace = true
Expand Down
22 changes: 21 additions & 1 deletion crates/forge_main/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::io::Read;
use std::panic;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::{Context, Result};
use clap::Parser;
use forge_api::ForgeAPI;
use forge_app::EnvironmentInfra;
use forge_config::ForgeConfig;
use forge_domain::TitleFormat;
use forge_infra::ForgeInfra;
use forge_main::{Cli, Sandbox, TitleDisplayExt, UI, tracker};

/// Enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on the stdout console handle.
Expand Down Expand Up @@ -120,8 +123,25 @@ async fn run() -> Result<()> {
(_, _) => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
};

// ForgeInfra is created once and reused across /new — it owns long-lived
// resources (HTTP client, gRPC) that don't need to reset per conversation.
let infra = Arc::new(ForgeInfra::new(cwd.clone(), config.clone()));

let mut ui = UI::init(cli, config, move |config| {
ForgeAPI::init(cwd.clone(), config)
// Config is intentionally unused here — ForgeInfra is frozen at startup.
let _ = config;

// Fresh pool on every /new. SQLite's busy_timeout handles any brief
// contention while the old pool drains from lingering hydration tasks.
let db_pool = Arc::new(
forge_repo::DatabasePool::try_from(forge_repo::PoolConfig::new(
infra.get_environment().database_path(),
))
.context("Failed to open Forge database")?,
);
let repo = Arc::new(forge_repo::ForgeRepo::new(infra.clone(), db_pool));
let services = Arc::new(forge_services::ForgeServices::new(repo.clone()));
Ok(ForgeAPI::new(services, repo))
})?;
ui.run().await;

Expand Down
10 changes: 5 additions & 5 deletions crates/forge_main/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ fn format_mcp_headers(server: &forge_domain::McpServerConfig) -> Option<String>
}
}

pub struct UI<A: ConsoleWriter, F: Fn(ForgeConfig) -> A> {
pub struct UI<A: ConsoleWriter, F: Fn(ForgeConfig) -> anyhow::Result<A>> {
markdown: MarkdownFormat,
state: UIState,
api: Arc<F::Output>,
api: Arc<A>,
new_api: Arc<F>,
console: Console,
command: Arc<ForgeCommandManager>,
Expand All @@ -115,7 +115,7 @@ pub struct UI<A: ConsoleWriter, F: Fn(ForgeConfig) -> A> {
_guard: forge_tracker::Guard,
}

impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI<A, F> {
impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> anyhow::Result<A> + Send + Sync> UI<A, F> {
/// Writes a line to the console output
/// Takes anything that implements ToString trait
fn writeln<T: ToString>(&mut self, content: T) -> anyhow::Result<()> {
Expand Down Expand Up @@ -161,7 +161,7 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
async fn on_new(&mut self) -> Result<()> {
let config = forge_config::ForgeConfig::read().unwrap_or_default();
self.config = config.clone();
self.api = Arc::new((self.new_api)(config));
self.api = Arc::new((self.new_api)(config)?);
self.init_state(false).await?;

// Set agent if provided via CLI
Expand Down Expand Up @@ -216,7 +216,7 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
/// from `forge config set` are reflected in new conversations
pub fn init(cli: Cli, config: ForgeConfig, f: F) -> Result<Self> {
// Parse CLI arguments first to get flags
let api = Arc::new(f(config.clone()));
let api = Arc::new(f(config.clone())?);
let env = api.environment();
let command = Arc::new(ForgeCommandManager::default());
let spinner = SharedSpinner::new(SpinnerManager::new(api.clone()));
Expand Down
1 change: 0 additions & 1 deletion crates/forge_repo/src/database/pool.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#![allow(dead_code)]
use std::path::PathBuf;
use std::time::Duration;

Expand Down
14 changes: 9 additions & 5 deletions crates/forge_repo/src/forge_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use url::Url;
use crate::agent::ForgeAgentRepository;
use crate::context_engine::ForgeContextEngineRepository;
use crate::conversation::ConversationRepositoryImpl;
use crate::database::{DatabasePool, PoolConfig};
use crate::database::DatabasePool;
use crate::fs_snap::ForgeFileSnapshotService;
use crate::fuzzy_search::ForgeFuzzySearchRepository;
use crate::provider::{ForgeChatRepository, ForgeProviderRepository};
Expand Down Expand Up @@ -60,13 +60,17 @@ impl<
+ HttpInfra,
> ForgeRepo<F>
{
pub fn new(infra: Arc<F>) -> Self {
/// Creates a new [`ForgeRepo`] with the provided infrastructure and a
/// shared database pool.
///
/// The pool is created once at application startup and reused across
/// `/new` conversation resets so that a fresh `ForgeRepo` never races
/// with the previous instance for the same SQLite file.
pub fn new(infra: Arc<F>, db_pool: Arc<DatabasePool>) -> Self {
let env = infra.get_environment();
let file_snapshot_service = Arc::new(ForgeFileSnapshotService::new(env.clone()));
let db_pool =
Arc::new(DatabasePool::try_from(PoolConfig::new(env.database_path())).unwrap());
let conversation_repository = Arc::new(ConversationRepositoryImpl::new(
db_pool.clone(),
db_pool,
env.workspace_hash(),
));

Expand Down
3 changes: 3 additions & 0 deletions crates/forge_repo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ mod proto_generated {
tonic::include_proto!("forge.v1");
}

// Expose the database pool types so callers (e.g. forge_api) can construct
// and share a pool without depending on internal module paths.
pub use database::{DatabasePool, PoolConfig};
// Only expose forge_repo container
pub use forge_repo::*;
Loading