Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
cf8aef7
feat(model): allow /model command to switch model and provider together
laststylebender14 Feb 20, 2026
4614727
perf(model): fetch models from all providers concurrently
laststylebender14 Feb 20, 2026
02dd375
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 20, 2026
f97091c
refactor(model): remove CliModel struct and its display implementation
laststylebender14 Feb 20, 2026
baca17d
feat(model): display provider as separate suffix in model output
laststylebender14 Feb 20, 2026
f69f6ee
fix(model): hide empty info brackets in model display
laststylebender14 Feb 20, 2026
2619c18
fix(ui): stop spinner on model loading error
laststylebender14 Feb 20, 2026
a954b95
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 20, 2026
8601d9b
refactor(provider): use ProviderModels struct instead of tuple
laststylebender14 Feb 20, 2026
8c3fc65
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 20, 2026
ec29070
refactor(model): move caching from in-memory to CacacheStorage with TTL
laststylebender14 Feb 23, 2026
47a40a8
Merge branch 'main' into feat/model-provider-switch-command
laststylebender14 Feb 23, 2026
652bc5d
refactor(model): remove provider switching from model selection
laststylebender14 Feb 23, 2026
fb59e25
refactor(model): reorder CliProvider impl and update display tests
laststylebender14 Feb 23, 2026
bc240e1
- add new lines
laststylebender14 Feb 23, 2026
29dd26f
perf(shell-plugin): consolidate model field extraction
laststylebender14 Feb 23, 2026
bbc2878
feat(config): add commit model configuration command
laststylebender14 Feb 23, 2026
7efa213
refactor(model): validate models against specific provider
laststylebender14 Feb 23, 2026
cf5b5e1
refactor(git_app): simplify provider and model resolution
laststylebender14 Feb 23, 2026
f85e09a
refactor(git_app): parallelize async operations
laststylebender14 Feb 23, 2026
4a885dc
fix(git_app): improve error logging for unavailable commit provider
laststylebender14 Feb 23, 2026
8ec080a
refactor(ui): streamline commit configuration display
laststylebender14 Feb 23, 2026
4e09673
refactor(shell-plugin): simplify config get commands
laststylebender14 Feb 23, 2026
6f0a7a6
Merge branch 'main' into feat/model-provider-switch-command
laststylebender14 Feb 25, 2026
5343a44
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 25, 2026
0489806
Merge branch 'main' into feat/model-provider-switch-command
tusharmath Feb 25, 2026
f7d268e
Merge branch 'feat/model-provider-switch-command' into feat/add-confi…
laststylebender14 Feb 25, 2026
9acf91c
Merge branch 'main' into feat/model-provider-switch-command
laststylebender14 Feb 25, 2026
b64f7c8
Merge branch 'main' into feat/model-provider-switch-command
tusharmath Mar 2, 2026
c513775
fix(ui): update spinner text to 'Fetching Models' on model list load
tusharmath Mar 2, 2026
92e8009
Merge branch 'main' into feat/model-provider-switch-command
laststylebender14 Mar 2, 2026
e51490a
refactor(chat): extract provider router and add background cache refresh
laststylebender14 Mar 2, 2026
def765d
feat(chat): implement background task management for model cache refresh
laststylebender14 Mar 2, 2026
a546138
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 2, 2026
c72b344
Merge branch 'feat/model-provider-switch-command' into feat/add-confi…
laststylebender14 Mar 2, 2026
0b25540
Merge branch 'main' into feat/add-config-commit-command
laststylebender14 Mar 2, 2026
926b52a
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 2, 2026
fd8fdc9
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Mar 2, 2026
d139c64
Merge branch 'main' into feat/add-config-commit-command
laststylebender14 Mar 5, 2026
ef27993
Merge branch 'main' into feat/add-config-commit-command
tusharmath Mar 9, 2026
b91651d
fix(git-app): fall back to active provider when credential refresh fails
laststylebender14 Mar 9, 2026
cbd5e6a
feat(config): add suggest-model command for command suggestion
laststylebender14 Mar 9, 2026
4e0883e
fix(git-app): correct match statement for provider credential refresh
laststylebender14 Mar 9, 2026
b4b9643
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 9, 2026
7a1d5de
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Mar 9, 2026
65403b1
Merge branch 'feat/add-config-commit-command' into feat/add-suggest-m…
laststylebender14 Mar 9, 2026
a305b56
refactor(command_generator): streamline suggest config retrieval in c…
laststylebender14 Mar 9, 2026
b2aec5a
Merge branch 'main' into feat/add-suggest-model-command
laststylebender14 Mar 10, 2026
5dc7cbc
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 10, 2026
5789a73
fix(ui): update provider handling to use as_ref for better string con…
laststylebender14 Mar 10, 2026
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
8 changes: 8 additions & 0 deletions crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<forge_domain::SuggestConfig>>;

