mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 17:10:44 -10:00
fix(tui): no-key first run sees cloud providers and Ollama keeps its real context
#7114: /provider counted every known id as configured because provider_config_table!(@read) is always Some once [providers] exists, so a fresh home showed a phantom "Antigravity (legacy, disabled)" row. The picker now asks provider_is_configured, the same resolver the rest of the runtime uses. A home whose only configured row is a keyless local runtime opens on the full catalog (DeepSeek etc.) instead of Configured, and Shift+A toggles the view like a. #7113: at dispatch an unloaded Ollama model has no /api/ps context, so the route fell back to 8192 and the second turn was refused with "~5120 tokens usable". refresh_unverified_local_context now asks the daemon to load the model (POST /api/generate, no prompt) when the context is still unknown and refreshes again. The dispatched turn's route limits are carried back into the App so the footer and compaction budget read the same window (it showed "context 100%" and the /compact nag before). #7116: provider_lake::models_for_provider also lists a provider whose own endpoint has answered a live listing, so a keyless Ollama that is not the active route shows its installed models in /model and GET /v1/providers/ollama/models. One resolver: the runtime API and model inventory both already go through models_for_provider. No-key default kept as Ollama adoption: a keyless DeepSeek default cannot answer a turn; with #7114 DeepSeek is now one Enter away in /provider. Evidence (dev build, isolated env -i CODEWHALE_HOME): - /provider on a fresh home: opens "Provider · all", no Antigravity row, Shift+A -> configured view, a -> back. - qwen3:4b unloaded (/api/ps empty): turn 1 read+patch calc.py, turn 2 wrote notes.md; both completed, footer context 34% then 52%; /api/ps 32768. Before: turn 1 failed with "~5120 tokens usable". - serve --http, DeepSeek active: after /v1/providers/ollama/models/refresh, GET /v1/providers/ollama/models lists qwen3:4b (context 32768); was []. - scripts/dev-cargo.sh test -p codewhale-tui --lib -- provider_picker provider_lake ollama_catalog list_provider_models provider_models dispatch context_usage: 443 passed; 0 failed. Paths: crates/tui/src/tui/provider_picker.rs, crates/tui/src/provider_lake.rs, crates/tui/src/client/ollama_catalog.rs, crates/tui/src/tui/ui/dispatch.rs, crates/tui/src/tui/ui.rs (UserDispatchOutcome field only), CHANGELOG.md and crates/tui/CHANGELOG.md (one line each). Refs #7113, #7114, #7116 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e5283cb022
commit
c21e0b1e14
7 files changed
+92
-25
No files matched your search
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Change mode and permissions while the agent works:** Tab, Shift+Tab, `/mode`, the hotbar and `/config mode` no longer refuse while a turn is running. A response that is already streaming is not restarted; the change applies at the agent's next step in the same turn (before its next tool batch, at every approval gate, and before its next model request, which carries the new mode notice). The status line says so until it applies. A call still waiting for approval goes back to the model under the new posture, and a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends. (CW-7552)
|
||||
- **Steering interrupts thinking:** a steer (Ctrl+Enter, or Enter twice after queueing) now closes the in-flight model request at once, whether the model is waiting on headers, reasoning, writing text or emitting a tool call, and continues the same turn with your message. Text already shown is kept as interrupted output; unfinished reasoning and partial tool calls are discarded, never run. (CW-7552)
|
||||
- **Switching provider lands on its current model:** `/provider stepfun` now selects Step 5 Preview instead of the older Step 3.7 Flash; a model you saved for a provider is still kept. `/model` now lists the current provider's own models (bundled and live, newest first, up to 20) instead of only the selected one. Search also matches ids typed without separators (`step5`). While you search, Shift+P and Shift+F type their letters rather than pinning or adding to Fleet, so `Step-5-Preview` can be typed in full; Ctrl+P and Ctrl+F pin or add the highlighted row at any time.
|
||||
- **First run without a key:** `/provider` now opens on the full provider list (DeepSeek and the other cloud providers) when only a keyless local runtime is set up, no longer shows a disabled Antigravity row that was never configured, and Shift+A toggles the view like `a`. A local Ollama model that is not loaded yet is loaded before the first turn, so the route uses its real context window (for example 32K for `qwen3:4b`) instead of an 8K fallback that refused the second message with "~5120 tokens usable"; the footer's context reading follows the same window. An installed Ollama model is listed in `/model` and `GET /v1/providers/ollama/models` once its own catalog has answered, even when another provider is active. (#7113, #7114, #7116)
|
||||
- **Remote control works again:** `codewhale rc` (`/rc`) can attach to the hosted control plane for web Chat again. The control plane had only accepted the retired 0.9.13 runtime identity, so every 0.10.x Engine was refused with `runner_runtime_identity_unavailable`; it now accepts the exact released runtime identity (commit, image and digest) and fails closed when they are not configured.
|
||||
- **Modern MCP client:** MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker by default (`mcp_backend = "host"`; `"rust"` stays available for one release). Servers that speak the 2026-07-28 revision are discovered with `server/discover`; other stdio and HTTP servers fall back to the legacy `initialize` handshake, and configured SSE servers keep theirs. Rust keeps process, network, credential, catalog, approval and cancellation authority. (CW-6713)
|
||||
- **MCP compatibility fixes:** older servers that answer the discovery probe with an error or an unrelated result now connect through the legacy handshake instead of being refused after the probe. A server that rejects sign-in reports its real 401 and the `/mcp login` hint rather than the probe's "method not found". A stale OAuth token is refreshed once when the probe itself is rejected. A server whose command cannot start now says why (for example, command not found) instead of a generic broker refusal.
|
||||
|
||||
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Change mode and permissions while the agent works:** Tab, Shift+Tab, `/mode`, the hotbar and `/config mode` no longer refuse while a turn is running. A response that is already streaming is not restarted; the change applies at the agent's next step in the same turn (before its next tool batch, at every approval gate, and before its next model request, which carries the new mode notice). The status line says so until it applies. A call still waiting for approval goes back to the model under the new posture, and a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends. (CW-7552)
|
||||
- **Steering interrupts thinking:** a steer (Ctrl+Enter, or Enter twice after queueing) now closes the in-flight model request at once, whether the model is waiting on headers, reasoning, writing text or emitting a tool call, and continues the same turn with your message. Text already shown is kept as interrupted output; unfinished reasoning and partial tool calls are discarded, never run. (CW-7552)
|
||||
- **Switching provider lands on its current model:** `/provider stepfun` now selects Step 5 Preview instead of the older Step 3.7 Flash; a model you saved for a provider is still kept. `/model` now lists the current provider's own models (bundled and live, newest first, up to 20) instead of only the selected one. Search also matches ids typed without separators (`step5`). While you search, Shift+P and Shift+F type their letters rather than pinning or adding to Fleet, so `Step-5-Preview` can be typed in full; Ctrl+P and Ctrl+F pin or add the highlighted row at any time.
|
||||
- **First run without a key:** `/provider` now opens on the full provider list (DeepSeek and the other cloud providers) when only a keyless local runtime is set up, no longer shows a disabled Antigravity row that was never configured, and Shift+A toggles the view like `a`. A local Ollama model that is not loaded yet is loaded before the first turn, so the route uses its real context window (for example 32K for `qwen3:4b`) instead of an 8K fallback that refused the second message with "~5120 tokens usable"; the footer's context reading follows the same window. An installed Ollama model is listed in `/model` and `GET /v1/providers/ollama/models` once its own catalog has answered, even when another provider is active. (#7113, #7114, #7116)
|
||||
- **Remote control works again:** `codewhale rc` (`/rc`) can attach to the hosted control plane for web Chat again. The control plane had only accepted the retired 0.9.13 runtime identity, so every 0.10.x Engine was refused with `runner_runtime_identity_unavailable`; it now accepts the exact released runtime identity (commit, image and digest) and fails closed when they are not configured.
|
||||
- **Modern MCP client:** MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker by default (`mcp_backend = "host"`; `"rust"` stays available for one release). Servers that speak the 2026-07-28 revision are discovered with `server/discover`; other stdio and HTTP servers fall back to the legacy `initialize` handshake, and configured SSE servers keep theirs. Rust keeps process, network, credential, catalog, approval and cancellation authority. (CW-6713)
|
||||
- **MCP compatibility fixes:** older servers that answer the discovery probe with an error or an unrelated result now connect through the legacy handshake instead of being refused after the probe. A server that rejects sign-in reports its real 401 and the `/mcp login` hint rather than the probe's "method not found". A stale OAuth token is refreshed once when the probe itself is rejected. A server whose command cannot start now says why (for example, command not found) instead of a generic broker refusal.
|
||||
|
||||
@@ -224,6 +224,25 @@ pub(crate) fn probe_client() -> anyhow::Result<reqwest::Client> {
|
||||
.build()?)
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
|
||||
pub(crate) const MODEL_LOAD_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
|
||||
pub(crate) async fn load_model(origin: &str, tag: &str) -> bool {
|
||||
let Ok(client) = codewhale_release::tls::reqwest_client_builder()
|
||||
.timeout(MODEL_LOAD_TIMEOUT)
|
||||
.build()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
client
|
||||
.post(format!("{origin}/api/generate"))
|
||||
.json(&serde_json::json!({ "model": tag }))
|
||||
.send()
|
||||
.await
|
||||
.is_ok_and(|response| response.status().is_success())
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
|
||||
expect(dead_code)
|
||||
|
||||
@@ -1505,18 +1505,29 @@ pub fn configured_providers(config: &Config) -> Vec<ProviderIdentity> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Catalog models for providers that qualify as configured for `active`.
|
||||
/// Catalog models for providers that qualify as configured for `active`, or
|
||||
/// whose own endpoint has answered a live listing (a keyless local runtime
|
||||
/// that is not the active route).
|
||||
#[must_use]
|
||||
pub fn models_for_provider(config: &Config, identity: &ProviderIdentity) -> Vec<String> {
|
||||
let Ok(active) = config.active_provider_identity() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if provider_is_configured_for_active(config, identity, &active) {
|
||||
let base_url = config.base_url_for_route(identity);
|
||||
let endpoint_listed = crate::provider_catalog_live::cached_entry_for_route(
|
||||
identity.provider,
|
||||
identity.key.as_str(),
|
||||
&base_url,
|
||||
)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some_and(|entry| entry.fetched_at > 0 && !entry.offerings.is_empty());
|
||||
if endpoint_listed || provider_is_configured_for_active(config, identity, &active) {
|
||||
configured_catalog_models_for_route(
|
||||
config,
|
||||
identity.provider,
|
||||
identity.key.as_str(),
|
||||
&config.base_url_for_route(identity),
|
||||
&base_url,
|
||||
)
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -1991,24 +2002,44 @@ pub(crate) async fn refresh_unverified_local_context(
|
||||
if identity.provider != ProviderKind::Ollama {
|
||||
return;
|
||||
}
|
||||
let provider = identity.provider;
|
||||
let key = identity.key.as_str().to_string();
|
||||
let base_url = config.base_url_for_route(identity);
|
||||
let model = model.to_string();
|
||||
let known = tokio::task::spawn_blocking(move || {
|
||||
catalog_offering_for_route(provider, &key, &base_url, &model)
|
||||
.and_then(|row| row.limit)
|
||||
.and_then(|limit| limit.context)
|
||||
.is_some()
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if !known {
|
||||
let _ = tokio::time::timeout(
|
||||
let known = || {
|
||||
let provider = identity.provider;
|
||||
let key = identity.key.as_str().to_string();
|
||||
let base_url = base_url.clone();
|
||||
let model = model.to_string();
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
catalog_offering_for_route(provider, &key, &base_url, &model)
|
||||
.and_then(|row| row.limit)
|
||||
.and_then(|limit| limit.context)
|
||||
.is_some()
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
};
|
||||
let refresh = || {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(3),
|
||||
refresh_local_ollama_catalog(config, identity),
|
||||
)
|
||||
.await;
|
||||
};
|
||||
if known().await {
|
||||
return;
|
||||
}
|
||||
let _ = refresh().await;
|
||||
if known().await {
|
||||
return;
|
||||
}
|
||||
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
|
||||
if crate::client::ollama_catalog::load_model(
|
||||
&crate::client::ollama_catalog::ollama_native_origin(&base_url),
|
||||
model,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let _ = refresh().await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2002,9 +2002,15 @@ impl ProviderPickerView {
|
||||
let active = active_identity
|
||||
.as_ref()
|
||||
.is_some_and(|identity| identity.key.as_str() == row.id);
|
||||
let configured = config.providers.as_ref().is_some_and(|providers| {
|
||||
codewhale_config::provider_config_table!(@read providers, row.id).is_some()
|
||||
});
|
||||
let configured = codewhale_runtime::config::provider_is_configured(
|
||||
row.kind,
|
||||
false,
|
||||
false,
|
||||
config.providers.as_ref().and_then(
|
||||
|providers| codewhale_config::provider_config_table!(@read providers, row.id),
|
||||
),
|
||||
false,
|
||||
);
|
||||
if active || configured {
|
||||
rows.push(ProviderDashboardRow::from_config_with_provider_id(
|
||||
row.kind,
|
||||
@@ -2038,10 +2044,14 @@ impl ProviderPickerView {
|
||||
.position(|row| row.is_active)
|
||||
.or_else(|| rows.iter().position(|row| row.provider == active))
|
||||
.unwrap_or(0);
|
||||
// Default to the configured-only view (#3830); if nothing is
|
||||
// configured yet (a fresh install), open straight on the full
|
||||
// catalog instead of an empty list with no obvious next step.
|
||||
let view = if rows.iter().any(|row| row.is_configured) {
|
||||
// Default to the configured-only view (#3830); if nothing beyond an
|
||||
// adopted local runtime is configured yet (a fresh install), open
|
||||
// straight on the full catalog so the cloud providers are visible.
|
||||
let view = if rows.iter().any(|row| {
|
||||
row.is_configured
|
||||
&& row.provider.provider().credential_help().acquisition
|
||||
!= CredentialAcquisition::LocalOptional
|
||||
}) {
|
||||
ProviderListView::Configured
|
||||
} else {
|
||||
ProviderListView::Catalog
|
||||
@@ -4897,7 +4907,7 @@ impl ModalView for ProviderPickerView {
|
||||
// type-ahead arm so `a`/`A` always toggles instead of
|
||||
// seeking a provider whose name starts with "a".
|
||||
KeyCode::Char(c)
|
||||
if key.modifiers.is_empty()
|
||||
if (key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT)
|
||||
&& self.query.is_empty()
|
||||
&& c.eq_ignore_ascii_case(&'a') =>
|
||||
{
|
||||
|
||||
@@ -829,6 +829,7 @@ pub(crate) struct UserDispatchOutcome {
|
||||
effective_provider_label: String,
|
||||
effective_reasoning_effort: EffectiveReasoningEffort,
|
||||
auto_selection: Option<crate::model_routing::AutoRouteSelection>,
|
||||
route_limits: codewhale_config::route::RouteLimits,
|
||||
}
|
||||
|
||||
/// Tell the operator that an explicit "make this my default" request did not
|
||||
|
||||
@@ -996,6 +996,7 @@ pub(crate) async fn spawned_dispatch_inner(
|
||||
scope: prepare.cost_scope,
|
||||
batch: Some(initial_routed_usage.clone()),
|
||||
};
|
||||
let route_limits = turn_route.candidate.limits();
|
||||
let op = Op::SendMessage(TurnSpec {
|
||||
profile_constitution: None,
|
||||
max_output_tokens: None,
|
||||
@@ -1040,6 +1041,7 @@ pub(crate) async fn spawned_dispatch_inner(
|
||||
effective_provider_label,
|
||||
effective_reasoning_effort: effective_reasoning_receipt,
|
||||
auto_selection,
|
||||
route_limits,
|
||||
};
|
||||
Box::new(move |app, current_engine, config| {
|
||||
// Admission stays serialized by this flag until its callback retires,
|
||||
@@ -1145,6 +1147,8 @@ pub(crate) fn build_dispatch_success_closure(
|
||||
app.last_effective_provider = None;
|
||||
app.last_effective_provider_identity = None;
|
||||
app.last_auto_route_receipt = None;
|
||||
app.set_active_route_limits(outcome.route_limits);
|
||||
app.update_model_compaction_budget();
|
||||
}
|
||||
app.pending_auto_route_receipt = outcome
|
||||
.auto_selection
|
||||
|
||||
Reference in new issue
Block a user