Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 7 additions & 1 deletion crates/rustyclaw-core/src/engines/joshua.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,13 @@ impl LocalEngine for JoshuaEngine {
let detected_endpoint = port
.map(|port| format!("http://127.0.0.1:{}", port))
.unwrap_or(endpoint);
let loaded = detected.len() as u32;
// Count only servers on this engine's own port — servers on
// other ports belong to other engines and must not inflate
// this engine's loaded-model count.
let loaded = detected
.iter()
.filter(|(_, port)| *port == Some(configured_port))
.count() as u32;
EngineRunStatus::Running {
endpoint: detected_endpoint,
loaded_models: loaded,
Expand Down
8 changes: 7 additions & 1 deletion crates/rustyclaw-core/src/engines/llamacpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,13 @@ impl LocalEngine for LlamaCppEngine {
let detected_endpoint = port
.map(|port| format!("http://127.0.0.1:{}", port))
.unwrap_or(endpoint);
let loaded = detected.len() as u32;
// Count only servers on this engine's own port — servers on
// other ports belong to other engines and must not inflate
// this engine's loaded-model count.
let loaded = detected
.iter()
.filter(|(_, port)| *port == Some(configured_port))
.count() as u32;
EngineRunStatus::Running {
endpoint: detected_endpoint,
loaded_models: loaded,
Expand Down
149 changes: 89 additions & 60 deletions crates/rustyclaw-tui/src/app/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ impl App {
}
Ok(UserInput::FetchModelCompletions { provider }) => {
let base_url = config.model.as_ref().and_then(|m| m.base_url.clone());
let engine_configs = config.engines.clone();
let api_key = rustyclaw_core::providers::secret_key_for_provider(&provider)
.and_then(|key_name| {
secrets_manager
Expand All @@ -502,28 +503,28 @@ impl App {
});
let gw_tx2 = gw_tx.clone();
tokio::spawn(async move {
match rustyclaw_core::providers::fetch_models(
let fetched = rustyclaw_core::engines::provider_models_with_local_fallback(
&provider,
api_key.as_deref(),
base_url.as_deref(),
&engine_configs,
)
.await
{
Ok(models) => {
.await;
let rustyclaw_core::engines::ProviderModels { models, error, .. } = fetched;
match error {
None => {
gw_tx2
.send(GwEvent::ModelCompletionsLoaded { provider, models })
.ignore();
}
Err(e) => {
Some(e) => {
gw_tx2
.send(GwEvent::Warning {
summary: format!(
"Failed to load model completions: {:#}",
e
),
details: Some(
rustyclaw_core::error_details::render_extended(&e),
),
summary: format!("Failed to load model completions: {e}"),
// The error string (built with the
// cause chain upstream) goes into the
// expandable detail view.
details: Some(e),
})
.ignore();
}
Expand Down Expand Up @@ -822,15 +823,22 @@ impl App {
);
let gw_tx2 = gw_tx.clone();
let base = config.model.as_ref().and_then(|m| m.base_url.clone());
let engine_configs = config.engines.clone();
tokio::spawn(async move {
match rustyclaw_core::providers::fetch_models(
let fetched = rustyclaw_core::engines::provider_models_with_local_fallback(
&pid,
None,
base.as_deref(),
&engine_configs,
)
.await
{
Ok(models) => {
.await;
let rustyclaw_core::engines::ProviderModels {
models,
error,
..
} = fetched;
match error {
None => {
gw_tx2
.send(GwEvent::ShowModelSelector {
provider: pid,
Expand All @@ -839,15 +847,13 @@ impl App {
})
.ignore();
}
Err(e) => {
gw_tx2.send(GwEvent::Error {
summary: format!("Failed to fetch models: {:#}", e),
details: Some(
rustyclaw_core::error_details::render_extended(
&e,
),
),
}).ignore();
Some(e) => {
gw_tx2
.send(GwEvent::Error {
summary: format!("Failed to fetch models: {e}"),
details: None,
})
.ignore();
}
}
});
Expand Down Expand Up @@ -906,15 +912,22 @@ impl App {
let gw_tx2 = gw_tx.clone();
let base =
config.model.as_ref().and_then(|m| m.base_url.clone());
let engine_configs = config.engines.clone();
tokio::spawn(async move {
match rustyclaw_core::providers::fetch_models(
&pid,
key.as_deref(),
base.as_deref(),
)
.await
{
Ok(models) => {
let fetched = rustyclaw_core::engines::provider_models_with_local_fallback(
&pid,
key.as_deref(),
base.as_deref(),
&engine_configs,
)
.await;
let rustyclaw_core::engines::ProviderModels {
models,
error,
..
} = fetched;
match error {
None => {
gw_tx2
.send(GwEvent::ShowModelSelector {
provider: pid,
Expand All @@ -923,11 +936,15 @@ impl App {
})
.ignore();
}
Err(e) => {
gw_tx2.send(GwEvent::Error {
summary: format!("Failed to fetch models: {:#}", e),
details: Some(rustyclaw_core::error_details::render_extended(&e)),
}).ignore();
Some(e) => {
gw_tx2
.send(GwEvent::Error {
summary: format!(
"Failed to fetch models: {e}"
),
details: None,
})
.ignore();
}
}
});
Expand Down Expand Up @@ -995,15 +1012,22 @@ impl App {
let gw_tx2 = gw_tx.clone();
let base =
config.model.as_ref().and_then(|m| m.base_url.clone());
let engine_configs = config.engines.clone();
tokio::spawn(async move {
match rustyclaw_core::providers::fetch_models(
&pid,
token.as_deref(),
base.as_deref(),
)
.await
{
Ok(models) => {
let fetched = rustyclaw_core::engines::provider_models_with_local_fallback(
&pid,
token.as_deref(),
base.as_deref(),
&engine_configs,
)
.await;
let rustyclaw_core::engines::ProviderModels {
models,
error,
..
} = fetched;
match error {
None => {
gw_tx2
.send(GwEvent::ShowModelSelector {
provider: pid,
Expand All @@ -1012,11 +1036,15 @@ impl App {
})
.ignore();
}
Err(e) => {
gw_tx2.send(GwEvent::Error {
summary: format!("Failed to fetch models: {:#}", e),
details: Some(rustyclaw_core::error_details::render_extended(&e)),
}).ignore();
Some(e) => {
gw_tx2
.send(GwEvent::Error {
summary: format!(
"Failed to fetch models: {e}"
),
details: None,
})
.ignore();
}
}
});
Expand Down Expand Up @@ -1184,15 +1212,18 @@ impl App {
let gw_tx2 = gw_tx.clone();
let api_key = Some(key);
let base = config.model.as_ref().and_then(|m| m.base_url.clone());
let engine_configs = config.engines.clone();
tokio::spawn(async move {
match rustyclaw_core::providers::fetch_models(
let fetched = rustyclaw_core::engines::provider_models_with_local_fallback(
&pid,
api_key.as_deref(),
base.as_deref(),
&engine_configs,
)
.await
{
Ok(models) => {
.await;
let rustyclaw_core::engines::ProviderModels { models, error, .. } = fetched;
match error {
None => {
gw_tx2
.send(GwEvent::ShowModelSelector {
provider: pid,
Expand All @@ -1201,13 +1232,11 @@ impl App {
})
.ignore();
}
Err(e) => {
Some(e) => {
gw_tx2
.send(GwEvent::Error {
summary: format!("Failed to fetch models: {:#}", e),
details: Some(
rustyclaw_core::error_details::render_extended(&e),
),
summary: format!("Failed to fetch models: {e}"),
details: None,
})
.ignore();
}
Expand Down
7 changes: 5 additions & 2 deletions crates/rustyclaw-tui/src/app/command_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,13 @@ pub(super) async fn handle_command_action(
});

let gw_tx2 = gw_tx.clone();
let engine_configs = config.engines.clone();
tokio::spawn(async move {
match rustyclaw_core::providers::fetch_models_detailed(
match rustyclaw_core::engines::provider_models_detailed_with_local_fallback(
&provider_id,
api_key.as_deref(),
base_url.as_deref(),
&engine_configs,
)
.await
{
Expand All @@ -311,7 +313,8 @@ pub(super) async fn handle_command_action(
.ignore();
}
Err(e) => {
gw_tx2.send(GwEvent::error_from_err(&e)).ignore();
let err = anyhow_tracing::Error::from(e);
gw_tx2.send(GwEvent::error_from_err(&err)).ignore();
}
}
});
Expand Down
5 changes: 5 additions & 0 deletions crates/rustyclaw-tui/src/app/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ pub(crate) enum GwEvent {
EngineListResult {
engines: Vec<rustyclaw_view::LocalEngineData>,
},
/// Full per-engine configuration, keyed by engine id (arrives right
/// after `EngineListResult`; patches the engine entries' configs).
EngineConfigList {
configs: std::collections::HashMap<String, rustyclaw_core::engines::EngineConfig>,
},
/// Engine model list result received.
EngineModelListResult {
engine: String,
Expand Down
40 changes: 39 additions & 1 deletion crates/rustyclaw-tui/src/app/tui_component/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ pub(super) fn apply_gw_event(
mut show_engines_dialog,
mut engines_data,
mut engines_cursor,
mut engines_params_edit,
mut engines_params_cursor,
mut engines_params_drafts,
mut engines_action_result,
mut engines_configs_received,
mut show_cron_dialog,
mut cron_data,
mut show_memory_dialog,
Expand Down Expand Up @@ -1629,9 +1634,24 @@ pub(super) fn apply_gw_event(
}
// ── Engines ──────────────────────────────────────────────────────
GwEvent::ShowEngines => {
// A fresh open starts in normal mode with no stale drafts and
// no leftover Load/Unload result row.
if !show_engines_dialog.get() {
engines_params_edit.set(false);
engines_params_drafts.write().clear();
engines_action_result.set(None);
}
show_engines_dialog.set(true);
}
GwEvent::EngineListResult { engines } => {
// A fresh exchange: the EngineConfigList snapshot that patches
// the panel's configs follows this frame and has not arrived
// yet — until it does, saving parameters must stay disabled.
engines_configs_received.set(false);
// The configs came from the gateway; drafts seeded from an older
// snapshot are stale, so drop them (p re-seeds from the fresh
// config when the user next enters edit mode).
engines_params_drafts.write().clear();
let mut data = engines_data.read().clone().unwrap_or_default();
// Fill in host resources from the last HostInfo snapshot.
if let Some(host) = host_info.read().as_ref() {
Expand All @@ -1648,6 +1668,18 @@ pub(super) fn apply_gw_event(
data.selected_engine = data.engines.get(cursor).map(|e| e.id.clone());
engines_data.set(Some(data));
}
GwEvent::EngineConfigList { configs } => {
// The real config snapshot is here: the panel entries are no
// longer placeholders, so saving parameters is safe again.
engines_configs_received.set(true);
let mut data = engines_data.read().clone().unwrap_or_default();
for engine in &mut data.engines {
if let Some(cfg) = configs.get(&engine.id) {
engine.config = cfg.clone();
}
}
engines_data.set(Some(data));
}
GwEvent::EngineModelListResult { engine, models } => {
let mut data = engines_data.read().clone().unwrap_or_default();
data.selected_engine = Some(engine.clone());
Expand Down Expand Up @@ -1703,10 +1735,16 @@ pub(super) fn apply_gw_event(
}
GwEvent::EngineActionResult {
engine,
model,
ok,
message,
..
} => {
// Keep the outcome of model actions (Load/Unload) for the
// dialog's inline feedback; lifecycle actions surface as
// notices instead.
if model.is_some() {
engines_action_result.set(Some((engine.clone(), ok, message.clone())));
}
// Record the terminal outcome on the engine's install panel (so
// the dialog shows "install complete/failed"), and also surface a
// one-line notice in the chat. Only finish an install that's
Expand Down
Loading
Loading