@hej / 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
@@ -906,10 +906,27 @@ mod tui {
906 }
907 Role::System => {
908 for text_line in msg.text.split('\n') {
909 - lines.push(Line::from(Span::styled(
910 - text_line.to_string(),
911 - Style::default().fg(Color::DarkGray),
912 - )));
909 + let trimmed = text_line.trim();
910 + let (prefix, body) = if trimmed.is_empty() {
911 + ("", "")
912 + } else {
913 + (" · ", trimmed)
914 + };
915 +
916 + lines.push(Line::from(vec![
917 + Span::styled(
918 + prefix.to_string(),
919 + Style::default()
920 + .fg(Color::Rgb(90, 90, 98))
921 + .add_modifier(Modifier::DIM),
922 + ),
923 + Span::styled(
924 + body.to_string(),
925 + Style::default()
926 + .fg(Color::Rgb(132, 132, 145))
927 + .add_modifier(Modifier::ITALIC | Modifier::DIM),
928 + ),
929 + ]));
930 }
931 }
932 Role::User => {
src/main.rs
+101 -34
@@ -60,7 +60,8 @@ use agent_client_protocol::{
60 NewSessionResponse, PromptRequest, PromptResponse, ProtocolVersion, SessionCapabilities,
61 SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption,
62 SessionConfigValueId, SessionForkCapabilities, SessionId, SessionNotification, SessionUpdate,
63 - SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, StopReason,
63 + SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, StopReason, ToolCall,
64 + ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind,
65 };
66 use futures::future::LocalBoxFuture;
67 use onde::inference::{ChatEngine, GgufModelConfig, ToolDefinition, ToolResult};
@@ -314,6 +315,13 @@ impl SiGitAgent {
315 }
316 }
317
318 + async fn send_tool_call_update(&self, session_id: SessionId, update: SessionUpdate) {
319 + let notification = SessionNotification::new(session_id, update);
320 + if self.notification_tx.send(notification).await.is_err() {
321 + log::warn!("notification channel closed");
322 + }
323 + }
324 +
325 async fn switch_model_by_id(
326 &self,
327 model_id: &str,
@@ -1199,9 +1207,24 @@ impl Agent for SiGitAgent {
1207 String::new()
1208 };
1209
1202 - self.send_assistant_message(
1210 + let tool_call_id = format!("model-switch-{}", uuid::Uuid::new_v4());
1211 +
1212 + self.send_tool_call_update(
1213 args.session_id.clone(),
1204 - format!("⏬ Downloading {display_name}{size_hint}… this may take a few minutes."),
1214 + SessionUpdate::ToolCall(
1215 + ToolCall::new(
1216 + tool_call_id.clone(),
1217 + format!("Downloading {display_name}{size_hint}"),
1218 + )
1219 + .kind(ToolKind::Execute)
1220 + .status(ToolCallStatus::InProgress)
1221 + .content(vec![
1222 + format!(
1223 + "Preparing download for {display_name}. This may take a few minutes."
1224 + )
1225 + .into(),
1226 + ]),
1227 + ),
1228 )
1229 .await;
1230
@@ -1210,6 +1233,7 @@ impl Agent for SiGitAgent {
1233 let poller_session = args.session_id.clone();
1234 let poller_model_id = model_id_owned.clone();
1235 let poller_stop = Arc::clone(&stop_flag);
1236 + let poller_tool_call_id = tool_call_id.clone();
1237
1238 tokio::task::spawn_local(async move {
1239 let cache_path = onde::hf_cache::model_cache_path(&poller_model_id);
@@ -1234,22 +1258,25 @@ impl Agent for SiGitAgent {
1258 ((downloaded as f64 / expected_bytes as f64) * 100.0).min(99.0) as u8;
1259 let bar = progress_bar(pct, 20);
1260 format!(
1237 - "\n⏬ {display_name} — {bar} {pct}% ({} / {})",
1261 + "{display_name} — {bar} {pct}% ({} / {})",
1262 format_size_human(downloaded),
1263 format_size_human(expected_bytes),
1264 )
1265 } else {
1266 format!(
1243 - "\n⏬ {display_name} — {} downloaded…",
1267 + "{display_name} — {} downloaded…",
1268 format_size_human(downloaded)
1269 )
1270 };
1271
1272 let notification = SessionNotification::new(
1273 poller_session.clone(),
1250 - SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from(
1251 - msg,
1252 - ))),
1274 + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
1275 + poller_tool_call_id.clone(),
1276 + ToolCallUpdateFields::new()
1277 + .status(ToolCallStatus::InProgress)
1278 + .content(vec![msg.into()]),
1279 + )),
1280 );
1281 if poller_tx.send(notification).await.is_err() {
1282 break;
@@ -1260,6 +1287,8 @@ impl Agent for SiGitAgent {
1287
1288 // For already-cached models, send a "loading" message and a spinner
1289 // so the user sees activity while mistralrs loads the weights (~10-30 s).
1290 + let tool_call_id = format!("model-switch-{}", uuid::Uuid::new_v4());
1291 +
1292 if !needs_download {
1293 let cached_display_name = models::build_model_picker_items()
1294 .into_iter()
@@ -1267,9 +1296,17 @@ impl Agent for SiGitAgent {
1296 .map(|item| item.display_name.clone())
1297 .unwrap_or_else(|| model_id.to_string());
1298
1270 - self.send_assistant_message(
1299 + self.send_tool_call_update(
1300 args.session_id.clone(),
1272 - format!("⏳ Loading {cached_display_name}…"),
1301 + SessionUpdate::ToolCall(
1302 + ToolCall::new(
1303 + tool_call_id.clone(),
1304 + format!("Loading {cached_display_name}"),
1305 + )
1306 + .kind(ToolKind::Execute)
1307 + .status(ToolCallStatus::InProgress)
1308 + .content(vec![format!("Loading {cached_display_name}…").into()]),
1309 + ),
1310 )
1311 .await;
1312
@@ -1279,6 +1316,7 @@ impl Agent for SiGitAgent {
1316 let spinner_session = args.session_id.clone();
1317 let spinner_name = cached_display_name.clone();
1318 let spinner_stop = Arc::clone(&stop_flag);
1319 + let spinner_tool_call_id = tool_call_id.clone();
1320 let load_start = std::time::Instant::now();
1321
1322 tokio::task::spawn_local(async move {
@@ -1303,12 +1341,15 @@ impl Agent for SiGitAgent {
1341 let frame = SPINNER[tick % SPINNER.len()];
1342 tick += 1;
1343
1306 - let msg = format!("\n{frame} Loading {spinner_name}… ({elapsed_str})");
1344 + let msg = format!("{frame} Loading {spinner_name}… ({elapsed_str})");
1345 let notification = SessionNotification::new(
1346 spinner_session.clone(),
1309 - SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::from(
1310 - msg,
1311 - ))),
1347 + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
1348 + spinner_tool_call_id.clone(),
1349 + ToolCallUpdateFields::new()
1350 + .status(ToolCallStatus::InProgress)
1351 + .content(vec![msg.into()]),
1352 + )),
1353 );
1354 if spinner_tx.send(notification).await.is_err() {
1355 break;
@@ -1322,29 +1363,55 @@ impl Agent for SiGitAgent {
1363 // Stop the progress / spinner poller regardless of success/failure.
1364 stop_flag.store(true, Ordering::Relaxed);
1365
1325 - let new_config = switch_result?;
1366 + match switch_result {
1367 + Ok(new_config) => {
1368 + let completion_title = if needs_download {
1369 + format!("{} downloaded and loaded", new_config.display_name)
1370 + } else {
1371 + format!("Switched to {}", new_config.display_name)
1372 + };
1373 + let completion_body = if needs_download {
1374 + format!("✓ {} downloaded and loaded.", new_config.display_name)
1375 + } else {
1376 + format!("✓ Switched to {}.", new_config.display_name)
1377 + };
1378 +
1379 + self.send_tool_call_update(
1380 + args.session_id.clone(),
1381 + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
1382 + tool_call_id,
1383 + ToolCallUpdateFields::new()
1384 + .title(completion_title)
1385 + .status(ToolCallStatus::Completed)
1386 + .content(vec![completion_body.into()]),
1387 + )),
1388 + )
1389 + .await;
1390
1327 - if needs_download {
1328 - self.send_assistant_message(
1329 - args.session_id.clone(),
1330 - format!("\n✓ {} downloaded and loaded.", new_config.display_name),
1331 - )
1332 - .await;
1333 - } else {
1334 - self.send_assistant_message(
1335 - args.session_id.clone(),
1336 - format!("\n✓ Switched to {}.", new_config.display_name),
1337 - )
1338 - .await;
1339 - }
1391 + let config_options = {
1392 + let guard = self.current_model.lock().unwrap();
1393 + build_model_config_options(&guard)
1394 + };
1395
1341 - let config_options = {
1342 - let guard = self.current_model.lock().unwrap();
1343 - build_model_config_options(&guard)
1344 - };
1396 + log::info!("model switch complete");
1397 + Ok(SetSessionConfigOptionResponse::new(config_options))
1398 + }
1399 + Err(err) => {
1400 + self.send_tool_call_update(
1401 + args.session_id.clone(),
1402 + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
1403 + tool_call_id,
1404 + ToolCallUpdateFields::new()
1405 + .title("Model switch failed".to_string())
1406 + .status(ToolCallStatus::Failed)
1407 + .content(vec![format!("error loading model: {}", err.message).into()]),
1408 + )),
1409 + )
1410 + .await;
1411
1346 - log::info!("model switch complete");
1347 - Ok(SetSessionConfigOptionResponse::new(config_options))
1412 + Err(err)
1413 + }
1414 + }
1415 }
1416 }
1417