Merge PR #6687 (fix/first-run-keeps-configured-provider) into integrate/0.10.1

This commit is contained in:
CodeWhale Bot committed 2026-09-27 07:27:56 -07:00
commit f45d1df6cb
8 files changed
+157 -10

No files matched your search

+3
View File
@@ -129,6 +129,9 @@ quieter, and Fleet runs can be checked before they spend anything.
- Keep reasoning effort labels, including `xhigh` and `ultra`, visible beside
short model names in the 80-column terminal footer.
- First-run TUI startup keeps explicitly configured providers and models,
including environment overrides, instead of adopting a detected local Ollama
model. Empty sessions no longer show a full context window from the startup prompt.
- `/trust on|off` changes file-tool trust for this session. Add `--save` to
persist workspace trust for project skills, commands, hooks, MCP servers,
and project context. Skipped-skill notices stay out of conversation titles
+3
View File
@@ -129,6 +129,9 @@ quieter, and Fleet runs can be checked before they spend anything.
- Keep reasoning effort labels, including `xhigh` and `ultra`, visible beside
short model names in the 80-column terminal footer.
- First-run TUI startup keeps explicitly configured providers and models,
including environment overrides, instead of adopting a detected local Ollama
model. Empty sessions no longer show a full context window from the startup prompt.
- `/trust on|off` changes file-tool trust for this session. Add `--save` to
persist workspace trust for project skills, commands, hooks, MCP servers,
and project context. Skipped-skill notices stay out of conversation titles
+8 -5
View File
@@ -1,10 +1,10 @@
//! First-run / missing-key adoption of a live local Ollama catalog.
//! Unconfigured first-run adoption of a live local Ollama catalog.
//!
//! Virgin sessions default to the DeepSeek costume (`deepseek-flash`). When a
//! real local daemon answers `GET /api/tags` (or the OpenAI-compat
//! `GET /v1/models` roster), the painted route must switch to a tag that
//! actually exists — never leave DeepSeek flash as the chrome while a live
//! local catalog is sitting on `:11434`.
//! actually exists. Discovery never replaces an explicit provider/model,
//! including a configured route whose credentials need repair.
use std::time::Duration;
@@ -129,13 +129,16 @@ pub(crate) fn parse_ollama_show_response(payload: &str) -> anyhow::Result<Ollama
/// True when this session should adopt a live local catalog into chrome.
///
/// First-run and missing-key recovery paint DeepSeek by default; a live local
/// roster must replace that costume. An already-keyed hosted route is left alone,
/// An unconfigured first run paints DeepSeek by default; a live local
/// roster can replace that default. Explicit routes are left alone,
/// and so is a provider picker the person has already started using: the
/// probe answers late, and switching provider under them would close the
/// picker mid-choice or mid-key.
#[must_use]
pub(crate) fn should_adopt_live_local_ollama(app: &mut crate::tui::app::App) -> bool {
if app.startup_route_configured {
return false;
}
if app.api_provider == ApiProvider::Ollama {
// Already on Ollama — route_runtime + #5795 own the tag; don't fight it.
return false;
+4
View File
@@ -1634,6 +1634,10 @@ pub struct App {
/// Updated by `/provider` switches so the UI/commands can read the
/// active backend without re-deriving it from the live config.
pub api_provider: ApiProvider,
/// The resolved startup config named a provider or model. Capture this
/// before runtime synchronization writes even the built-in route to Config;
/// missing credentials must not make that choice eligible for discovery.
pub(crate) startup_route_configured: bool,
/// Exact configured provider key for persistence and route restoration.
/// Built-ins use their canonical slug; named custom providers retain the
/// user-owned key instead of collapsing to `custom`.
+10 -1
View File
@@ -132,7 +132,15 @@ impl App {
}
let selected = startup_config.apply_saved_selection(&settings);
let config = &startup_config;
let model = if selected {
let startup_route_configured = config.provider.is_some()
|| config.default_text_model.is_some()
|| config.legacy_model.is_some()
|| config
.provider_config_for(config.api_provider())
.is_some_and(|entry| entry.model.is_some());
// Provider and model come from the same resolved config, even on the
// first run. An options default must not replace a configured model.
let model = if selected || startup_route_configured {
config.default_model()
} else {
model
@@ -965,6 +973,7 @@ impl App {
redaction_gate_confirming: false,
redaction_gate_scroll: std::cell::Cell::new(0),
onboarding_needs_api_key: needs_api_key,
startup_route_configured,
onboarding_provider: provider,
onboarding_workspace_trust_gate,
onboarding_missing_key_recovery,
+4 -4
View File
@@ -1762,10 +1762,7 @@ pub(crate) async fn run_event_loop(
if let Some(ref handle) = local_ollama_probe {
local_done = handle.is_finished();
}
if local_done
&& let Ok(Some(catalog)) = local_ollama_probe.take().unwrap().await
&& crate::local_ollama::should_adopt_live_local_ollama(app)
{
if local_done && let Ok(Some(catalog)) = local_ollama_probe.take().unwrap().await {
adopt_live_local_ollama_catalog(app, &mut engine_handle, config, catalog).await;
}
@@ -6934,6 +6931,9 @@ pub(super) async fn adopt_live_local_ollama_catalog(
config: &mut Config,
catalog: crate::local_ollama::LiveLocalOllamaCatalog,
) {
if !crate::local_ollama::should_adopt_live_local_ollama(app) {
return;
}
let Some(tag) = catalog.preferred_tag().map(str::to_string) else {
return;
};
+9
View File
@@ -2173,6 +2173,15 @@ pub(crate) fn context_usage_snapshot(app: &App) -> Option<(i64, u32, f64)> {
}
pub(crate) fn context_usage_snapshot_for_window(app: &App, max: u32) -> Option<(i64, u32, f64)> {
// Before a conversation starts, the assembled startup prompt alone is not
// conversation usage, and compacting an empty session cannot reclaim it.
// Once messages or provider usage exist, retain the real pressure reading.
if app.api_messages.is_empty()
&& app.session.last_prompt_tokens.unwrap_or(0) == 0
&& app.last_billed_input_tokens.unwrap_or(0) == 0
{
return Some((0, max, 0.0));
}
let max_i64 = i64::from(max);
let reported = app
.session
+116
View File
@@ -10370,6 +10370,122 @@ async fn adopting_a_local_model_closes_the_connect_picker_and_names_it() {
);
}
#[test]
fn first_run_route_starts_with_configured_provider_and_model() {
use crate::test_support::EnvVarGuard;
for from_env in [false, true] {
let _home = SettingsHomeGuard::new();
let _env: Vec<_> = [
"CODEWHALE_MODEL",
"DEEPSEEK_MODEL",
"CODEWHALE_BASE_URL",
"DEEPSEEK_BASE_URL",
"OPENAI_BASE_URL",
"OPENAI_MODEL",
]
.into_iter()
.map(EnvVarGuard::remove)
.collect();
let path = crate::config::home_config_path().unwrap();
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let route = if from_env {
""
} else {
"provider = 'openai'\ndefault_text_model = 'gpui-fixture'\n"
};
std::fs::write(&path, format!("{route}[providers.openai]\napi_key = 'fixture-key'\nbase_url = 'http://127.0.0.1:9/v1'\n")).unwrap();
let _provider = from_env.then(|| EnvVarGuard::set("CODEWHALE_PROVIDER", "openai"));
let _model = from_env.then(|| EnvVarGuard::set("CODEWHALE_MODEL", "gpui-fixture"));
let config = Config::load(None, None).expect("load fresh home config");
assert_eq!(config.api_provider(), ApiProvider::Openai);
assert_eq!(config.default_model(), "gpui-fixture");
// An options default must not replace the resolved config's model.
let mut options = create_test_options();
options.model = DEFAULT_TEXT_MODEL.to_string();
let mut app = App::new(options, &config);
assert_eq!(app.api_provider, ApiProvider::Openai);
assert_eq!(app.model, "gpui-fixture");
assert!(!crate::local_ollama::should_adopt_live_local_ollama(
&mut app
));
assert!(codewhale_config::SetupState::load().unwrap().is_none());
assert_eq!(
Config::load(None, None).unwrap().default_model(),
"gpui-fixture"
);
}
}
#[tokio::test]
async fn first_run_route_keeps_explicit_choice_when_credentials_are_missing() {
let _home = SettingsHomeGuard::new();
for document in [
"provider = 'openai'\ndefault_text_model = 'gpui-fixture'\n",
"provider = 'openai'\n",
"default_text_model = 'deepseek-v4-flash'\n",
"[providers.deepseek]\nmodel = 'deepseek-v4-flash'\n",
] {
let mut config = Config::from_saved_document(document, None).unwrap();
let provider = config.api_provider();
let model = config.default_model();
let mut options = create_test_options();
options.model = model.clone();
let mut app = App::new(options, &config);
// A missing/rejected key can enter recovery after construction too.
app.onboarding_needs_api_key = true;
app.onboarding_missing_key_recovery = true;
sync_config_provider_from_app(&mut config, &app);
assert!(!crate::local_ollama::should_adopt_live_local_ollama(
&mut app
));
let mut engine = mock_engine_handle();
super::event_loop::adopt_live_local_ollama_catalog(
&mut app,
&mut engine.handle,
&mut config,
crate::local_ollama::LiveLocalOllamaCatalog {
endpoint_v1: "http://127.0.0.1:11434/v1".into(),
tags: vec!["qwen3:4b".into()],
chat_tag: Some("qwen3:4b".into()),
},
)
.await;
assert_eq!(app.api_provider, provider);
assert_eq!(app.model, model);
assert_eq!(config.api_provider(), provider);
assert!(codewhale_config::SetupState::load().unwrap().is_none());
}
}
#[test]
fn first_run_route_context_is_empty_until_conversation_starts() {
let mut app = create_test_app();
app.set_provider_identity(ApiProvider::Ollama, "ollama");
app.set_model_selection("qwen3:4b".into());
app.active_route_limits = Some(codewhale_config::route::RouteLimits {
context_tokens: Some(8192),
..Default::default()
});
app.system_prompt = Some(SystemPrompt::Text("startup instructions ".repeat(8192)));
assert!(app.api_messages.is_empty());
assert_eq!(context_usage_snapshot(&app), Some((0, 8192, 0.0)));
assert_eq!(crate::tui::phase_strip::context_percent_from_app(&app), 0);
// A real conversation still exposes pressure; the fix must not cap it.
app.api_messages_mut().push(Message {
role: Role::User,
content: vec![ContentBlock::Text {
text: "hello".into(),
cache_control: None,
}],
});
assert_eq!(context_usage_snapshot(&app), Some((8192, 8192, 100.0)));
}
#[tokio::test]
async fn provider_switch_to_deepseek_canonicalizes_openrouter_default_model() {
let _home = SettingsHomeGuard::new();