diff --git a/crates/forge_api/src/api.rs b/crates/forge_api/src/api.rs index 9ca4cc05ba..c254e6c5ef 100644 --- a/crates/forge_api/src/api.rs +++ b/crates/forge_api/src/api.rs @@ -165,6 +165,14 @@ pub trait API: Sync + Send { /// generation). async fn set_commit_config(&self, config: forge_domain::CommitConfig) -> anyhow::Result<()>; + /// Gets the suggest configuration (provider and model for command + /// suggestion generation). + async fn get_suggest_config(&self) -> anyhow::Result>; + + /// Sets the suggest configuration (provider and model for command + /// suggestion generation). + async fn set_suggest_config(&self, config: forge_domain::SuggestConfig) -> anyhow::Result<()>; + /// Refresh MCP caches by fetching fresh data async fn reload_mcp(&self) -> Result<()>; diff --git a/crates/forge_api/src/forge_api.rs b/crates/forge_api/src/forge_api.rs index cf8f5f06f7..1aa57c769a 100644 --- a/crates/forge_api/src/forge_api.rs +++ b/crates/forge_api/src/forge_api.rs @@ -301,6 +301,14 @@ impl< self.services.set_commit_config(config).await } + async fn get_suggest_config(&self) -> anyhow::Result> { + self.services.get_suggest_config().await + } + + async fn set_suggest_config(&self, config: SuggestConfig) -> anyhow::Result<()> { + self.services.set_suggest_config(config).await + } + async fn get_login_info(&self) -> Result> { self.services.auth_service().get_auth_token().await } diff --git a/crates/forge_app/src/command_generator.rs b/crates/forge_app/src/command_generator.rs index 3079510148..01ad5b33d5 100644 --- a/crates/forge_app/src/command_generator.rs +++ b/crates/forge_app/src/command_generator.rs @@ -44,10 +44,20 @@ where &serde_json::json!({"env": env, "files": files}), )?; - // Get required services and data - let provider_id = self.services.get_default_provider().await?; - let provider = self.services.get_provider(provider_id).await?; - let model = self.services.get_provider_model(Some(&provider.id)).await?; + // Get required services and data - use suggest config if available, + // otherwise fall back to default provider/model + let (provider, model) = match self.services.get_suggest_config().await? { + Some(config) => { + let provider = self.services.get_provider(config.provider).await?; + (provider, config.model) + } + None => { + let provider_id = self.services.get_default_provider().await?; + let provider = self.services.get_provider(provider_id).await?; + let model = self.services.get_provider_model(Some(&provider.id)).await?; + (provider, model) + } + }; // Build user prompt with task and recent commands let user_content = format!("{}", prompt.as_str()); @@ -249,6 +259,14 @@ mod tests { async fn set_commit_config(&self, _config: forge_domain::CommitConfig) -> Result<()> { Ok(()) } + + async fn get_suggest_config(&self) -> Result> { + Ok(None) + } + + async fn set_suggest_config(&self, _config: forge_domain::SuggestConfig) -> Result<()> { + Ok(()) + } } #[tokio::test] diff --git a/crates/forge_app/src/services.rs b/crates/forge_app/src/services.rs index 13276ef9ff..7b5a6dd3c3 100644 --- a/crates/forge_app/src/services.rs +++ b/crates/forge_app/src/services.rs @@ -222,6 +222,14 @@ pub trait AppConfigService: Send + Sync { /// Sets the commit configuration (provider and model for commit message /// generation). async fn set_commit_config(&self, config: forge_domain::CommitConfig) -> anyhow::Result<()>; + + /// Gets the suggest configuration (provider and model for command + /// suggestion generation). + async fn get_suggest_config(&self) -> anyhow::Result>; + + /// Sets the suggest configuration (provider and model for command + /// suggestion generation). + async fn set_suggest_config(&self, config: forge_domain::SuggestConfig) -> anyhow::Result<()>; } #[async_trait::async_trait] @@ -1048,6 +1056,14 @@ impl AppConfigService for I { async fn set_commit_config(&self, config: forge_domain::CommitConfig) -> anyhow::Result<()> { self.config_service().set_commit_config(config).await } + + async fn get_suggest_config(&self) -> anyhow::Result> { + self.config_service().get_suggest_config().await + } + + async fn set_suggest_config(&self, config: forge_domain::SuggestConfig) -> anyhow::Result<()> { + self.config_service().set_suggest_config(config).await + } } #[async_trait::async_trait] diff --git a/crates/forge_domain/src/app_config.rs b/crates/forge_domain/src/app_config.rs index 87f35136cf..9f1fc3894c 100644 --- a/crates/forge_domain/src/app_config.rs +++ b/crates/forge_domain/src/app_config.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use derive_more::From; use serde::{Deserialize, Serialize}; -use crate::{CommitConfig, ModelId, ProviderId}; +use crate::{CommitConfig, ModelId, ProviderId, SuggestConfig}; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -23,6 +23,8 @@ pub struct AppConfig { pub model: HashMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub commit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub suggest: Option, } #[derive(Clone, Serialize, Deserialize, From, Debug, PartialEq)] diff --git a/crates/forge_domain/src/lib.rs b/crates/forge_domain/src/lib.rs index 24d903b512..70c543ed8b 100644 --- a/crates/forge_domain/src/lib.rs +++ b/crates/forge_domain/src/lib.rs @@ -41,6 +41,7 @@ mod session_metrics; mod shell; mod skill; mod snapshot; +mod suggest_config; mod suggestion; mod system_context; mod temperature; @@ -98,6 +99,7 @@ pub use session_metrics::*; pub use shell::*; pub use skill::*; pub use snapshot::*; +pub use suggest_config::*; pub use suggestion::*; pub use system_context::*; pub use temperature::*; diff --git a/crates/forge_domain/src/suggest_config.rs b/crates/forge_domain/src/suggest_config.rs new file mode 100644 index 0000000000..0ac93ee54c --- /dev/null +++ b/crates/forge_domain/src/suggest_config.rs @@ -0,0 +1,22 @@ +use derive_setters::Setters; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ModelId, ProviderId}; + +/// Configuration for shell command suggestion generation. +/// +/// Allows specifying a dedicated provider and model for shell command +/// suggestion generation, instead of using the active agent's provider and +/// model. This is useful when you want to use a cheaper or faster model for +/// simple command suggestions. Both provider and model must be specified +/// together. +#[derive(Debug, Clone, Serialize, Deserialize, Setters, JsonSchema, PartialEq)] +#[setters(into)] +pub struct SuggestConfig { + /// Provider ID to use for command suggestion generation. + pub provider: ProviderId, + + /// Model ID to use for command suggestion generation. + pub model: ModelId, +} diff --git a/crates/forge_main/src/built_in_commands.json b/crates/forge_main/src/built_in_commands.json index 55dffbfc57..c24237668c 100644 --- a/crates/forge_main/src/built_in_commands.json +++ b/crates/forge_main/src/built_in_commands.json @@ -19,6 +19,10 @@ "command": "config-commit-model", "description": "Set the model used for commit message generation [alias: ccm]" }, + { + "command": "suggest-model", + "description": "Set the model used for command suggestion generation [alias: sm]" + }, { "command": "new", "description": "Start new conversation [alias: n]" diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index 1c169f3597..5f095e86d3 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -553,6 +553,13 @@ pub enum ConfigSetField { /// Model ID to use for commit message generation. model: ModelId, }, + /// Set the provider and model for command suggestion generation. + Suggest { + /// Provider ID to use for command suggestion generation. + provider: ProviderId, + /// Model ID to use for command suggestion generation. + model: ModelId, + }, } /// Type-safe subcommands for `forge config get`. @@ -564,6 +571,8 @@ pub enum ConfigGetField { Provider, /// Get the commit message generation config. Commit, + /// Get the command suggestion generation config. + Suggest, } /// Command group for conversation management. diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 659a419e44..03926c9055 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -1337,12 +1337,24 @@ impl A + Send + Sync> UI { .map(|m| m.as_str().to_string()) .unwrap_or_else(|| markers::EMPTY.to_string()); + let suggest_config = self.api.get_suggest_config().await.ok().flatten(); + let suggest_provider = suggest_config + .as_ref() + .map(|c| c.provider.to_string()) + .unwrap_or_else(|| markers::EMPTY.to_string()); + let suggest_model = suggest_config + .as_ref() + .map(|c| c.model.as_str().to_string()) + .unwrap_or_else(|| markers::EMPTY.to_string()); + let info = Info::new() .add_title("CONFIGURATION") .add_key_value("Default Model", model) .add_key_value("Default Provider", provider) .add_key_value("Commit Provider", commit_provider) - .add_key_value("Commit Model", commit_model); + .add_key_value("Commit Model", commit_model) + .add_key_value("Suggest Provider", suggest_provider) + .add_key_value("Suggest Model", suggest_model); if porcelain { self.writeln( @@ -3169,6 +3181,18 @@ impl A + Send + Sync> UI { .sub_title(format!("is now the commit model for provider '{provider}'")), )?; } + ConfigSetField::Suggest { provider, model } => { + // Validate provider exists and model belongs to that specific provider + let validated_model = self.validate_model(model.as_str(), Some(&provider)).await?; + let suggest_config = forge_domain::SuggestConfig { + provider: provider.clone(), + model: validated_model.clone(), + }; + self.api.set_suggest_config(suggest_config).await?; + self.writeln_title(TitleFormat::action(validated_model.as_str()).sub_title( + format!("is now the suggest model for provider '{provider}'"), + ))?; + } } Ok(()) @@ -3208,7 +3232,7 @@ impl A + Send + Sync> UI { Some(config) => { let provider = config .provider - .map(|p| p.to_string()) + .map(|p| p.as_ref().to_string()) .unwrap_or_else(|| "Not set".to_string()); let model = config .model @@ -3220,6 +3244,16 @@ impl A + Send + Sync> UI { None => self.writeln("Commit: Not set")?, } } + ConfigGetField::Suggest => { + let suggest_config = self.api.get_suggest_config().await?; + match suggest_config { + Some(config) => { + self.writeln(config.provider.as_ref())?; + self.writeln(config.model.as_str().to_string())?; + } + None => self.writeln("Suggest: Not set")?, + } + } } Ok(()) diff --git a/crates/forge_services/src/app_config.rs b/crates/forge_services/src/app_config.rs index f68ae0ce03..42c2f017e7 100644 --- a/crates/forge_services/src/app_config.rs +++ b/crates/forge_services/src/app_config.rs @@ -95,6 +95,21 @@ impl AppConfigService }) .await } + + async fn get_suggest_config(&self) -> anyhow::Result> { + let config = self.infra.get_app_config().await?; + Ok(config.suggest) + } + + async fn set_suggest_config( + &self, + suggest_config: forge_domain::SuggestConfig, + ) -> anyhow::Result<()> { + self.update(|config| { + config.suggest = Some(suggest_config); + }) + .await + } } #[cfg(test)] diff --git a/shell-plugin/lib/actions/config.zsh b/shell-plugin/lib/actions/config.zsh index bce098a0d2..2b3d90a1c7 100644 --- a/shell-plugin/lib/actions/config.zsh +++ b/shell-plugin/lib/actions/config.zsh @@ -191,6 +191,31 @@ function _forge_action_commit_model() { ) } +# Action handler: Select model for command suggestion generation +# Calls `forge config set suggest ` on selection. +function _forge_action_suggest_model() { + local input_text="$1" + ( + echo + local current_suggest_model + current_suggest_model=$(_forge_exec config get suggest 2>/dev/null | tail -n 1) + + local selected + selected=$(_forge_pick_model "Suggest Model ❯ " "$current_suggest_model" "$input_text") + + if [[ -n "$selected" ]]; then + # Field 1 = model_id (raw), field 4 = provider_id (raw) + local model_id provider_id + read -r model_id provider_id <<<$(echo "$selected" | awk -F ' +' '{print $1, $4}') + + model_id=${model_id//[[:space:]]/} + provider_id=${provider_id//[[:space:]]/} + + _forge_exec config set suggest "$provider_id" "$model_id" + fi + ) +} + # Action handler: Sync workspace for codebase search function _forge_action_sync() { echo diff --git a/shell-plugin/lib/dispatcher.zsh b/shell-plugin/lib/dispatcher.zsh index 46061c111b..0518cb161a 100644 --- a/shell-plugin/lib/dispatcher.zsh +++ b/shell-plugin/lib/dispatcher.zsh @@ -169,6 +169,9 @@ function forge-accept-line() { config-commit-model|ccm) _forge_action_commit_model "$input_text" ;; + suggest-model|sm) + _forge_action_suggest_model "$input_text" + ;; tools|t) _forge_action_tools ;;