4runr-os 2.10.73 → 2.10.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ use crate::storage::Cache;
5
5
  use crate::ui::agent_builder::AgentBuilderState;
6
6
  use crate::ui::run_manager::RunManagerState;
7
7
  use crate::ui::sentinel_config::SentinelConfigState;
8
+ use crate::ui::shield_dashboard::ShieldDashboardState;
8
9
  use crate::ui::settings::SettingsState;
9
10
  use crate::websocket::WebSocketClient;
10
11
  use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
@@ -386,6 +387,8 @@ pub struct AppState {
386
387
  pub posture_status: String,
387
388
  pub shield_mode: String, // "off", "monitor", "enforce"
388
389
  pub shield_detectors: Vec<String>, // ["pii", "injection", "hallucination"]
390
+ pub shield_blocks_total: u64,
391
+ pub shield_masks_total: u64,
389
392
  pub sentinel_state: String, // "idle", "watching", "triggered"
390
393
  pub sentinel_active_runs: usize,
391
394
 
@@ -429,6 +432,7 @@ pub struct AppState {
429
432
  pub agent_builder: AgentBuilderState,
430
433
  pub run_manager: RunManagerState,
431
434
  pub sentinel_config: SentinelConfigState,
435
+ pub shield_dashboard: ShieldDashboardState,
432
436
  pub settings: SettingsState,
433
437
  pub agent_list: AgentListState,
434
438
  pub connection_portal: ConnectionPortalState,
@@ -461,6 +465,7 @@ pub struct AppState {
461
465
 
462
466
  pub pending_sentinel_load_id: Option<String>,
463
467
  pub pending_sentinel_apply_id: Option<String>,
468
+ pub pending_shield_load_id: Option<String>,
464
469
 
465
470
  // Deletion confirmation (Step 5.5)
466
471
  pub agent_delete_requested: bool,
@@ -489,6 +494,8 @@ impl Default for AppState {
489
494
  posture_status: "Demo Mode".to_string(),
490
495
  shield_mode: "enforce".to_string(),
491
496
  shield_detectors: vec!["pii".into(), "injection".into(), "hallucination".into()],
497
+ shield_blocks_total: 0,
498
+ shield_masks_total: 0,
492
499
  sentinel_state: "idle".to_string(),
493
500
  sentinel_active_runs: 0,
494
501
  total_runs: 0,
@@ -526,6 +533,7 @@ impl Default for AppState {
526
533
  agent_builder: AgentBuilderState::default(),
527
534
  run_manager: RunManagerState::default(),
528
535
  sentinel_config: SentinelConfigState::default(),
536
+ shield_dashboard: ShieldDashboardState::default(),
529
537
  settings: SettingsState::default(),
530
538
  agent_list: AgentListState::default(),
531
539
  connection_portal: ConnectionPortalState::default(),
@@ -550,6 +558,7 @@ impl Default for AppState {
550
558
  pending_run_quick_id: None,
551
559
  pending_sentinel_load_id: None,
552
560
  pending_sentinel_apply_id: None,
561
+ pending_shield_load_id: None,
553
562
  agent_delete_requested: false,
554
563
  operation_mode: OperationMode::Local,
555
564
  cache: Cache::new().ok(),
@@ -828,6 +837,40 @@ impl App {
828
837
  }
829
838
  }
830
839
 
840
+ pub fn open_shield_dashboard(&mut self, ws: Option<&WebSocketClient>) {
841
+ self.state.pending_shield_load_id = None;
842
+ self.push_overlay(Screen::ShieldDashboard);
843
+ self.state
844
+ .logs
845
+ .push_back("[NAV] Opening Shield (production safety layer)...".into());
846
+ if self.state.operation_mode == OperationMode::Connected {
847
+ if let Some(ws) = ws {
848
+ self.begin_shield_load_request(ws);
849
+ }
850
+ } else {
851
+ self.state.shield_dashboard.error = Some(
852
+ "Connect to Gateway first (connect portal).".to_string(),
853
+ );
854
+ }
855
+ self.request_immediate_render("open_shield_dashboard");
856
+ }
857
+
858
+ pub fn begin_shield_load_request(&mut self, ws: &WebSocketClient) {
859
+ if self.state.operation_mode != OperationMode::Connected {
860
+ return;
861
+ }
862
+ if self.state.pending_shield_load_id.is_some() {
863
+ return;
864
+ }
865
+ self.state.shield_dashboard.loading = true;
866
+ self.state.shield_dashboard.error = None;
867
+ if let Ok(id) = ws.send_command("shield.load", None) {
868
+ self.state.pending_shield_load_id = Some(id);
869
+ } else {
870
+ self.state.shield_dashboard.loading = false;
871
+ }
872
+ }
873
+
831
874
  pub fn open_sentinel_config(&mut self, ws: Option<&WebSocketClient>) {
832
875
  self.state.pending_sentinel_load_id = None;
833
876
  self.state.pending_sentinel_apply_id = None;
@@ -1316,6 +1359,11 @@ impl App {
1316
1359
  return self.handle_sentinel_config_input(key, ws_client);
1317
1360
  }
1318
1361
 
1362
+ // === SHIELD DASHBOARD INPUT HANDLING ===
1363
+ if self.state.navigation.current_screen() == &Screen::ShieldDashboard {
1364
+ return self.handle_shield_dashboard_input(key, ws_client);
1365
+ }
1366
+
1319
1367
  // === SETTINGS INPUT HANDLING ===
1320
1368
  if self.state.navigation.current_screen() == &Screen::Settings {
1321
1369
  return self.handle_settings_input(key);
@@ -1489,6 +1537,10 @@ impl App {
1489
1537
  " sentinel - Configure Sentinel policies (templates)"
1490
1538
  .to_string(),
1491
1539
  );
1540
+ self.state.logs.push_back(
1541
+ " shield - Shield safety dashboard (live metrics + enforcement)"
1542
+ .to_string(),
1543
+ );
1492
1544
  self.state.logs.push_back(
1493
1545
  " build - Open Agent Builder (6-step wizard)"
1494
1546
  .to_string(),
@@ -1570,6 +1622,9 @@ impl App {
1570
1622
  "sentinel" | "sentinel config" | "sentinel policies" => {
1571
1623
  self.open_sentinel_config(ws_client);
1572
1624
  }
1625
+ "shield" | "shield status" | "shield dashboard" => {
1626
+ self.open_shield_dashboard(ws_client);
1627
+ }
1573
1628
  "config" | "settings" => {
1574
1629
  self.push_overlay(Screen::Settings);
1575
1630
  self.state
@@ -1854,6 +1909,10 @@ impl App {
1854
1909
  use crate::ui::sentinel_config;
1855
1910
  sentinel_config::render(f, &self.state);
1856
1911
  }
1912
+ Screen::ShieldDashboard => {
1913
+ use crate::ui::shield_dashboard;
1914
+ shield_dashboard::render(f, &self.state);
1915
+ }
1857
1916
  Screen::Settings => {
1858
1917
  use crate::ui::settings;
1859
1918
  settings::render(f, &self.state);
@@ -2588,6 +2647,50 @@ impl App {
2588
2647
  Ok(false)
2589
2648
  }
2590
2649
 
2650
+ fn handle_shield_dashboard_input(
2651
+ &mut self,
2652
+ key: KeyEvent,
2653
+ ws_client: Option<&WebSocketClient>,
2654
+ ) -> anyhow::Result<bool> {
2655
+ use crossterm::event::KeyModifiers;
2656
+
2657
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2658
+ match key.code {
2659
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2660
+ _ => return Ok(false),
2661
+ }
2662
+ }
2663
+
2664
+ match key.code {
2665
+ KeyCode::Char('r') | KeyCode::Char('R') => {
2666
+ if self.state.operation_mode == OperationMode::Connected {
2667
+ if let Some(ws) = ws_client {
2668
+ self.begin_shield_load_request(ws);
2669
+ }
2670
+ }
2671
+ self.request_render("shield_refresh");
2672
+ }
2673
+ KeyCode::Char('a') | KeyCode::Char('A') => {
2674
+ self.state.shield_dashboard.auto_refresh_enabled =
2675
+ !self.state.shield_dashboard.auto_refresh_enabled;
2676
+ let status = if self.state.shield_dashboard.auto_refresh_enabled {
2677
+ "ON"
2678
+ } else {
2679
+ "OFF"
2680
+ };
2681
+ self.add_log(format!("[SHIELD] Live refresh {}", status));
2682
+ self.request_render("shield_live_toggle");
2683
+ }
2684
+ KeyCode::Esc => {
2685
+ self.pop_overlay();
2686
+ self.request_render("shield_close");
2687
+ }
2688
+ _ => {}
2689
+ }
2690
+
2691
+ Ok(false)
2692
+ }
2693
+
2591
2694
  // ============================================================
2592
2695
  // SETTINGS INPUT HANDLING (Step 4.8)
2593
2696
  // ============================================================
@@ -189,6 +189,27 @@ fn main() -> Result<()> {
189
189
  }
190
190
  }
191
191
 
192
+ // Shield dashboard: live poll shield.load every ~5s when enabled
193
+ if matches!(current_screen, Screen::ShieldDashboard)
194
+ && app.state.operation_mode == OperationMode::Connected
195
+ {
196
+ let shield_poll = {
197
+ let sd = &app.state.shield_dashboard;
198
+ if !sd.auto_refresh_enabled || sd.loading {
199
+ false
200
+ } else {
201
+ sd.last_refresh
202
+ .map(|lr| lr.elapsed() >= sd.auto_refresh_interval)
203
+ .unwrap_or(true)
204
+ }
205
+ };
206
+ if shield_poll {
207
+ if let Some(ws) = &ws_client {
208
+ app.begin_shield_load_request(ws);
209
+ }
210
+ }
211
+ }
212
+
192
213
  // Run Manager: live poll run.list every ~3s when enabled (no manual R)
193
214
  if matches!(current_screen, Screen::RunManager)
194
215
  && app.state.operation_mode == OperationMode::Connected
@@ -279,6 +300,58 @@ fn main() -> Result<()> {
279
300
  app.state.posture_status = posture_str.to_string();
280
301
  }
281
302
  }
303
+ if let Some(sh) =
304
+ obj.get("shield").and_then(|v| v.as_object())
305
+ {
306
+ if let Some(m) =
307
+ sh.get("mode").and_then(|v| v.as_str())
308
+ {
309
+ app.state.shield_mode = m.to_string();
310
+ }
311
+ if let Some(dets) =
312
+ sh.get("detectors").and_then(|v| v.as_array())
313
+ {
314
+ app.state.shield_detectors = dets
315
+ .iter()
316
+ .filter_map(|v| {
317
+ v.as_str().map(|s| s.to_string())
318
+ })
319
+ .collect();
320
+ }
321
+ app.state.shield_blocks_total = sh
322
+ .get("blocksTotal")
323
+ .and_then(|v| v.as_u64())
324
+ .unwrap_or(0);
325
+ app.state.shield_masks_total = sh
326
+ .get("masksTotal")
327
+ .and_then(|v| v.as_u64())
328
+ .unwrap_or(0);
329
+ }
330
+ if let Some(sent) =
331
+ obj.get("sentinel").and_then(|v| v.as_object())
332
+ {
333
+ app.state.sentinel_active_runs = sent
334
+ .get("watchedRuns")
335
+ .and_then(|v| v.as_u64())
336
+ .unwrap_or(0) as usize;
337
+ let enabled = sent
338
+ .get("enabled")
339
+ .and_then(|v| v.as_bool())
340
+ .unwrap_or(false);
341
+ let healthy = sent
342
+ .get("healthy")
343
+ .and_then(|v| v.as_bool())
344
+ .unwrap_or(false);
345
+ app.state.sentinel_state = if !enabled {
346
+ "off".to_string()
347
+ } else if app.state.sentinel_active_runs > 0 {
348
+ "watching".to_string()
349
+ } else if healthy {
350
+ "idle".to_string()
351
+ } else {
352
+ "degraded".to_string()
353
+ };
354
+ }
282
355
  app.add_log(format!(
283
356
  "✓ [{}] Mode: {}, Posture: {}",
284
357
  short_id,
@@ -288,6 +361,61 @@ fn main() -> Result<()> {
288
361
  .unwrap_or("?")
289
362
  ));
290
363
  }
364
+ else if obj.get("shieldDemo").and_then(|v| v.as_bool())
365
+ == Some(true)
366
+ {
367
+ let msg = obj
368
+ .get("message")
369
+ .and_then(|v| v.as_str())
370
+ .unwrap_or("Shield demo run started");
371
+ app.add_log(format!("✓ [{}] {}", short_id, msg));
372
+ if let Some(rid) =
373
+ obj.get("runId").and_then(|v| v.as_str())
374
+ {
375
+ app.add_log(format!(
376
+ "[SHIELD] Demo run id: {} — open Run Manager",
377
+ rid
378
+ ));
379
+ }
380
+ }
381
+ else if obj.get("shieldProbe").and_then(|v| v.as_bool())
382
+ == Some(true)
383
+ {
384
+ let result = obj.get("result");
385
+ let action = result
386
+ .and_then(|r| r.get("decision"))
387
+ .and_then(|d| d.get("action"))
388
+ .and_then(|a| a.as_str())
389
+ .unwrap_or("?");
390
+ app.add_log(format!(
391
+ "✓ [{}] Shield probe: action={}",
392
+ short_id, action
393
+ ));
394
+ }
395
+ else if obj.get("shieldStatus").and_then(|v| v.as_bool())
396
+ == Some(true)
397
+ {
398
+ let metrics = obj.get("metrics").and_then(|v| v.as_object());
399
+ let blocks = metrics
400
+ .and_then(|m| m.get("blocks"))
401
+ .and_then(|v| v.as_u64())
402
+ .unwrap_or(0);
403
+ let masks = metrics
404
+ .and_then(|m| m.get("masks"))
405
+ .and_then(|v| v.as_u64())
406
+ .unwrap_or(0);
407
+ app.state.shield_blocks_total = blocks;
408
+ app.state.shield_masks_total = masks;
409
+ let mode = obj
410
+ .get("health")
411
+ .and_then(|h| h.get("mode"))
412
+ .and_then(|v| v.as_str())
413
+ .unwrap_or("?");
414
+ app.add_log(format!(
415
+ "✓ [{}] Shield {} — {} block(s), {} mask(s)",
416
+ short_id, mode, blocks, masks
417
+ ));
418
+ }
291
419
  // Handle agent.list response
292
420
  else if let Some(agents_data) = obj.get("agents") {
293
421
  if let Some(agents_array) = agents_data.as_array() {
@@ -562,6 +690,66 @@ fn main() -> Result<()> {
562
690
  }
563
691
  app.request_render("run_quick_ok");
564
692
  }
693
+ // Shield dashboard: load health + config + metrics + history
694
+ else if Some(&resp.id)
695
+ == app.state.pending_shield_load_id.as_ref()
696
+ {
697
+ app.state.pending_shield_load_id = None;
698
+ if obj.get("shieldLoad").and_then(|v| v.as_bool()) == Some(true)
699
+ {
700
+ use crate::ui::shield_dashboard::parse_shield_load;
701
+ let health_val = obj
702
+ .get("health")
703
+ .cloned()
704
+ .unwrap_or(serde_json::Value::Null);
705
+ let config_val = obj
706
+ .get("config")
707
+ .cloned()
708
+ .unwrap_or(serde_json::Value::Null);
709
+ let metrics_val = obj
710
+ .get("metrics")
711
+ .cloned()
712
+ .unwrap_or(serde_json::Value::Null);
713
+ let gateway_health_val = obj
714
+ .get("gatewayHealth")
715
+ .cloned()
716
+ .unwrap_or(serde_json::Value::Null);
717
+ let runs_val = obj
718
+ .get("recentRuns")
719
+ .cloned()
720
+ .unwrap_or(serde_json::Value::Null);
721
+ let warnings_vec: Vec<String> = obj
722
+ .get("warnings")
723
+ .and_then(|v| v.as_array())
724
+ .map(|a| {
725
+ a.iter()
726
+ .filter_map(|w| {
727
+ w.as_str().map(|s| s.to_string())
728
+ })
729
+ .collect()
730
+ })
731
+ .unwrap_or_default();
732
+ let (h, c, m, mut w, recent) = parse_shield_load(
733
+ &health_val,
734
+ &config_val,
735
+ &metrics_val,
736
+ &gateway_health_val,
737
+ &runs_val,
738
+ );
739
+ w.extend(warnings_vec);
740
+ app.state.shield_dashboard.apply_load(
741
+ &h, &c, &m, w, recent,
742
+ );
743
+ app.state.shield_mode = h.mode.clone();
744
+ app.state.shield_blocks_total = m.blocks;
745
+ app.state.shield_masks_total = m.masks;
746
+ app.add_log(format!(
747
+ "✓ [{}] Shield loaded — {} block(s), {} mask(s)",
748
+ short_id, m.blocks, m.masks
749
+ ));
750
+ }
751
+ app.request_render("shield_loaded");
752
+ }
565
753
  // Sentinel config: load templates + current + health
566
754
  else if Some(&resp.id)
567
755
  == app.state.pending_sentinel_load_id.as_ref()
@@ -1435,6 +1623,14 @@ fn main() -> Result<()> {
1435
1623
  "✗ [{}] run.quick failed: {}",
1436
1624
  short_id, error_msg
1437
1625
  ));
1626
+ } else if Some(&resp.id) == app.state.pending_shield_load_id.as_ref() {
1627
+ app.state.pending_shield_load_id = None;
1628
+ app.state.shield_dashboard.loading = false;
1629
+ app.state.shield_dashboard.error = Some(error_msg.clone());
1630
+ app.add_log(format!(
1631
+ "✗ [{}] shield.load failed: {}",
1632
+ short_id, error_msg
1633
+ ));
1438
1634
  } else if Some(&resp.id) == app.state.pending_sentinel_load_id.as_ref()
1439
1635
  {
1440
1636
  app.state.pending_sentinel_load_id = None;
@@ -18,6 +18,7 @@ pub enum Screen {
18
18
  AgentBuilder,
19
19
  RunManager,
20
20
  SentinelConfig,
21
+ ShieldDashboard,
21
22
  Settings,
22
23
  AgentList,
23
24
  ConnectionPortal,
@@ -40,7 +41,7 @@ impl Screen {
40
41
  pub fn is_overlay(&self) -> bool {
41
42
  matches!(
42
43
  self,
43
- Screen::AgentBuilder | Screen::RunManager | Screen::SentinelConfig | Screen::Settings | Screen::AgentList
44
+ Screen::AgentBuilder | Screen::RunManager | Screen::SentinelConfig | Screen::ShieldDashboard | Screen::Settings | Screen::AgentList
44
45
  )
45
46
  }
46
47
 
@@ -70,6 +71,7 @@ impl Screen {
70
71
  Screen::AgentBuilder => "Agent Builder",
71
72
  Screen::RunManager => "Run Manager",
72
73
  Screen::SentinelConfig => "Sentinel Config",
74
+ Screen::ShieldDashboard => "Shield",
73
75
  Screen::Settings => "Settings",
74
76
  Screen::AgentList => "Agent List",
75
77
  Screen::ConnectionPortal => "Connection Portal",
@@ -394,6 +394,18 @@ fn render_left_column(f: &mut Frame, area: Rect, state: &AppState) {
394
394
  Style::default().fg(TEXT_DIM),
395
395
  ),
396
396
  ]));
397
+ if state.shield_blocks_total > 0 || state.shield_masks_total > 0 {
398
+ lines.push(Line::from(vec![
399
+ Span::raw(" "),
400
+ Span::styled(
401
+ format!(
402
+ "↳ {} block(s) · {} mask(s) (Gateway metrics)",
403
+ state.shield_blocks_total, state.shield_masks_total
404
+ ),
405
+ Style::default().fg(TEXT_DIM),
406
+ ),
407
+ ]));
408
+ }
397
409
 
398
410
  // Sentinel status
399
411
  let sentinel_color = match state.sentinel_state.as_str() {
@@ -8,5 +8,6 @@ pub mod portal_monitoring;
8
8
  pub mod run_manager;
9
9
  pub mod safe_viewport;
10
10
  pub mod sentinel_config;
11
+ pub mod shield_dashboard;
11
12
  pub mod settings;
12
13
  pub mod setup_portal;
@@ -67,6 +67,9 @@ pub struct RunInfo {
67
67
  pub total_cost: Option<f64>,
68
68
  pub model: Option<String>,
69
69
  pub error_message: Option<String>,
70
+ /// True when Gateway output indicates Shield blocked or masked on the run path.
71
+ pub shield_blocked: bool,
72
+ pub safety_reason: Option<String>,
70
73
  pub logs: Vec<String>,
71
74
  }
72
75
 
@@ -213,12 +216,20 @@ pub fn run_info_from_gateway_value(v: &Value) -> Option<RunInfo> {
213
216
  .collect()
214
217
  })
215
218
  .unwrap_or_default();
216
- let error_message = o
217
- .get("output")
218
- .and_then(|out| out.as_object())
219
+ let output_obj = o.get("output").and_then(|out| out.as_object());
220
+ let error_message = output_obj
219
221
  .and_then(|oo| oo.get("error"))
220
222
  .and_then(|e| e.as_str())
221
223
  .map(|s| s.to_string());
224
+ let safety_reason = output_obj
225
+ .and_then(|oo| oo.get("reason"))
226
+ .and_then(|r| r.as_str())
227
+ .map(|s| s.to_string());
228
+ let shield_blocked = error_message
229
+ .as_deref()
230
+ .map(|e| e.to_lowercase().contains("shield"))
231
+ .unwrap_or(false)
232
+ || logs.iter().any(|l| l.to_lowercase().contains("shield blocked"));
222
233
  let model = o
223
234
  .get("input")
224
235
  .and_then(|i| i.as_object())
@@ -248,6 +259,8 @@ pub fn run_info_from_gateway_value(v: &Value) -> Option<RunInfo> {
248
259
  total_cost: None,
249
260
  model,
250
261
  error_message,
262
+ shield_blocked,
263
+ safety_reason,
251
264
  logs,
252
265
  })
253
266
  }
@@ -542,7 +555,7 @@ fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
542
555
  let duration_str = run.duration.as_deref().unwrap_or("--");
543
556
 
544
557
  // Build line with colored spans
545
- let line = Line::from(vec![
558
+ let mut spans = vec![
546
559
  Span::styled(
547
560
  if is_selected { "▶ " } else { " " },
548
561
  Style::default().fg(BRAND_PURPLE).bold(),
@@ -551,6 +564,16 @@ fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
551
564
  format!("{:<10}", run.status.as_str()),
552
565
  Style::default().fg(run.status.color()).bold(),
553
566
  ),
567
+ ];
568
+ if run.shield_blocked {
569
+ spans.push(Span::styled(
570
+ " SHIELD ",
571
+ Style::default()
572
+ .fg(Color::Rgb(255, 140, 0))
573
+ .add_modifier(Modifier::BOLD),
574
+ ));
575
+ }
576
+ spans.extend([
554
577
  Span::styled(" │ ", Style::default().fg(TEXT_DIM)),
555
578
  Span::styled(
556
579
  format!("{:<20}", run.name),
@@ -583,6 +606,8 @@ fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
583
606
  ),
584
607
  ]);
585
608
 
609
+ let line = Line::from(spans);
610
+
586
611
  ListItem::new(line)
587
612
  })
588
613
  .collect();
@@ -764,8 +789,28 @@ fn render_detail_view(f: &mut Frame, area: Rect, state: &RunManagerState) {
764
789
  ]));
765
790
  }
766
791
 
767
- // Add error if present
768
- if let Some(error) = &run.error_message {
792
+ if run.shield_blocked {
793
+ info_lines.push(Line::from(""));
794
+ info_lines.push(
795
+ Line::from("Safety (Shield):").style(Style::default().fg(Color::Rgb(255, 140, 0)).bold()),
796
+ );
797
+ info_lines.push(Line::from(vec![
798
+ Span::styled(" Action: ", Style::default().fg(TEXT_DIM)),
799
+ Span::styled("BLOCKED", Style::default().fg(Color::Rgb(255, 140, 0)).bold()),
800
+ ]));
801
+ if let Some(reason) = &run.safety_reason {
802
+ info_lines.push(Line::from(vec![
803
+ Span::styled(" Reason: ", Style::default().fg(TEXT_DIM)),
804
+ Span::styled(reason, Style::default().fg(TEXT_PRIMARY)),
805
+ ]));
806
+ }
807
+ if let Some(error) = &run.error_message {
808
+ info_lines.push(Line::from(vec![
809
+ Span::styled(" Error: ", Style::default().fg(TEXT_DIM)),
810
+ Span::styled(error, Style::default().fg(Color::Rgb(255, 69, 69))),
811
+ ]));
812
+ }
813
+ } else if let Some(error) = &run.error_message {
769
814
  info_lines.push(Line::from(""));
770
815
  info_lines
771
816
  .push(Line::from("Error:").style(Style::default().fg(Color::Rgb(255, 69, 69)).bold()));