@setoelkahfi / sigit / commits / c32c6e8

Improve model switch progress reporting with ToolCall updates

- Replace assistant messages with ToolCall and ToolCallUpdate notifications for model download and loading progress - Add detailed status, progress, and completion/failure updates using ToolCallStatus and ToolCallUpdateFields - Update TUI system message formatting for improved readability

paydii committed Apr 26, 2026 at 22:55 UTC c32c6e80f40bc3100b54781b35bdd9bc16672909
2 files changed +122 -38
src/chat.rs
+21 -4
index ea23b4c..7bb639f 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -906,10 +906,27 @@ mod tui { } Role::System => { for text_line in msg.text.split('\n') { - lines.push(Line::from(Span::styled( - text_line.to_string(), - Style::default().fg(Color::DarkGray), - ))); + let trimmed = text_line.trim(); + let (prefix, body) = if trimmed.is_empty() { + ("", "") + } else { + (" · ", trimmed) + }; + + lines.push(Line::from(vec![ + Span::styled( + prefix.to_string(), + Style::default() + .fg(Color::Rgb(90, 90, 98)) + .add_modifier(Modifier::DIM), + ), + Span::styled( + body.to_string(), + Style::default() + .fg(Color::Rgb(132, 132, 145)) + .add_modifier(Modifier::ITALIC | Modifier::DIM), + ), + ])); } } Role::User => {
src/main.rs
+101 -34
index 2597d3a..ac802fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,7 +60,8 @@ use agent_client_protocol::{ NewSessionResponse, PromptRequest, PromptResponse, ProtocolVersion, SessionCapabilities, SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption, SessionConfigValueId, SessionForkCapabilities, SessionId, SessionNotification, SessionUpdate, - SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, StopReason, + SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, StopReason, ToolCall, + ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, }; use futures::future::LocalBoxFuture; use onde::inference::{ChatEngine, GgufModelConfig, ToolDefinition, ToolResult}; @@ -314,6 +315,13 @@ impl SiGitAgent { } } + async fn send_tool_call_update(&self, session_id: SessionId, update: SessionUpdate) { + let notification = SessionNotification::new(session_id, update); + if self.notification_tx.send(notification).await.is_err() { + log::warn!("notification channel closed"); + } + } + async fn switch_model_by_id( &self, model_id: &str, @@ -1199,9 +1207,24 @@ impl Agent for SiGitAgent { String::new() }; - self.send_assistant_message( + let tool_call_id = format!("model-switch-{}", uuid::Uuid::new_v4()); + + self.send_tool_call_update( args.session_id.clone(), - format!("⏬ Downloading {display_name}{size_hint}… this may take a few minutes."), + SessionUpdate::ToolCall( + ToolCall::new( + tool_call_id.clone(), + format!("Downloading {display_name}{size_hint}"), + ) + .kind(ToolKind::Execute) + .status(ToolCallStatus::InProgress) + .content(vec![ + format!( + "Preparing download for {display_name}. This may take a few minutes." + ) + .into(), + ]), + ), ) .await; @@ -1210,6 +1233,7 @@ impl Agent for SiGitAgent { let poller_session = args.session_id.clone(); let poller_model_id = model_id_owned.clone(); let poller_stop = Arc::clone(&stop_flag); + let poller_tool_call_id = tool_call_id.clone(); tokio::task::spawn_local(async move { let cache_path = onde::hf_cache::model_cache_path(&poller_model_id); @@ -1234,22 +1258,25 @@ impl Agent for SiGitAgent { ((downloaded as f64 / expected_bytes as f64) * 100.0).min(99.0) as u8; let bar = progress_bar(pct, 20); format!( - "\n⏬ {display_name} — {bar} {pct}% ({} / {})", + "{display_name} — {bar} {pct}% ({} / {})", format_size_human(downloaded), format_size_human(expected_bytes), ) } else { format!( - "\n⏬ {display_name} — {} downloaded…", + "{display_name} — {} downloaded…", format_size_human(downloaded) ) }; let notification = SessionNotification::new( poller_session.clone(), - SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from( - msg, - ))), + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( + poller_tool_call_id.clone(), + ToolCallUpdateFields::new() + .status(ToolCallStatus::InProgress) + .content(vec![msg.into()]), + )), ); if poller_tx.send(notification).await.is_err() { break; @@ -1260,6 +1287,8 @@ impl Agent for SiGitAgent { // For already-cached models, send a "loading" message and a spinner // so the user sees activity while mistralrs loads the weights (~10-30 s). + let tool_call_id = format!("model-switch-{}", uuid::Uuid::new_v4()); + if !needs_download { let cached_display_name = models::build_model_picker_items() .into_iter() @@ -1267,9 +1296,17 @@ impl Agent for SiGitAgent { .map(|item| item.display_name.clone()) .unwrap_or_else(|| model_id.to_string()); - self.send_assistant_message( + self.send_tool_call_update( args.session_id.clone(), - format!("⏳ Loading {cached_display_name}…"), + SessionUpdate::ToolCall( + ToolCall::new( + tool_call_id.clone(), + format!("Loading {cached_display_name}"), + ) + .kind(ToolKind::Execute) + .status(ToolCallStatus::InProgress) + .content(vec![format!("Loading {cached_display_name}…").into()]), + ), ) .await; @@ -1279,6 +1316,7 @@ impl Agent for SiGitAgent { let spinner_session = args.session_id.clone(); let spinner_name = cached_display_name.clone(); let spinner_stop = Arc::clone(&stop_flag); + let spinner_tool_call_id = tool_call_id.clone(); let load_start = std::time::Instant::now(); tokio::task::spawn_local(async move { @@ -1303,12 +1341,15 @@ impl Agent for SiGitAgent { let frame = SPINNER[tick % SPINNER.len()]; tick += 1; - let msg = format!("\n{frame} Loading {spinner_name}… ({elapsed_str})"); + let msg = format!("{frame} Loading {spinner_name}… ({elapsed_str})"); let notification = SessionNotification::new( spinner_session.clone(), - SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from( - msg, - ))), + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( + spinner_tool_call_id.clone(), + ToolCallUpdateFields::new() + .status(ToolCallStatus::InProgress) + .content(vec![msg.into()]), + )), ); if spinner_tx.send(notification).await.is_err() { break; @@ -1322,29 +1363,55 @@ impl Agent for SiGitAgent { // Stop the progress / spinner poller regardless of success/failure. stop_flag.store(true, Ordering::Relaxed); - let new_config = switch_result?; + match switch_result { + Ok(new_config) => { + let completion_title = if needs_download { + format!("{} downloaded and loaded", new_config.display_name) + } else { + format!("Switched to {}", new_config.display_name) + }; + let completion_body = if needs_download { + format!("✓ {} downloaded and loaded.", new_config.display_name) + } else { + format!("✓ Switched to {}.", new_config.display_name) + }; + + self.send_tool_call_update( + args.session_id.clone(), + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( + tool_call_id, + ToolCallUpdateFields::new() + .title(completion_title) + .status(ToolCallStatus::Completed) + .content(vec![completion_body.into()]), + )), + ) + .await; - if needs_download { - self.send_assistant_message( - args.session_id.clone(), - format!("\n✓ {} downloaded and loaded.", new_config.display_name), - ) - .await; - } else { - self.send_assistant_message( - args.session_id.clone(), - format!("\n✓ Switched to {}.", new_config.display_name), - ) - .await; - } + let config_options = { + let guard = self.current_model.lock().unwrap(); + build_model_config_options(&guard) + }; - let config_options = { - let guard = self.current_model.lock().unwrap(); - build_model_config_options(&guard) - }; + log::info!("model switch complete"); + Ok(SetSessionConfigOptionResponse::new(config_options)) + } + Err(err) => { + self.send_tool_call_update( + args.session_id.clone(), + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( + tool_call_id, + ToolCallUpdateFields::new() + .title("Model switch failed".to_string()) + .status(ToolCallStatus::Failed) + .content(vec![format!("error loading model: {}", err.message).into()]), + )), + ) + .await; - log::info!("model switch complete"); - Ok(SetSessionConfigOptionResponse::new(config_options)) + Err(err) + } + } } }