/// 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<()>;

Expand Down
8 changes: 8 additions & 0 deletions crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,14 @@ impl<
self.services.set_commit_config(config).await
}

async fn get_suggest_config(&self) -> anyhow::Result<Option<SuggestConfig>> {
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<Option<LoginInfo>> {
self.services.auth_service().get_auth_token().await
}
Expand Down
26 changes: 22 additions & 4 deletions crates/forge_app/src/command_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!("<task>{}</task>", prompt.as_str());
Expand Down Expand Up @@ -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<Option<forge_domain::SuggestConfig>> {
Ok(None)
}

async fn set_suggest_config(&self, _config: forge_domain::SuggestConfig) -> Result<()> {
Ok(())
}
}

#[tokio::test]
Expand Down
16 changes: 16 additions & 0 deletions crates/forge_app/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<forge_domain::SuggestConfig>>;

/// 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]
Expand Down Expand Up @@ -1048,6 +1056,14 @@ impl<I: Services> 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<Option<forge_domain::SuggestConfig>> {
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]
Expand Down
4 changes: 3 additions & 1 deletion crates/forge_domain/src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -23,6 +23,8 @@ pub struct AppConfig {
pub model: HashMap<ProviderId, ModelId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<CommitConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub suggest: Option<SuggestConfig>,
}

#[derive(Clone, Serialize, Deserialize, From, Debug, PartialEq)]
Expand Down
2 changes: 2 additions & 0 deletions crates/forge_domain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod session_metrics;
mod shell;
mod skill;
mod snapshot;
mod suggest_config;
mod suggestion;
mod system_context;
mod temperature;
Expand Down Expand Up @@ -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::*;
Expand Down
22 changes: 22 additions & 0 deletions crates/forge_domain/src/suggest_config.rs
Original file line number Diff line number Diff line change
@@ -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,
}
4 changes: 4 additions & 0 deletions crates/forge_main/src/built_in_commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand Down
9 changes: 9 additions & 0 deletions crates/forge_main/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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.
Expand Down
38 changes: 36 additions & 2 deletions crates/forge_main/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1337,12 +1337,24 @@ impl<A: API + ConsoleWriter + 'static, F: Fn() -> A + Send + Sync> UI<A, F> {
.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(
Expand Down Expand Up @@ -3169,6 +3181,18 @@ impl<A: API + ConsoleWriter + 'static, F: Fn() -> A + Send + Sync> UI<A, F> {
.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(())
Expand Down Expand Up @@ -3208,7 +3232,7 @@ impl<A: API + ConsoleWriter + 'static, F: Fn() -> A + Send + Sync> UI<A, F> {
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
Expand All @@ -3220,6 +3244,16 @@ impl<A: API + ConsoleWriter + 'static, F: Fn() -> A + Send + Sync> UI<A, F> {
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(())
Expand Down
15 changes: 15 additions & 0 deletions crates/forge_services/src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ impl<F: ProviderRepository + AppConfigRepository + Send + Sync> AppConfigService
})
.await
}

async fn get_suggest_config(&self) -> anyhow::Result<Option<forge_domain::SuggestConfig>> {
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)]
Expand Down
25 changes: 25 additions & 0 deletions shell-plugin/lib/actions/config.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,31 @@ function _forge_action_commit_model() {
)
}

# Action handler: Select model for command suggestion generation
# Calls `forge config set suggest <provider_id> <model_id>` 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
Expand Down
3 changes: 3 additions & 0 deletions shell-plugin/lib/dispatcher.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand Down
Loading