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