mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
Merge PR #6687 (fix/first-run-keeps-configured-provider) into integrate/0.10.1
This commit is contained in:
commit
f45d1df6cb
8 files changed
+157
-10
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user