4runr-os 2.10.40 → 2.10.41

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.
@@ -48,8 +48,8 @@ pub enum AppMode {
48
48
 
49
49
  #[derive(Debug, Clone, PartialEq)]
50
50
  pub enum OperationMode {
51
- Local, // No Gateway connection
52
- Connected, // Connected to Gateway
51
+ Local, // No Gateway connection
52
+ Connected, // Connected to Gateway
53
53
  }
54
54
 
55
55
  impl OperationMode {
@@ -99,9 +99,9 @@ pub enum PortalField {
99
99
 
100
100
  #[derive(Debug, Clone, PartialEq)]
101
101
  pub enum GatewayType {
102
- FourRunrServer, // External 4Runr cloud server
103
- LocalBundle, // Local Gateway bundle (included with Full OS)
104
- CustomUrl, // User's own Gateway server
102
+ FourRunrServer, // External 4Runr cloud server
103
+ LocalBundle, // Local Gateway bundle (included with Full OS)
104
+ CustomUrl, // User's own Gateway server
105
105
  }
106
106
 
107
107
  impl GatewayType {
@@ -112,7 +112,7 @@ impl GatewayType {
112
112
  GatewayType::CustomUrl => "Custom URL",
113
113
  }
114
114
  }
115
-
115
+
116
116
  pub fn default_url(&self) -> &str {
117
117
  match self {
118
118
  GatewayType::FourRunrServer => "https://gateway.4runr.com",
@@ -125,7 +125,7 @@ impl GatewayType {
125
125
  // Activity Log for Connection Portal
126
126
  #[derive(Debug, Clone)]
127
127
  pub struct ActivityLogEntry {
128
- pub timestamp: String, // HH:MM:SS format
128
+ pub timestamp: String, // HH:MM:SS format
129
129
  pub level: LogLevel,
130
130
  pub message: String,
131
131
  }
@@ -152,7 +152,6 @@ pub struct ConnectionPortalState {
152
152
  pub activity_log: Vec<ActivityLogEntry>,
153
153
  }
154
154
 
155
-
156
155
  impl Default for ConnectionPortalState {
157
156
  fn default() -> Self {
158
157
  Self {
@@ -171,7 +170,10 @@ impl Default for ConnectionPortalState {
171
170
 
172
171
  impl ConnectionPortalState {
173
172
  pub fn reset(&mut self) {
174
- self.gateway_url = self.last_successful_url.clone().unwrap_or_else(|| "http://localhost:3001".to_string());
173
+ self.gateway_url = self
174
+ .last_successful_url
175
+ .clone()
176
+ .unwrap_or_else(|| "http://localhost:3001".to_string());
175
177
  self.username = String::new();
176
178
  self.focused_field = PortalField::GatewayUrl;
177
179
  self.connecting = false;
@@ -190,21 +192,21 @@ impl ConnectionPortalState {
190
192
  self.connecting = false;
191
193
  self.connecting_started = None;
192
194
  }
193
-
195
+
194
196
  pub fn toggle_field(&mut self) {
195
197
  self.focused_field = match self.focused_field {
196
198
  PortalField::GatewayUrl => PortalField::Username,
197
199
  PortalField::Username => PortalField::GatewayUrl,
198
200
  };
199
201
  }
200
-
202
+
201
203
  pub fn add_log_entry(&mut self, timestamp: String, level: LogLevel, message: String) {
202
204
  self.activity_log.push(ActivityLogEntry {
203
205
  timestamp,
204
206
  level,
205
207
  message,
206
208
  });
207
-
209
+
208
210
  // Keep last N lines so the portal can show a full connect trace on small terminals
209
211
  const MAX_ACTIVITY: usize = 80;
210
212
  if self.activity_log.len() > MAX_ACTIVITY {
@@ -361,12 +363,12 @@ impl AdvancedMonitoringState {
361
363
  pub struct AppState {
362
364
  // Navigation state (NEW - replaces simple mode)
363
365
  pub navigation: NavigationState,
364
-
366
+
365
367
  // App mode (DEPRECATED - keeping for backward compatibility during transition)
366
368
  pub mode: AppMode,
367
-
369
+
368
370
  // Boot state
369
- pub boot_progress: u16, // 0-100
371
+ pub boot_progress: u16, // 0-100
370
372
  pub boot_lines: VecDeque<String>,
371
373
  pub boot_done: bool,
372
374
  pub boot_started_at: Instant,
@@ -374,7 +376,7 @@ pub struct AppState {
374
376
  pub connected: bool,
375
377
  #[allow(dead_code)]
376
378
  pub gateway_url: Option<String>,
377
-
379
+
378
380
  // Real component status
379
381
  #[allow(dead_code)]
380
382
  pub gateway_healthy: bool,
@@ -385,35 +387,35 @@ pub struct AppState {
385
387
  pub shield_detectors: Vec<String>, // ["pii", "injection", "hallucination"]
386
388
  pub sentinel_state: String, // "idle", "watching", "triggered"
387
389
  pub sentinel_active_runs: usize,
388
-
390
+
389
391
  // Real metrics
390
392
  pub total_runs: u64,
391
393
  #[allow(dead_code)]
392
394
  pub active_sse_connections: u64,
393
395
  #[allow(dead_code)]
394
396
  pub idempotency_store_size: u64,
395
-
397
+
396
398
  // System resources
397
399
  pub cpu: f64,
398
400
  pub mem: f64,
399
401
  pub network_status: String,
400
-
402
+
401
403
  // Real logs
402
404
  pub logs: VecDeque<String>,
403
-
405
+
404
406
  // Capabilities
405
407
  pub capabilities: Vec<String>,
406
-
408
+
407
409
  // UI state
408
410
  pub command_input: String,
409
411
  pub command_focused: bool,
410
412
  pub log_scroll: usize,
411
-
413
+
412
414
  // Animation state
413
415
  pub tick: u64,
414
416
  pub spinner_frame: usize,
415
417
  pub uptime_secs: u64, // Updated from real clock in main loop, not from tick()
416
-
418
+
417
419
  // Perf overlay
418
420
  pub perf_overlay: bool,
419
421
  pub render_count: u64,
@@ -421,7 +423,7 @@ pub struct AppState {
421
423
  pub render_durations: VecDeque<u64>, // ms
422
424
  pub render_scheduled_count: u64,
423
425
  pub log_write_count: u64,
424
-
426
+
425
427
  // Screen-specific state
426
428
  pub agent_builder: AgentBuilderState,
427
429
  pub run_manager: RunManagerState,
@@ -432,34 +434,34 @@ pub struct AppState {
432
434
  pub portal_monitoring: PortalMonitoringState,
433
435
  /// Last successful observability pull (for auto-refresh interval).
434
436
  pub portal_observability_last_poll: Option<Instant>,
435
-
437
+
436
438
  /// Section selection / expand state for Portal Monitoring (Phase 1).
437
439
  pub advanced_monitoring: AdvancedMonitoringState,
438
-
440
+
439
441
  // Command tracking for response handling (Step 5.1)
440
442
  pub pending_agent_create_id: Option<String>,
441
443
  pub pending_agent_list_id: Option<String>,
442
444
  pub pending_agent_delete_id: Option<String>,
443
445
  pub pending_gateway_connect_id: Option<String>,
444
- pub pending_gateway_health_id: Option<String>, // Phase 2.1: Track health checks
446
+ pub pending_gateway_health_id: Option<String>, // Phase 2.1: Track health checks
445
447
  pub pending_gateway_observability_id: Option<String>,
446
448
  pub pending_monitoring_refresh_id: Option<String>,
447
449
  pub pending_monitoring_logs_id: Option<String>,
448
450
  pub pending_monitoring_drill_id: Option<String>,
449
- pub pending_setup_detect_id: Option<String>, // Track setup.detect command
450
-
451
+ pub pending_setup_detect_id: Option<String>, // Track setup.detect command
452
+
451
453
  // Step 6: Gateway runs
452
454
  pub pending_run_list_id: Option<String>,
453
455
  pub pending_run_get_id: Option<String>,
454
456
  pub pending_run_cancel_id: Option<String>,
455
457
  pub pending_run_quick_id: Option<String>,
456
-
458
+
457
459
  // Deletion confirmation (Step 5.5)
458
460
  pub agent_delete_requested: bool,
459
-
461
+
460
462
  // Mode tracking (Step 7)
461
463
  pub operation_mode: OperationMode,
462
-
464
+
463
465
  // Local cache
464
466
  pub cache: Option<Cache>,
465
467
  pub cache_loaded: bool,
@@ -528,7 +530,7 @@ impl Default for AppState {
528
530
  pending_agent_list_id: None,
529
531
  pending_agent_delete_id: None,
530
532
  pending_gateway_connect_id: None,
531
- pending_gateway_health_id: None, // Phase 2.1: Initialize health check tracking
533
+ pending_gateway_health_id: None, // Phase 2.1: Initialize health check tracking
532
534
  pending_gateway_observability_id: None,
533
535
  pending_monitoring_refresh_id: None,
534
536
  pending_monitoring_logs_id: None,
@@ -561,7 +563,7 @@ impl GatewayOption {
561
563
  GatewayOption::CustomUrl => "Custom URL",
562
564
  }
563
565
  }
564
-
566
+
565
567
  pub fn default_url(&self) -> &str {
566
568
  match self {
567
569
  GatewayOption::LocalBundle => "http://localhost:3001",
@@ -626,15 +628,15 @@ impl SetupPortalState {
626
628
  // Forward (Down key, Scroll Down)
627
629
  (LocalBundle, 1) => CloudServer,
628
630
  (CloudServer, 1) => CustomUrl,
629
- (CustomUrl, 1) => LocalBundle, // Wrap to top
631
+ (CustomUrl, 1) => LocalBundle, // Wrap to top
630
632
  // Backward (Up key, Scroll Up)
631
- (LocalBundle, -1) => CustomUrl, // Wrap to bottom
633
+ (LocalBundle, -1) => CustomUrl, // Wrap to bottom
632
634
  (CloudServer, -1) => LocalBundle,
633
635
  (CustomUrl, -1) => CloudServer,
634
636
  _ => return, // No change for invalid direction
635
637
  };
636
638
  }
637
-
639
+
638
640
  /// Clear detection state and errors (full reset; prefer [`soft_open`](Self::soft_open) when reopening Setup).
639
641
  #[allow(dead_code)]
640
642
  pub fn reset(&mut self) {
@@ -649,7 +651,7 @@ impl SetupPortalState {
649
651
  pub struct AgentListState {
650
652
  pub agents: Vec<AgentInfo>,
651
653
  pub selected_index: usize,
652
- pub detail_view: Option<usize>, // None = list view, Some(index) = detail popup
654
+ pub detail_view: Option<usize>, // None = list view, Some(index) = detail popup
653
655
  }
654
656
 
655
657
  impl Default for AgentListState {
@@ -685,16 +687,16 @@ impl App {
685
687
  pub fn new() -> Self {
686
688
  let render_scheduler = RenderScheduler::new();
687
689
  let run_mode = render_scheduler.run_mode();
688
-
690
+
689
691
  // Input debounce: browser 10ms, local 5ms (minimal delay for smooth typing)
690
692
  let input_debounce_duration = match run_mode {
691
693
  RunMode::Browser => Duration::from_millis(10),
692
694
  RunMode::Local => Duration::from_millis(5),
693
695
  };
694
-
696
+
695
697
  let mut state = AppState::default();
696
698
  Self::load_cached_data(&mut state);
697
-
699
+
698
700
  Self {
699
701
  state,
700
702
  render_scheduler,
@@ -739,7 +741,11 @@ impl App {
739
741
  }
740
742
 
741
743
  /// Phase 2: refresh one Portal Monitoring section via CLI (`monitoring.refresh` → Gateway APIs).
742
- pub fn begin_monitoring_refresh_request(&mut self, ws: &WebSocketClient, section: MonitoringSection) {
744
+ pub fn begin_monitoring_refresh_request(
745
+ &mut self,
746
+ ws: &WebSocketClient,
747
+ section: MonitoringSection,
748
+ ) {
743
749
  if self.state.pending_gateway_observability_id.is_some()
744
750
  || self.state.pending_monitoring_refresh_id.is_some()
745
751
  || self.state.pending_monitoring_logs_id.is_some()
@@ -796,7 +802,11 @@ impl App {
796
802
  }
797
803
 
798
804
  /// Phase 3: Gateway drill-down (`monitoring.drill`) for Metrics sub-panels (HTTP / Runs / Queue / SSE).
799
- pub fn begin_monitoring_drill_request(&mut self, ws: &WebSocketClient, panel: MetricsDrillPanel) {
805
+ pub fn begin_monitoring_drill_request(
806
+ &mut self,
807
+ ws: &WebSocketClient,
808
+ panel: MetricsDrillPanel,
809
+ ) {
800
810
  if self.state.pending_gateway_observability_id.is_some()
801
811
  || self.state.pending_monitoring_refresh_id.is_some()
802
812
  || self.state.pending_monitoring_logs_id.is_some()
@@ -843,7 +853,8 @@ impl App {
843
853
  match ws.send_command("monitoring.drill", Some(data)) {
844
854
  Ok(id) => {
845
855
  self.state.pending_monitoring_drill_id = Some(id);
846
- self.state.portal_monitoring.section_refresh_loading = Some(MonitoringSection::Dependencies);
856
+ self.state.portal_monitoring.section_refresh_loading =
857
+ Some(MonitoringSection::Dependencies);
847
858
  self.state.portal_monitoring.error = None;
848
859
  }
849
860
  Err(e) => {
@@ -871,7 +882,8 @@ impl App {
871
882
  match ws.send_command("monitoring.drill", Some(data)) {
872
883
  Ok(id) => {
873
884
  self.state.pending_monitoring_drill_id = Some(id);
874
- self.state.portal_monitoring.section_refresh_loading = Some(MonitoringSection::System);
885
+ self.state.portal_monitoring.section_refresh_loading =
886
+ Some(MonitoringSection::System);
875
887
  self.state.portal_monitoring.error = None;
876
888
  }
877
889
  Err(e) => {
@@ -901,8 +913,7 @@ impl App {
901
913
  .duration_since(std::time::UNIX_EPOCH)
902
914
  .map(|d| d.as_secs())
903
915
  .unwrap_or(0);
904
- let mut path = std::env::current_dir()
905
- .unwrap_or_else(|_| std::env::temp_dir());
916
+ let mut path = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir());
906
917
  path.push(format!("4runr-monitoring-export-{unix}.json"));
907
918
  let body = serde_json::to_string_pretty(&payload)?;
908
919
  std::fs::write(&path, body)?;
@@ -950,33 +961,35 @@ impl App {
950
961
  // Load cached agents
951
962
  let agents = cache.get_agents();
952
963
  if !agents.is_empty() {
953
- state.capabilities = agents.iter()
954
- .map(|a| a.name.clone())
955
- .collect();
964
+ state.capabilities = agents.iter().map(|a| a.name.clone()).collect();
956
965
  state.cache_loaded = true;
957
966
  }
958
-
967
+
959
968
  // Load cached system status
960
969
  if let Some(status) = cache.get_system_status() {
961
- state.network_status = if status.connected { "Connected (cached)".to_string() } else { "Disconnected (cached)".to_string() };
970
+ state.network_status = if status.connected {
971
+ "Connected (cached)".to_string()
972
+ } else {
973
+ "Disconnected (cached)".to_string()
974
+ };
962
975
  state.posture_status = format!("{} (cached)", status.posture);
963
976
  }
964
977
  }
965
978
  }
966
-
979
+
967
980
  pub fn request_render(&mut self, reason: &str) -> bool {
968
981
  self.render_scheduler.request_render(reason)
969
982
  }
970
-
983
+
971
984
  /// Request immediate render (bypass throttling) - for critical input events
972
985
  pub fn request_immediate_render(&mut self, reason: &str) -> bool {
973
986
  self.render_scheduler.request_immediate_render(reason)
974
987
  }
975
-
988
+
976
989
  pub fn should_render(&mut self) -> bool {
977
990
  self.render_scheduler.should_render()
978
991
  }
979
-
992
+
980
993
  pub fn record_render(&mut self, duration_ms: u64) {
981
994
  self.state.render_count += 1;
982
995
  self.state.render_durations.push_back(duration_ms);
@@ -985,30 +998,30 @@ impl App {
985
998
  }
986
999
  self.state.last_render_time = Instant::now();
987
1000
  }
988
-
1001
+
989
1002
  pub fn run_mode(&self) -> RunMode {
990
1003
  self.render_scheduler.run_mode()
991
1004
  }
992
-
1005
+
993
1006
  pub fn render_scheduler_stats(&self) -> (u64, u64) {
994
1007
  (
995
1008
  self.render_scheduler.render_scheduled_count(),
996
1009
  self.render_scheduler.min_render_interval_ms(),
997
1010
  )
998
1011
  }
999
-
1012
+
1000
1013
  /// IMPORTANT: Only call tick() when poll() times out (NO keyboard input)!
1001
1014
  /// Otherwise animations will flash weirdly when typing.
1002
- ///
1015
+ ///
1003
1016
  /// NOTE: Uptime is now tracked via Instant::now() in main loop,
1004
1017
  /// so we don't increment it here anymore.
1005
1018
  pub fn tick(&mut self) {
1006
1019
  self.state.tick = self.state.tick.wrapping_add(1);
1007
-
1020
+
1008
1021
  // Update spinner frame (for "Processing..." indicator only)
1009
1022
  // Cycle through 8 braille spinner frames
1010
1023
  self.state.spinner_frame = (self.state.spinner_frame + 1) % 8;
1011
-
1024
+
1012
1025
  // Boot timeline: update progress every ~150-250ms
1013
1026
  if self.state.mode == AppMode::Boot && !self.state.boot_done {
1014
1027
  let elapsed = self.state.boot_started_at.elapsed();
@@ -1019,15 +1032,15 @@ impl App {
1019
1032
  (Duration::from_millis(1100), "Telemetry connected…"),
1020
1033
  (Duration::from_millis(1400), "System ready."),
1021
1034
  ];
1022
-
1035
+
1023
1036
  let mut current_progress = 0;
1024
1037
  let mut step_idx = 0;
1025
-
1038
+
1026
1039
  for (i, (delay, msg)) in boot_steps.iter().enumerate() {
1027
1040
  if elapsed >= *delay {
1028
1041
  step_idx = i + 1;
1029
1042
  current_progress = ((i + 1) * 100 / boot_steps.len()) as u16;
1030
-
1043
+
1031
1044
  // Add line if not already added
1032
1045
  if self.state.boot_lines.len() <= i {
1033
1046
  self.state.boot_lines.push_back(msg.to_string());
@@ -1035,23 +1048,28 @@ impl App {
1035
1048
  }
1036
1049
  }
1037
1050
  }
1038
-
1051
+
1039
1052
  self.state.boot_progress = current_progress.min(100);
1040
-
1053
+
1041
1054
  // Mark boot done after last step
1042
1055
  if step_idx >= boot_steps.len() && self.state.boot_progress >= 100 {
1043
1056
  self.state.boot_done = true;
1044
1057
  self.request_render("boot_done");
1045
1058
  }
1046
1059
  }
1047
-
1060
+
1048
1061
  // NOTE: pulse_frame is NOT updated here - we use static dots for status indicators
1049
1062
  // to prevent flashing when typing. Only use animated pulse for non-status elements.
1050
1063
  }
1051
1064
 
1052
- pub fn handle_input(&mut self, key: KeyEvent, _io: &mut IoHandler, ws_client: Option<&WebSocketClient>) -> anyhow::Result<bool> {
1065
+ pub fn handle_input(
1066
+ &mut self,
1067
+ key: KeyEvent,
1068
+ _io: &mut IoHandler,
1069
+ ws_client: Option<&WebSocketClient>,
1070
+ ) -> anyhow::Result<bool> {
1053
1071
  use crossterm::event::KeyModifiers;
1054
-
1072
+
1055
1073
  // === EXIT SHORTCUTS (always checked first) ===
1056
1074
  if key.modifiers.contains(KeyModifiers::CONTROL) {
1057
1075
  match key.code {
@@ -1061,45 +1079,45 @@ impl App {
1061
1079
  _ => return Ok(false), // Ignore other Ctrl combos
1062
1080
  }
1063
1081
  }
1064
-
1082
+
1065
1083
  if key.code == KeyCode::F(10) {
1066
1084
  return Ok(true); // Exit
1067
1085
  }
1068
-
1086
+
1069
1087
  // === BOOT MODE: Any key to continue ===
1070
1088
  if self.state.mode == AppMode::Boot && self.state.boot_done {
1071
1089
  // Any key press switches to main dashboard
1072
1090
  self.navigate_to(Screen::Main);
1073
1091
  return Ok(false);
1074
1092
  }
1075
-
1093
+
1076
1094
  // In boot mode, ignore other input until boot is done
1077
1095
  if self.state.mode == AppMode::Boot {
1078
1096
  return Ok(false);
1079
1097
  }
1080
-
1098
+
1081
1099
  // F12 - Toggle perf overlay
1082
1100
  if key.code == KeyCode::F(12) {
1083
1101
  self.state.perf_overlay = !self.state.perf_overlay;
1084
1102
  self.request_render("perf_toggle");
1085
1103
  return Ok(false);
1086
1104
  }
1087
-
1105
+
1088
1106
  // === AGENT BUILDER INPUT HANDLING ===
1089
1107
  if self.state.navigation.current_screen() == &Screen::AgentBuilder {
1090
1108
  return self.handle_agent_builder_input(key, ws_client);
1091
1109
  }
1092
-
1110
+
1093
1111
  // === RUN MANAGER INPUT HANDLING ===
1094
1112
  if self.state.navigation.current_screen() == &Screen::RunManager {
1095
1113
  return self.handle_run_manager_input(key, ws_client);
1096
1114
  }
1097
-
1115
+
1098
1116
  // === SETTINGS INPUT HANDLING ===
1099
1117
  if self.state.navigation.current_screen() == &Screen::Settings {
1100
1118
  return self.handle_settings_input(key);
1101
1119
  }
1102
-
1120
+
1103
1121
  // === CONNECTION PORTAL INPUT HANDLING ===
1104
1122
  if self.state.navigation.current_screen() == &Screen::ConnectionPortal {
1105
1123
  return self.handle_connection_portal_input(key, ws_client);
@@ -1109,17 +1127,17 @@ impl App {
1109
1127
  if self.state.navigation.current_screen() == &Screen::PortalMonitoring {
1110
1128
  return self.handle_portal_monitoring_input(key, ws_client);
1111
1129
  }
1112
-
1130
+
1113
1131
  // === SETUP PORTAL INPUT HANDLING ===
1114
1132
  if self.state.navigation.current_screen() == &Screen::SetupPortal {
1115
1133
  return self.handle_setup_portal_input(key, ws_client);
1116
1134
  }
1117
-
1135
+
1118
1136
  // === AGENT LIST INPUT HANDLING ===
1119
1137
  if self.state.navigation.current_screen() == &Screen::AgentList {
1120
1138
  return self.handle_agent_list_input(key, ws_client);
1121
1139
  }
1122
-
1140
+
1123
1141
  // === MAIN INPUT HANDLING ===
1124
1142
  match key.code {
1125
1143
  // F2 - Open Setup Portal (Gateway options) from main screen
@@ -1135,7 +1153,8 @@ impl App {
1135
1153
  } else {
1136
1154
  self.state.setup_portal.detecting = false;
1137
1155
  self.state.setup_portal.detecting_since = None;
1138
- self.state.setup_portal.error = Some("Failed to start detection".to_string());
1156
+ self.state.setup_portal.error =
1157
+ Some("Failed to start detection".to_string());
1139
1158
  }
1140
1159
  } else {
1141
1160
  self.state.setup_portal.error = Some(
@@ -1143,7 +1162,9 @@ impl App {
1143
1162
  .into(),
1144
1163
  );
1145
1164
  }
1146
- self.state.logs.push_back("[NAV] Opening Setup Portal (F2)...".into());
1165
+ self.state
1166
+ .logs
1167
+ .push_back("[NAV] Opening Setup Portal (F2)...".into());
1147
1168
  self.request_immediate_render("open_setup_portal");
1148
1169
  }
1149
1170
  // Typing - ALL characters go to command input (immediate render for responsiveness)
@@ -1154,14 +1175,14 @@ impl App {
1154
1175
  // OPTIMIZATION: Immediate render for instant typing feedback
1155
1176
  self.request_immediate_render("typing_input");
1156
1177
  }
1157
-
1178
+
1158
1179
  // Submit command
1159
1180
  KeyCode::Enter => {
1160
1181
  if !self.state.command_input.is_empty() {
1161
1182
  let cmd: String = self.state.command_input.drain(..).collect();
1162
1183
  self.state.command_focused = false;
1163
1184
  self.input_debounce = None;
1164
-
1185
+
1165
1186
  match cmd.to_lowercase().as_str() {
1166
1187
  "quit" | "exit" => return Ok(true),
1167
1188
  "clear" => self.state.logs.clear(),
@@ -1169,59 +1190,131 @@ impl App {
1169
1190
  // Display help in operations log - clean and organized
1170
1191
  // NOTE: VecDeque displays newest items first, so we push in REVERSE order
1171
1192
  // (Last item pushed appears at top of log)
1172
-
1193
+
1173
1194
  // Push Keyboard Shortcuts FIRST (will appear at BOTTOM)
1174
- self.state.logs.push_back(" Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save".to_string());
1175
- self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".to_string());
1176
- self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Back, ESC=Cancel".to_string());
1177
- self.state.logs.push_back("────────────────────────────────────────────────────────────────".to_string());
1178
- self.state.logs.push_back("⌨️ KEYBOARD SHORTCUTS".to_string());
1195
+ self.state.logs.push_back(
1196
+ " Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save"
1197
+ .to_string(),
1198
+ );
1199
+ self.state.logs.push_back(
1200
+ " Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh"
1201
+ .to_string(),
1202
+ );
1203
+ self.state.logs.push_back(
1204
+ " Agent Builder: Enter=Next, Backspace=Back, ESC=Cancel"
1205
+ .to_string(),
1206
+ );
1207
+ self.state.logs.push_back(
1208
+ "────────────────────────────────────────────────────────────────"
1209
+ .to_string(),
1210
+ );
1211
+ self.state
1212
+ .logs
1213
+ .push_back("⌨️ KEYBOARD SHORTCUTS".to_string());
1179
1214
  self.state.logs.push_back("".to_string());
1180
-
1215
+
1181
1216
  // WebSocket Commands
1182
- self.state.logs.push_back(" tool.list - List available tools".to_string());
1183
- self.state.logs.push_back(" run.list - List runs (requires gateway)".to_string());
1184
- self.state.logs.push_back(" system.status - Get system status".to_string());
1185
- self.state.logs.push_back(" agent.delete - Delete agent (data: {name})".to_string());
1186
- self.state.logs.push_back(" agent.create - Create new agent (use Agent Builder)".to_string());
1187
- self.state.logs.push_back(" agent.get - Get agent details (data: {name})".to_string());
1188
- self.state.logs.push_back(" agent.list - List all agents".to_string());
1189
- self.state.logs.push_back("────────────────────────────────────────────────────────────────".to_string());
1190
- self.state.logs.push_back("🌐 WEBSOCKET COMMANDS (requires connection)".to_string());
1217
+ self.state.logs.push_back(
1218
+ " tool.list - List available tools".to_string(),
1219
+ );
1220
+ self.state.logs.push_back(
1221
+ " run.list - List runs (requires gateway)".to_string(),
1222
+ );
1223
+ self.state
1224
+ .logs
1225
+ .push_back(" system.status - Get system status".to_string());
1226
+ self.state.logs.push_back(
1227
+ " agent.delete - Delete agent (data: {name})".to_string(),
1228
+ );
1229
+ self.state.logs.push_back(
1230
+ " agent.create - Create new agent (use Agent Builder)"
1231
+ .to_string(),
1232
+ );
1233
+ self.state.logs.push_back(
1234
+ " agent.get - Get agent details (data: {name})"
1235
+ .to_string(),
1236
+ );
1237
+ self.state
1238
+ .logs
1239
+ .push_back(" agent.list - List all agents".to_string());
1240
+ self.state.logs.push_back(
1241
+ "────────────────────────────────────────────────────────────────"
1242
+ .to_string(),
1243
+ );
1244
+ self.state.logs.push_back(
1245
+ "🌐 WEBSOCKET COMMANDS (requires connection)".to_string(),
1246
+ );
1191
1247
  self.state.logs.push_back("".to_string());
1192
-
1248
+
1193
1249
  // Local Commands
1194
- self.state.logs.push_back(" :perf - Show performance stats".to_string());
1195
- self.state.logs.push_back(" help - Show this help".to_string());
1196
- self.state.logs.push_back(" clear - Clear operations log".to_string());
1197
- self.state.logs.push_back(" quit, exit - Exit application".to_string());
1198
- self.state.logs.push_back("────────────────────────────────────────────────────────────────".to_string());
1250
+ self.state.logs.push_back(
1251
+ " :perf - Show performance stats".to_string(),
1252
+ );
1253
+ self.state
1254
+ .logs
1255
+ .push_back(" help - Show this help".to_string());
1256
+ self.state.logs.push_back(
1257
+ " clear - Clear operations log".to_string(),
1258
+ );
1259
+ self.state
1260
+ .logs
1261
+ .push_back(" quit, exit - Exit application".to_string());
1262
+ self.state.logs.push_back(
1263
+ "────────────────────────────────────────────────────────────────"
1264
+ .to_string(),
1265
+ );
1199
1266
  self.state.logs.push_back("💻 LOCAL COMMANDS".to_string());
1200
1267
  self.state.logs.push_back("".to_string());
1201
-
1268
+
1202
1269
  // Navigation Commands
1203
1270
  self.state.logs.push_back(" disconnect - Disconnect from Gateway (return to LOCAL mode)".to_string());
1204
1271
  self.state.logs.push_back(" portal monitoring - Gateway /metrics traffic (R refresh, ↑↓ scroll)".to_string());
1205
1272
  self.state.logs.push_back(" connect portal - Open Connection Portal (connect to Gateway)".to_string());
1206
- self.state.logs.push_back(" a, agents - Open Agent List (view, select, delete)".to_string());
1207
- self.state.logs.push_back(" config, settings - Open Settings (mode, AI provider)".to_string());
1208
- self.state.logs.push_back(" runs - Open Run Manager (list, filter, sort)".to_string());
1209
- self.state.logs.push_back(" build - Open Agent Builder (6-step wizard)".to_string());
1210
- self.state.logs.push_back("────────────────────────────────────────────────────────────────".to_string());
1211
- self.state.logs.push_back("🧭 NAVIGATION COMMANDS".to_string());
1273
+ self.state.logs.push_back(
1274
+ " a, agents - Open Agent List (view, select, delete)"
1275
+ .to_string(),
1276
+ );
1277
+ self.state.logs.push_back(
1278
+ " config, settings - Open Settings (mode, AI provider)"
1279
+ .to_string(),
1280
+ );
1281
+ self.state.logs.push_back(
1282
+ " runs - Open Run Manager (list, filter, sort)"
1283
+ .to_string(),
1284
+ );
1285
+ self.state.logs.push_back(
1286
+ " build - Open Agent Builder (6-step wizard)"
1287
+ .to_string(),
1288
+ );
1289
+ self.state.logs.push_back(
1290
+ "────────────────────────────────────────────────────────────────"
1291
+ .to_string(),
1292
+ );
1293
+ self.state
1294
+ .logs
1295
+ .push_back("🧭 NAVIGATION COMMANDS".to_string());
1212
1296
  self.state.logs.push_back("".to_string());
1213
-
1297
+
1214
1298
  // Push Header LAST (will appear at TOP)
1215
- self.state.logs.push_back("════════════════════════════════════════════════════════════════".to_string());
1216
- self.state.logs.push_back("📖 4RUNR AI AGENT OS - COMMAND REFERENCE".to_string());
1217
- self.state.logs.push_back("════════════════════════════════════════════════════════════════".to_string());
1218
-
1299
+ self.state.logs.push_back(
1300
+ "════════════════════════════════════════════════════════════════"
1301
+ .to_string(),
1302
+ );
1303
+ self.state
1304
+ .logs
1305
+ .push_back("📖 4RUNR AI AGENT OS - COMMAND REFERENCE".to_string());
1306
+ self.state.logs.push_back(
1307
+ "════════════════════════════════════════════════════════════════"
1308
+ .to_string(),
1309
+ );
1310
+
1219
1311
  self.request_render("help_command");
1220
1312
  }
1221
1313
  ":perf" => {
1222
1314
  // Perf self-check command
1223
1315
  let rps = if !self.state.render_durations.is_empty() {
1224
- let avg_ms: f64 = self.state.render_durations.iter().sum::<u64>() as f64
1316
+ let avg_ms: f64 = self.state.render_durations.iter().sum::<u64>()
1317
+ as f64
1225
1318
  / self.state.render_durations.len() as f64;
1226
1319
  if avg_ms > 0.0 {
1227
1320
  (1000.0 / avg_ms) as u64
@@ -1231,30 +1324,31 @@ impl App {
1231
1324
  } else {
1232
1325
  0
1233
1326
  };
1234
-
1327
+
1235
1328
  let mode_str = match self.run_mode() {
1236
1329
  RunMode::Browser => "browser",
1237
1330
  RunMode::Local => "local",
1238
1331
  };
1239
-
1332
+
1240
1333
  let (scheduled_count, interval_ms) = self.render_scheduler_stats();
1241
-
1334
+
1242
1335
  self.state.logs.push_back(format!(
1243
1336
  "[PERF] Mode: {} | RPS: {} | Render interval: {}ms | Scheduled: {}",
1244
- mode_str,
1245
- rps,
1246
- interval_ms,
1247
- scheduled_count
1337
+ mode_str, rps, interval_ms, scheduled_count
1248
1338
  ));
1249
1339
  }
1250
1340
  // Navigation commands
1251
1341
  "build" | "build new" | "agent new" => {
1252
1342
  self.push_overlay(Screen::AgentBuilder);
1253
- self.state.logs.push_back("[NAV] Opening Agent Builder...".into());
1343
+ self.state
1344
+ .logs
1345
+ .push_back("[NAV] Opening Agent Builder...".into());
1254
1346
  }
1255
1347
  "runs" | "run list" | "run manager" => {
1256
1348
  self.push_overlay(Screen::RunManager);
1257
- self.state.logs.push_back("[NAV] Opening Run Manager...".into());
1349
+ self.state
1350
+ .logs
1351
+ .push_back("[NAV] Opening Run Manager...".into());
1258
1352
  if self.state.operation_mode == OperationMode::Connected {
1259
1353
  if let Some(ws) = ws_client {
1260
1354
  let data = serde_json::json!({ "limit": 50 });
@@ -1264,16 +1358,23 @@ impl App {
1264
1358
  }
1265
1359
  }
1266
1360
  } else {
1267
- self.add_log("[RUN] Connect to Gateway (connect portal) to load live runs.".to_string());
1361
+ self.add_log(
1362
+ "[RUN] Connect to Gateway (connect portal) to load live runs."
1363
+ .to_string(),
1364
+ );
1268
1365
  }
1269
1366
  }
1270
1367
  "config" | "settings" => {
1271
1368
  self.push_overlay(Screen::Settings);
1272
- self.state.logs.push_back("[NAV] Opening Settings...".into());
1369
+ self.state
1370
+ .logs
1371
+ .push_back("[NAV] Opening Settings...".into());
1273
1372
  }
1274
1373
  "a" | "agents" | "agent list" => {
1275
1374
  self.push_overlay(Screen::AgentList);
1276
- self.state.logs.push_back("[NAV] Opening Agent List...".into());
1375
+ self.state
1376
+ .logs
1377
+ .push_back("[NAV] Opening Agent List...".into());
1277
1378
  // Request agent list from backend
1278
1379
  if let Some(ws) = ws_client {
1279
1380
  if let Ok(list_id) = ws.send_command("agent.list", None) {
@@ -1289,7 +1390,9 @@ impl App {
1289
1390
  self.state.advanced_monitoring =
1290
1391
  AdvancedMonitoringState::new(self.state.gateway_url.clone());
1291
1392
  self.state.portal_monitoring.scroll_offset = 0;
1292
- self.state.logs.push_back("[NAV] Opening Portal Monitoring...".into());
1393
+ self.state
1394
+ .logs
1395
+ .push_back("[NAV] Opening Portal Monitoring...".into());
1293
1396
  if let Some(ws) = ws_client {
1294
1397
  self.begin_portal_observability_request(ws);
1295
1398
  }
@@ -1307,25 +1410,31 @@ impl App {
1307
1410
  self.state
1308
1411
  .navigation
1309
1412
  .navigate_to_base(Screen::ConnectionPortal);
1310
- self.state.logs.push_back("[NAV] Opening Connection Portal...".into());
1413
+ self.state
1414
+ .logs
1415
+ .push_back("[NAV] Opening Connection Portal...".into());
1311
1416
  self.request_immediate_render("open_connection_portal");
1312
1417
  }
1313
1418
  "setup" | "setup gateway" | "setup portal" => {
1314
1419
  // Navigate to portal as base screen (standalone, not overlay)
1315
1420
  self.state.navigation.navigate_to_base(Screen::SetupPortal);
1316
1421
  self.state.setup_portal.soft_open();
1317
-
1422
+
1318
1423
  // Auto-detect Gateway options when portal opens
1319
1424
  if let Some(ws) = ws_client {
1320
1425
  self.state.setup_portal.detecting = true;
1321
- self.state.setup_portal.detecting_since = Some(std::time::Instant::now());
1426
+ self.state.setup_portal.detecting_since =
1427
+ Some(std::time::Instant::now());
1322
1428
  if let Ok(id) = ws.send_command("setup.detect", None) {
1323
1429
  self.state.pending_setup_detect_id = Some(id);
1324
- self.add_log("[SETUP] Detecting Gateway options...".to_string());
1430
+ self.add_log(
1431
+ "[SETUP] Detecting Gateway options...".to_string(),
1432
+ );
1325
1433
  } else {
1326
1434
  self.state.setup_portal.detecting = false;
1327
1435
  self.state.setup_portal.detecting_since = None;
1328
- self.state.setup_portal.error = Some("Failed to start detection".to_string());
1436
+ self.state.setup_portal.error =
1437
+ Some("Failed to start detection".to_string());
1329
1438
  }
1330
1439
  } else {
1331
1440
  self.state.setup_portal.error = Some(
@@ -1333,8 +1442,10 @@ impl App {
1333
1442
  .into(),
1334
1443
  );
1335
1444
  }
1336
-
1337
- self.state.logs.push_back("[NAV] Opening Setup Portal...".into());
1445
+
1446
+ self.state
1447
+ .logs
1448
+ .push_back("[NAV] Opening Setup Portal...".into());
1338
1449
  self.request_immediate_render("open_setup_portal");
1339
1450
  }
1340
1451
  "disconnect" => {
@@ -1343,15 +1454,23 @@ impl App {
1343
1454
  match ws.send_command("gateway.disconnect", None) {
1344
1455
  Ok(id) => {
1345
1456
  let short_id = &id[id.len().saturating_sub(8)..];
1346
- self.state.logs.push_back(format!("[GATEWAY] Disconnecting (id: {})", short_id));
1457
+ self.state.logs.push_back(format!(
1458
+ "[GATEWAY] Disconnecting (id: {})",
1459
+ short_id
1460
+ ));
1347
1461
  // Mode will be updated when response is received
1348
1462
  }
1349
1463
  Err(e) => {
1350
- self.state.logs.push_back(format!("[ERROR] Failed to disconnect: {}", e));
1464
+ self.state.logs.push_back(format!(
1465
+ "[ERROR] Failed to disconnect: {}",
1466
+ e
1467
+ ));
1351
1468
  }
1352
1469
  }
1353
1470
  } else {
1354
- self.state.logs.push_back("[ERROR] WebSocket not connected".into());
1471
+ self.state
1472
+ .logs
1473
+ .push_back("[ERROR] WebSocket not connected".into());
1355
1474
  }
1356
1475
  }
1357
1476
  _ => {
@@ -1363,29 +1482,40 @@ impl App {
1363
1482
  match ws.send_command(&cmd, None) {
1364
1483
  Ok(id) => {
1365
1484
  let short_id = &id[id.len().saturating_sub(8)..];
1366
- self.state.logs.push_back(format!("> {} [{}]", cmd, short_id));
1485
+ self.state
1486
+ .logs
1487
+ .push_back(format!("> {} [{}]", cmd, short_id));
1367
1488
  }
1368
1489
  Err(e) => {
1369
- self.state.logs.push_back(format!("[ERROR] Failed to send '{}': {}", cmd, e));
1490
+ self.state.logs.push_back(format!(
1491
+ "[ERROR] Failed to send '{}': {}",
1492
+ cmd, e
1493
+ ));
1370
1494
  }
1371
1495
  }
1372
1496
  } else {
1373
1497
  // Unknown local command - provide helpful guidance
1374
- self.state.logs.push_back(format!("> {} (unknown command)", cmd));
1498
+ self.state
1499
+ .logs
1500
+ .push_back(format!("> {} (unknown command)", cmd));
1375
1501
  self.state.logs.push_back("[HELP] Try: help, build, runs, config, agent.list, system.status".into());
1376
1502
  }
1377
1503
  } else {
1378
1504
  // No WebSocket connection - guide to local commands
1379
- self.state.logs.push_back(format!("> {} (WebSocket not connected)", cmd));
1505
+ self.state
1506
+ .logs
1507
+ .push_back(format!("> {} (WebSocket not connected)", cmd));
1380
1508
  self.state.logs.push_back("[WARN] Backend not running. Start backend or use local commands.".into());
1381
- self.state.logs.push_back("[HELP] Local: help, build, runs, config, clear".into());
1509
+ self.state.logs.push_back(
1510
+ "[HELP] Local: help, build, runs, config, clear".into(),
1511
+ );
1382
1512
  }
1383
1513
  }
1384
1514
  }
1385
1515
  self.request_render("command_submit");
1386
1516
  }
1387
1517
  }
1388
-
1518
+
1389
1519
  // Delete character
1390
1520
  KeyCode::Backspace => {
1391
1521
  self.state.command_input.pop();
@@ -1395,7 +1525,7 @@ impl App {
1395
1525
  // OPTIMIZATION: Immediate render for instant feedback
1396
1526
  self.request_immediate_render("backspace_input");
1397
1527
  }
1398
-
1528
+
1399
1529
  // Clear input or close overlay/popup
1400
1530
  KeyCode::Esc => {
1401
1531
  // Priority: close popup > close overlay > clear input
@@ -1410,41 +1540,41 @@ impl App {
1410
1540
  self.request_render("clear_input");
1411
1541
  }
1412
1542
  }
1413
-
1543
+
1414
1544
  // === SCROLL: Arrow keys (only when not typing) ===
1415
1545
  // Calculate max valid scroll based on visible height
1416
1546
  KeyCode::Up if self.state.command_input.is_empty() => {
1417
1547
  let visible_height = 15; // Approximate visible log height (adjust based on actual panel size)
1418
1548
  let total_logs = self.state.logs.len();
1419
1549
  let max_scroll = total_logs.saturating_sub(visible_height.max(1));
1420
-
1550
+
1421
1551
  // Clamp scroll to valid range [0, max_scroll]
1422
1552
  self.state.log_scroll = (self.state.log_scroll + 1).min(max_scroll);
1423
1553
  self.request_render("scroll_up");
1424
1554
  }
1425
-
1555
+
1426
1556
  KeyCode::Down if self.state.command_input.is_empty() => {
1427
1557
  self.state.log_scroll = self.state.log_scroll.saturating_sub(1);
1428
1558
  self.request_render("scroll_down");
1429
1559
  }
1430
-
1560
+
1431
1561
  KeyCode::PageUp if self.state.command_input.is_empty() => {
1432
1562
  let visible_height = 15;
1433
1563
  let total_logs = self.state.logs.len();
1434
1564
  let max_scroll = total_logs.saturating_sub(visible_height.max(1));
1435
-
1565
+
1436
1566
  self.state.log_scroll = (self.state.log_scroll + 10).min(max_scroll);
1437
1567
  self.request_render("scroll_page_up");
1438
1568
  }
1439
-
1569
+
1440
1570
  KeyCode::PageDown if self.state.command_input.is_empty() => {
1441
1571
  self.state.log_scroll = self.state.log_scroll.saturating_sub(10);
1442
1572
  self.request_render("scroll_page_down");
1443
1573
  }
1444
-
1574
+
1445
1575
  _ => {}
1446
1576
  }
1447
-
1577
+
1448
1578
  Ok(false)
1449
1579
  }
1450
1580
 
@@ -1476,15 +1606,21 @@ impl App {
1476
1606
  pub fn render(&mut self, f: &mut Frame) {
1477
1607
  // Use new navigation system - render based on current screen
1478
1608
  let current = self.state.navigation.current_screen();
1479
-
1609
+
1480
1610
  // DEBUG: Log render decisions
1481
1611
  #[cfg(debug_assertions)]
1482
1612
  {
1483
1613
  eprintln!("[RENDER] Current screen: {:?}", current);
1484
- eprintln!("[RENDER] Base screen: {:?}", self.state.navigation.base_screen);
1485
- eprintln!("[RENDER] Overlay stack: {:?}", self.state.navigation.overlay_stack);
1614
+ eprintln!(
1615
+ "[RENDER] Base screen: {:?}",
1616
+ self.state.navigation.base_screen
1617
+ );
1618
+ eprintln!(
1619
+ "[RENDER] Overlay stack: {:?}",
1620
+ self.state.navigation.overlay_stack
1621
+ );
1486
1622
  }
1487
-
1623
+
1488
1624
  match current {
1489
1625
  Screen::Boot => {
1490
1626
  use crate::ui::boot;
@@ -1548,7 +1684,7 @@ impl App {
1548
1684
  }
1549
1685
  }
1550
1686
  }
1551
-
1687
+
1552
1688
  /// Add a log message
1553
1689
  pub fn add_log(&mut self, message: String) {
1554
1690
  self.state.logs.push_back(message);
@@ -1557,27 +1693,27 @@ impl App {
1557
1693
  }
1558
1694
  self.state.log_write_count += 1;
1559
1695
  }
1560
-
1696
+
1561
1697
  // ============================================================
1562
1698
  // NAVIGATION METHODS (NEW - Step 4.1)
1563
1699
  // ============================================================
1564
-
1700
+
1565
1701
  /// Navigate to a base screen (Boot or Main)
1566
1702
  pub fn navigate_to(&mut self, screen: Screen) {
1567
1703
  if screen.is_base() {
1568
1704
  self.state.navigation.navigate_to_base(screen.clone());
1569
-
1705
+
1570
1706
  // Update legacy mode field for backward compatibility
1571
1707
  self.state.mode = match screen {
1572
1708
  Screen::Boot => AppMode::Boot,
1573
1709
  Screen::Main => AppMode::Main,
1574
1710
  _ => self.state.mode.clone(),
1575
1711
  };
1576
-
1712
+
1577
1713
  self.request_render("navigate_to");
1578
1714
  }
1579
1715
  }
1580
-
1716
+
1581
1717
  /// Push an overlay screen (Agent Builder, Run Manager, Settings)
1582
1718
  pub fn push_overlay(&mut self, screen: Screen) {
1583
1719
  if screen.is_overlay() {
@@ -1585,7 +1721,7 @@ impl App {
1585
1721
  self.request_render("push_overlay");
1586
1722
  }
1587
1723
  }
1588
-
1724
+
1589
1725
  /// Pop the current overlay and return to previous screen
1590
1726
  pub fn pop_overlay(&mut self) -> Option<Screen> {
1591
1727
  let result = self.state.navigation.pop_overlay();
@@ -1594,7 +1730,7 @@ impl App {
1594
1730
  }
1595
1731
  result
1596
1732
  }
1597
-
1733
+
1598
1734
  /// Push a popup screen (Confirmation, Alert, Help)
1599
1735
  pub fn push_popup(&mut self, screen: Screen) {
1600
1736
  if screen.is_popup() {
@@ -1602,7 +1738,7 @@ impl App {
1602
1738
  self.request_render("push_popup");
1603
1739
  }
1604
1740
  }
1605
-
1741
+
1606
1742
  /// Pop the current popup
1607
1743
  pub fn pop_popup(&mut self) -> Option<Screen> {
1608
1744
  let result = self.state.navigation.pop_popup();
@@ -1611,34 +1747,38 @@ impl App {
1611
1747
  }
1612
1748
  result
1613
1749
  }
1614
-
1750
+
1615
1751
  /// Close all overlays and popups (return to base screen)
1616
1752
  #[allow(dead_code)]
1617
1753
  pub fn close_all_overlays(&mut self) {
1618
1754
  self.state.navigation.close_all();
1619
1755
  self.request_render("close_all");
1620
1756
  }
1621
-
1757
+
1622
1758
  /// Get the currently visible screen
1623
1759
  #[allow(dead_code)]
1624
1760
  pub fn current_screen(&self) -> &Screen {
1625
1761
  self.state.navigation.current_screen()
1626
1762
  }
1627
-
1763
+
1628
1764
  /// Check if we're on the base screen (no overlays/popups)
1629
1765
  #[allow(dead_code)]
1630
1766
  pub fn is_on_base_screen(&self) -> bool {
1631
1767
  self.state.navigation.is_on_base()
1632
1768
  }
1633
-
1769
+
1634
1770
  // ============================================================
1635
1771
  // AGENT BUILDER INPUT HANDLING (Step 4.5)
1636
1772
  // ============================================================
1637
-
1773
+
1638
1774
  /// Handle input when Agent Builder screen is active
1639
- fn handle_agent_builder_input(&mut self, key: KeyEvent, ws_client: Option<&WebSocketClient>) -> anyhow::Result<bool> {
1775
+ fn handle_agent_builder_input(
1776
+ &mut self,
1777
+ key: KeyEvent,
1778
+ ws_client: Option<&WebSocketClient>,
1779
+ ) -> anyhow::Result<bool> {
1640
1780
  use crossterm::event::KeyModifiers;
1641
-
1781
+
1642
1782
  // Ctrl+C/Q to exit
1643
1783
  if key.modifiers.contains(KeyModifiers::CONTROL) {
1644
1784
  match key.code {
@@ -1646,7 +1786,7 @@ impl App {
1646
1786
  _ => {}
1647
1787
  }
1648
1788
  }
1649
-
1789
+
1650
1790
  match key.code {
1651
1791
  // Character input - route to appropriate field
1652
1792
  KeyCode::Char(c) => {
@@ -1723,15 +1863,15 @@ impl App {
1723
1863
  }
1724
1864
  _ => {}
1725
1865
  }
1726
-
1866
+
1727
1867
  Ok(false)
1728
1868
  }
1729
-
1869
+
1730
1870
  /// Handle character input for Agent Builder fields
1731
1871
  fn handle_agent_builder_char_input(&mut self, c: char) {
1732
1872
  let step = self.state.agent_builder.current_step;
1733
1873
  let field = self.state.agent_builder.focused_field;
1734
-
1874
+
1735
1875
  match step {
1736
1876
  1 => {
1737
1877
  // Step 1: Basic Info
@@ -1789,7 +1929,10 @@ impl App {
1789
1929
  if field < tools.len() {
1790
1930
  let tool = tools[field].clone();
1791
1931
  if self.state.agent_builder.selected_tools.contains(&tool) {
1792
- self.state.agent_builder.selected_tools.retain(|t| t != &tool);
1932
+ self.state
1933
+ .agent_builder
1934
+ .selected_tools
1935
+ .retain(|t| t != &tool);
1793
1936
  } else {
1794
1937
  self.state.agent_builder.selected_tools.push(tool);
1795
1938
  }
@@ -1799,30 +1942,40 @@ impl App {
1799
1942
  _ => {}
1800
1943
  }
1801
1944
  }
1802
-
1945
+
1803
1946
  /// Handle backspace for Agent Builder fields
1804
1947
  fn handle_agent_builder_backspace(&mut self) {
1805
1948
  let step = self.state.agent_builder.current_step;
1806
1949
  let field = self.state.agent_builder.focused_field;
1807
-
1950
+
1808
1951
  match step {
1809
- 1 => {
1810
- match field {
1811
- 0 => { self.state.agent_builder.name.pop(); }
1812
- 1 => { self.state.agent_builder.description.pop(); }
1813
- _ => {}
1952
+ 1 => match field {
1953
+ 0 => {
1954
+ self.state.agent_builder.name.pop();
1814
1955
  }
1815
- }
1816
- 2 => {
1817
- match field {
1818
- 0 => { self.state.agent_builder.provider.pop(); }
1819
- 1 => { self.state.agent_builder.model.pop(); }
1820
- 2 => { self.state.agent_builder.local_provider.pop(); }
1821
- 3 => { self.state.agent_builder.local_model.pop(); }
1822
- 4 => { self.state.agent_builder.local_url.pop(); }
1823
- _ => {}
1956
+ 1 => {
1957
+ self.state.agent_builder.description.pop();
1824
1958
  }
1825
- }
1959
+ _ => {}
1960
+ },
1961
+ 2 => match field {
1962
+ 0 => {
1963
+ self.state.agent_builder.provider.pop();
1964
+ }
1965
+ 1 => {
1966
+ self.state.agent_builder.model.pop();
1967
+ }
1968
+ 2 => {
1969
+ self.state.agent_builder.local_provider.pop();
1970
+ }
1971
+ 3 => {
1972
+ self.state.agent_builder.local_model.pop();
1973
+ }
1974
+ 4 => {
1975
+ self.state.agent_builder.local_url.pop();
1976
+ }
1977
+ _ => {}
1978
+ },
1826
1979
  3 => {
1827
1980
  self.state.agent_builder.system_prompt.pop();
1828
1981
  }
@@ -1853,12 +2006,14 @@ impl App {
1853
2006
  _ => {}
1854
2007
  }
1855
2008
  }
1856
-
2009
+
1857
2010
  /// Create agent from builder state and send to backend
1858
2011
  fn create_agent_from_builder(&mut self, ws_client: Option<&WebSocketClient>) {
1859
2012
  // Validate one final time
1860
2013
  if !self.state.agent_builder.validate_step() {
1861
- self.add_log("[ERROR] Agent validation failed. Please fix errors and try again.".to_string());
2014
+ self.add_log(
2015
+ "[ERROR] Agent validation failed. Please fix errors and try again.".to_string(),
2016
+ );
1862
2017
  // Clone errors to avoid borrow checker issues
1863
2018
  let errors = self.state.agent_builder.validation_errors.clone();
1864
2019
  for error in &errors {
@@ -1866,16 +2021,16 @@ impl App {
1866
2021
  }
1867
2022
  return;
1868
2023
  }
1869
-
2024
+
1870
2025
  // Clone name before borrowing mutably
1871
2026
  let agent_name = self.state.agent_builder.name.clone();
1872
-
2027
+
1873
2028
  // Validate name is not empty
1874
2029
  if agent_name.trim().is_empty() {
1875
2030
  self.add_log("[ERROR] Agent name cannot be empty".to_string());
1876
2031
  return;
1877
2032
  }
1878
-
2033
+
1879
2034
  // Build agent data
1880
2035
  let builder = &self.state.agent_builder;
1881
2036
  let agent_data = serde_json::json!({
@@ -1910,10 +2065,10 @@ impl App {
1910
2065
  "presencePenalty": builder.presence_penalty,
1911
2066
  "tools": builder.selected_tools,
1912
2067
  });
1913
-
2068
+
1914
2069
  // Log the creation attempt
1915
2070
  self.add_log(format!("[AGENT] Creating agent '{}'...", agent_name));
1916
-
2071
+
1917
2072
  // Send agent.create command via WebSocket
1918
2073
  if let Some(ws) = ws_client {
1919
2074
  match ws.send_command("agent.create", Some(agent_data)) {
@@ -1933,21 +2088,21 @@ impl App {
1933
2088
  self.add_log("[HELP] Start the backend server and reconnect.".to_string());
1934
2089
  return;
1935
2090
  }
1936
-
2091
+
1937
2092
  // Close the builder
1938
2093
  self.pop_overlay();
1939
-
2094
+
1940
2095
  // Reset wizard state
1941
2096
  self.state.agent_builder = AgentBuilderState::default();
1942
-
2097
+
1943
2098
  // Suppress unused variable warning
1944
2099
  let _ = agent_data;
1945
2100
  }
1946
-
2101
+
1947
2102
  // ============================================================
1948
2103
  // RUN MANAGER INPUT HANDLING (Step 4.7)
1949
2104
  // ============================================================
1950
-
2105
+
1951
2106
  /// Handle input when Run Manager screen is active
1952
2107
  fn handle_run_manager_input(
1953
2108
  &mut self,
@@ -1955,7 +2110,7 @@ impl App {
1955
2110
  ws_client: Option<&WebSocketClient>,
1956
2111
  ) -> anyhow::Result<bool> {
1957
2112
  use crossterm::event::KeyModifiers;
1958
-
2113
+
1959
2114
  // Ctrl+C/Q to exit
1960
2115
  if key.modifiers.contains(KeyModifiers::CONTROL) {
1961
2116
  match key.code {
@@ -1963,7 +2118,7 @@ impl App {
1963
2118
  _ => return Ok(false),
1964
2119
  }
1965
2120
  }
1966
-
2121
+
1967
2122
  match key.code {
1968
2123
  // Navigation
1969
2124
  KeyCode::Up => {
@@ -1974,19 +2129,19 @@ impl App {
1974
2129
  self.state.run_manager.select_next();
1975
2130
  self.request_render("run_manager_down");
1976
2131
  }
1977
-
2132
+
1978
2133
  // Filter
1979
2134
  KeyCode::Char('f') | KeyCode::Char('F') => {
1980
2135
  self.state.run_manager.next_filter();
1981
2136
  self.request_render("run_manager_filter");
1982
2137
  }
1983
-
2138
+
1984
2139
  // Sort
1985
2140
  KeyCode::Char('s') | KeyCode::Char('S') => {
1986
2141
  self.state.run_manager.next_sort();
1987
2142
  self.request_render("run_manager_sort");
1988
2143
  }
1989
-
2144
+
1990
2145
  // Refresh
1991
2146
  KeyCode::Char('r') | KeyCode::Char('R') => {
1992
2147
  if self.state.operation_mode == OperationMode::Connected {
@@ -2005,7 +2160,7 @@ impl App {
2005
2160
  }
2006
2161
  self.request_render("run_manager_refresh");
2007
2162
  }
2008
-
2163
+
2009
2164
  // View details / Close detail view
2010
2165
  KeyCode::Enter => {
2011
2166
  if self.state.run_manager.is_detail_view() {
@@ -2033,11 +2188,15 @@ impl App {
2033
2188
  }
2034
2189
  self.request_render("run_manager_view");
2035
2190
  }
2036
-
2191
+
2037
2192
  // Cancel run
2038
2193
  KeyCode::Char('c') | KeyCode::Char('C') => {
2039
2194
  if let Some(run) = self.state.run_manager.selected_run() {
2040
- if matches!(run.status, crate::ui::run_manager::RunStatus::Running | crate::ui::run_manager::RunStatus::Pending) {
2195
+ if matches!(
2196
+ run.status,
2197
+ crate::ui::run_manager::RunStatus::Running
2198
+ | crate::ui::run_manager::RunStatus::Pending
2199
+ ) {
2041
2200
  if self.state.operation_mode == OperationMode::Connected {
2042
2201
  if let Some(ws) = ws_client {
2043
2202
  let data = serde_json::json!({ "runId": run.id.clone() });
@@ -2057,13 +2216,13 @@ impl App {
2057
2216
  }
2058
2217
  self.request_render("run_manager_cancel");
2059
2218
  }
2060
-
2219
+
2061
2220
  // Delete run
2062
2221
  KeyCode::Char('d') | KeyCode::Char('D') => {
2063
2222
  self.add_log("[RUN] Delete run is not supported by the Gateway API (use cancel for active runs).".to_string());
2064
2223
  self.request_render("run_manager_delete");
2065
2224
  }
2066
-
2225
+
2067
2226
  // Close detail view or Run Manager
2068
2227
  KeyCode::Esc => {
2069
2228
  if self.state.run_manager.is_detail_view() {
@@ -2076,21 +2235,21 @@ impl App {
2076
2235
  self.pop_overlay();
2077
2236
  }
2078
2237
  }
2079
-
2238
+
2080
2239
  _ => {}
2081
2240
  }
2082
-
2241
+
2083
2242
  Ok(false)
2084
2243
  }
2085
-
2244
+
2086
2245
  // ============================================================
2087
2246
  // SETTINGS INPUT HANDLING (Step 4.8)
2088
2247
  // ============================================================
2089
-
2248
+
2090
2249
  /// Handle input when Settings screen is active
2091
2250
  fn handle_settings_input(&mut self, key: KeyEvent) -> anyhow::Result<bool> {
2092
2251
  use crossterm::event::KeyModifiers;
2093
-
2252
+
2094
2253
  // Ctrl+C/Q to exit
2095
2254
  if key.modifiers.contains(KeyModifiers::CONTROL) {
2096
2255
  match key.code {
@@ -2098,7 +2257,7 @@ impl App {
2098
2257
  _ => return Ok(false),
2099
2258
  }
2100
2259
  }
2101
-
2260
+
2102
2261
  match key.code {
2103
2262
  // Navigation
2104
2263
  KeyCode::Up => {
@@ -2109,27 +2268,36 @@ impl App {
2109
2268
  self.state.settings.next_section();
2110
2269
  self.request_render("settings_down");
2111
2270
  }
2112
-
2271
+
2113
2272
  // Toggle current setting
2114
2273
  KeyCode::Char(' ') => {
2115
2274
  match self.state.settings.focused_section {
2116
2275
  crate::ui::settings::SettingsSection::Mode => {
2117
2276
  self.state.settings.toggle_mode();
2118
- self.add_log(format!("[SETTINGS] Mode: {}", self.state.settings.mode.as_str()));
2277
+ self.add_log(format!(
2278
+ "[SETTINGS] Mode: {}",
2279
+ self.state.settings.mode.as_str()
2280
+ ));
2119
2281
  }
2120
2282
  crate::ui::settings::SettingsSection::AIProvider => {
2121
2283
  self.state.settings.next_provider();
2122
- self.add_log(format!("[SETTINGS] Provider: {}", self.state.settings.ai_provider));
2284
+ self.add_log(format!(
2285
+ "[SETTINGS] Provider: {}",
2286
+ self.state.settings.ai_provider
2287
+ ));
2123
2288
  }
2124
2289
  crate::ui::settings::SettingsSection::AutoUpdate => {
2125
2290
  self.state.settings.toggle_auto_update();
2126
- self.add_log(format!("[SETTINGS] Auto-update: {}", self.state.settings.auto_update));
2291
+ self.add_log(format!(
2292
+ "[SETTINGS] Auto-update: {}",
2293
+ self.state.settings.auto_update
2294
+ ));
2127
2295
  }
2128
2296
  _ => {}
2129
2297
  }
2130
2298
  self.request_render("settings_toggle");
2131
2299
  }
2132
-
2300
+
2133
2301
  // Save settings
2134
2302
  KeyCode::Enter => {
2135
2303
  if self.state.settings.has_changes {
@@ -2139,7 +2307,7 @@ impl App {
2139
2307
  self.request_render("settings_save");
2140
2308
  }
2141
2309
  }
2142
-
2310
+
2143
2311
  // Close (with discard warning if changes)
2144
2312
  KeyCode::Esc => {
2145
2313
  if self.state.settings.has_changes {
@@ -2148,13 +2316,13 @@ impl App {
2148
2316
  }
2149
2317
  self.pop_overlay();
2150
2318
  }
2151
-
2319
+
2152
2320
  _ => {}
2153
2321
  }
2154
-
2322
+
2155
2323
  Ok(false)
2156
2324
  }
2157
-
2325
+
2158
2326
  /// Handle input when Agent List screen is active
2159
2327
  fn handle_agent_list_input(
2160
2328
  &mut self,
@@ -2162,7 +2330,7 @@ impl App {
2162
2330
  ws_client: Option<&WebSocketClient>,
2163
2331
  ) -> anyhow::Result<bool> {
2164
2332
  use crossterm::event::KeyModifiers;
2165
-
2333
+
2166
2334
  // Ctrl+C/Q to exit
2167
2335
  if key.modifiers.contains(KeyModifiers::CONTROL) {
2168
2336
  match key.code {
@@ -2170,7 +2338,7 @@ impl App {
2170
2338
  _ => return Ok(false),
2171
2339
  }
2172
2340
  }
2173
-
2341
+
2174
2342
  match key.code {
2175
2343
  // Navigation
2176
2344
  KeyCode::Up => {
@@ -2186,7 +2354,7 @@ impl App {
2186
2354
  }
2187
2355
  self.request_render("agent_list_down");
2188
2356
  }
2189
-
2357
+
2190
2358
  // View details / Close detail view
2191
2359
  KeyCode::Enter => {
2192
2360
  if self.state.agent_list.detail_view.is_some() {
@@ -2198,7 +2366,7 @@ impl App {
2198
2366
  }
2199
2367
  self.request_render("agent_list_toggle_detail");
2200
2368
  }
2201
-
2369
+
2202
2370
  // Close
2203
2371
  KeyCode::Esc => {
2204
2372
  if self.state.agent_list.detail_view.is_some() {
@@ -2211,24 +2379,30 @@ impl App {
2211
2379
  self.request_render("agent_list_close");
2212
2380
  }
2213
2381
  }
2214
-
2382
+
2215
2383
  // Delete agent (Step 5.5)
2216
2384
  KeyCode::Delete | KeyCode::Char('d') | KeyCode::Char('D') => {
2217
2385
  // Only allow deletion in list view (not in detail view)
2218
- if self.state.agent_list.detail_view.is_none() && !self.state.agent_list.agents.is_empty() {
2386
+ if self.state.agent_list.detail_view.is_none()
2387
+ && !self.state.agent_list.agents.is_empty()
2388
+ {
2219
2389
  self.state.agent_delete_requested = true;
2220
2390
  self.request_render("agent_delete_requested");
2221
2391
  }
2222
2392
  }
2223
-
2393
+
2224
2394
  // Step 6: queue a Gateway run (uses built-in `test` agent executor on the server)
2225
2395
  KeyCode::Char('x') | KeyCode::Char('X') => {
2226
2396
  if self.state.operation_mode != OperationMode::Connected {
2227
- self.add_log("[AGENT] Connect to Gateway first to run on the server (key x).".to_string());
2397
+ self.add_log(
2398
+ "[AGENT] Connect to Gateway first to run on the server (key x)."
2399
+ .to_string(),
2400
+ );
2228
2401
  } else if self.state.agent_list.agents.is_empty() {
2229
2402
  self.add_log("[AGENT] No agents in list.".to_string());
2230
- } else if let Some((agent_name, agent_model)) =
2231
- self.get_selected_agent().map(|a| (a.name.clone(), a.model.clone()))
2403
+ } else if let Some((agent_name, agent_model)) = self
2404
+ .get_selected_agent()
2405
+ .map(|a| (a.name.clone(), a.model.clone()))
2232
2406
  {
2233
2407
  if let Some(ws) = ws_client {
2234
2408
  let name = format!("TUI: {}", agent_name);
@@ -2242,7 +2416,10 @@ impl App {
2242
2416
  match ws.send_command("run.quick", Some(data)) {
2243
2417
  Ok(id) => {
2244
2418
  self.state.pending_run_quick_id = Some(id);
2245
- self.add_log(format!("[AGENT] Queued Gateway run for profile '{}' (x)", agent_name));
2419
+ self.add_log(format!(
2420
+ "[AGENT] Queued Gateway run for profile '{}' (x)",
2421
+ agent_name
2422
+ ));
2246
2423
  }
2247
2424
  Err(e) => self.add_log(format!("[ERROR] run.quick: {}", e)),
2248
2425
  }
@@ -2250,31 +2427,36 @@ impl App {
2250
2427
  }
2251
2428
  self.request_render("agent_list_quick_run");
2252
2429
  }
2253
-
2430
+
2254
2431
  _ => {}
2255
2432
  }
2256
-
2433
+
2257
2434
  Ok(false)
2258
2435
  }
2259
-
2436
+
2260
2437
  /// Delete selected agent (Step 5.5)
2261
2438
  pub fn delete_selected_agent(&mut self, ws_client: &Option<crate::websocket::WebSocketClient>) {
2262
2439
  if self.state.agent_list.agents.is_empty() {
2263
2440
  return;
2264
2441
  }
2265
-
2266
- let agent_name = self.state.agent_list.agents[self.state.agent_list.selected_index].name.clone();
2267
-
2442
+
2443
+ let agent_name = self.state.agent_list.agents[self.state.agent_list.selected_index]
2444
+ .name
2445
+ .clone();
2446
+
2268
2447
  // Send agent.delete command via WebSocket
2269
2448
  if let Some(ws) = ws_client {
2270
2449
  let delete_data = serde_json::json!({
2271
2450
  "name": agent_name
2272
2451
  });
2273
-
2452
+
2274
2453
  match ws.send_command("agent.delete", Some(delete_data)) {
2275
2454
  Ok(id) => {
2276
2455
  let short_id = &id[id.len().saturating_sub(8)..];
2277
- self.add_log(format!("[AGENT] Sent agent.delete for '{}' (id: {})", agent_name, short_id));
2456
+ self.add_log(format!(
2457
+ "[AGENT] Sent agent.delete for '{}' (id: {})",
2458
+ agent_name, short_id
2459
+ ));
2278
2460
  // Store command ID for response tracking
2279
2461
  self.state.pending_agent_delete_id = Some(id);
2280
2462
  }
@@ -2286,24 +2468,31 @@ impl App {
2286
2468
  self.add_log("[ERROR] WebSocket not connected. Cannot delete agent.".to_string());
2287
2469
  }
2288
2470
  }
2289
-
2471
+
2290
2472
  /// Get selected agent (Step 5.6)
2291
2473
  pub fn get_selected_agent(&self) -> Option<&AgentInfo> {
2292
2474
  if self.state.agent_list.agents.is_empty() {
2293
2475
  return None;
2294
2476
  }
2295
- self.state.agent_list.agents.get(self.state.agent_list.selected_index)
2477
+ self.state
2478
+ .agent_list
2479
+ .agents
2480
+ .get(self.state.agent_list.selected_index)
2296
2481
  }
2297
-
2482
+
2298
2483
  /// Get selected agent name (Step 5.6)
2299
2484
  pub fn get_selected_agent_name(&self) -> Option<String> {
2300
2485
  self.get_selected_agent().map(|agent| agent.name.clone())
2301
2486
  }
2302
-
2487
+
2303
2488
  /// Handle input when Connection Portal screen is active (Step 7)
2304
- fn handle_connection_portal_input(&mut self, key: KeyEvent, ws_client: Option<&WebSocketClient>) -> anyhow::Result<bool> {
2489
+ fn handle_connection_portal_input(
2490
+ &mut self,
2491
+ key: KeyEvent,
2492
+ ws_client: Option<&WebSocketClient>,
2493
+ ) -> anyhow::Result<bool> {
2305
2494
  use crossterm::event::KeyModifiers;
2306
-
2495
+
2307
2496
  // Ctrl+C/Q to exit
2308
2497
  if key.modifiers.contains(KeyModifiers::CONTROL) {
2309
2498
  match key.code {
@@ -2311,12 +2500,12 @@ impl App {
2311
2500
  _ => return Ok(false),
2312
2501
  }
2313
2502
  }
2314
-
2503
+
2315
2504
  // Disable input if connecting
2316
2505
  if self.state.connection_portal.connecting {
2317
2506
  return Ok(false);
2318
2507
  }
2319
-
2508
+
2320
2509
  match key.code {
2321
2510
  // ESC - Close portal and return to Main
2322
2511
  KeyCode::Esc => {
@@ -2346,7 +2535,7 @@ impl App {
2346
2535
  }
2347
2536
  self.request_immediate_render("open_portal_monitoring");
2348
2537
  }
2349
-
2538
+
2350
2539
  // L - Continue in local-only mode (no Gateway connection)
2351
2540
  KeyCode::Char('l') | KeyCode::Char('L') => {
2352
2541
  use crate::app::OperationMode;
@@ -2371,7 +2560,7 @@ impl App {
2371
2560
  self.add_log("[MODE] Continuing in LOCAL mode (no Gateway connection)".to_string());
2372
2561
  self.request_immediate_render("portal_local_mode");
2373
2562
  }
2374
-
2563
+
2375
2564
  // Tab - Switch between URL and Username fields
2376
2565
  KeyCode::Tab | KeyCode::BackTab => {
2377
2566
  if self.state.connection_portal.connection_success {
@@ -2380,7 +2569,7 @@ impl App {
2380
2569
  self.state.connection_portal.toggle_field();
2381
2570
  self.request_immediate_render("portal_field_toggle");
2382
2571
  }
2383
-
2572
+
2384
2573
  // Up/Down - Navigate between fields
2385
2574
  KeyCode::Up | KeyCode::Down => {
2386
2575
  if self.state.connection_portal.connection_success {
@@ -2389,7 +2578,7 @@ impl App {
2389
2578
  self.state.connection_portal.toggle_field();
2390
2579
  self.request_immediate_render("portal_navigate");
2391
2580
  }
2392
-
2581
+
2393
2582
  // Enter - Connect to Gateway
2394
2583
  KeyCode::Enter => {
2395
2584
  let raw = self.state.connection_portal.gateway_url.clone();
@@ -2403,49 +2592,56 @@ impl App {
2403
2592
 
2404
2593
  // Validate URL
2405
2594
  if url.is_empty() {
2406
- self.state.connection_portal.error = Some("Gateway URL is required".to_string());
2595
+ self.state.connection_portal.error =
2596
+ Some("Gateway URL is required".to_string());
2407
2597
  self.request_immediate_render("portal_error");
2408
2598
  return Ok(false);
2409
2599
  }
2410
-
2600
+
2411
2601
  if !url.starts_with("http://") && !url.starts_with("https://") {
2412
- self.state.connection_portal.error = Some("URL must start with http:// or https://".to_string());
2602
+ self.state.connection_portal.error =
2603
+ Some("URL must start with http:// or https://".to_string());
2413
2604
  self.request_immediate_render("portal_error");
2414
2605
  return Ok(false);
2415
2606
  }
2416
-
2607
+
2417
2608
  // Connect to Gateway
2418
2609
  self.state.connection_portal.error = None;
2419
2610
  self.state.connection_portal.begin_connecting();
2420
-
2611
+
2421
2612
  if let Some(ws) = ws_client {
2422
2613
  let connect_data = serde_json::json!({
2423
2614
  "url": url,
2424
2615
  "username": self.state.connection_portal.username
2425
2616
  });
2426
-
2617
+
2427
2618
  match ws.send_command("gateway.connect", Some(connect_data)) {
2428
2619
  Ok(id) => {
2429
2620
  let short_id = &id[id.len().saturating_sub(8)..];
2430
- self.add_log(format!("[GATEWAY] Connecting to {} (id: {})", url, short_id));
2621
+ self.add_log(format!(
2622
+ "[GATEWAY] Connecting to {} (id: {})",
2623
+ url, short_id
2624
+ ));
2431
2625
  self.state.pending_gateway_connect_id = Some(id);
2432
2626
  }
2433
2627
  Err(e) => {
2434
2628
  self.state.connection_portal.finish_connecting();
2435
- self.state.connection_portal.error = Some(format!("Failed to connect: {}", e));
2629
+ self.state.connection_portal.error =
2630
+ Some(format!("Failed to connect: {}", e));
2436
2631
  self.add_log(format!("[ERROR] Gateway connection failed: {}", e));
2437
2632
  }
2438
2633
  }
2439
2634
  } else {
2440
2635
  self.state.connection_portal.finish_connecting();
2441
- self.state.connection_portal.error = Some("WebSocket not connected".to_string());
2636
+ self.state.connection_portal.error =
2637
+ Some("WebSocket not connected".to_string());
2442
2638
  self.add_log("[ERROR] WebSocket not connected".to_string());
2443
2639
  }
2444
-
2640
+
2445
2641
  // CRITICAL: Use immediate render for instant status update
2446
2642
  self.request_immediate_render("portal_connect");
2447
2643
  }
2448
-
2644
+
2449
2645
  // Typing - edit focused field (immediate render for responsiveness)
2450
2646
  KeyCode::Char(c) => {
2451
2647
  if self.state.connection_portal.connection_success {
@@ -2467,7 +2663,7 @@ impl App {
2467
2663
  }
2468
2664
  }
2469
2665
  }
2470
-
2666
+
2471
2667
  // Backspace - delete character (immediate render for responsiveness)
2472
2668
  KeyCode::Backspace => {
2473
2669
  if self.state.connection_portal.connection_success {
@@ -2485,10 +2681,10 @@ impl App {
2485
2681
  // OPTIMIZATION: Immediate render for instant feedback
2486
2682
  self.request_immediate_render("portal_delete");
2487
2683
  }
2488
-
2684
+
2489
2685
  _ => {}
2490
2686
  }
2491
-
2687
+
2492
2688
  Ok(false)
2493
2689
  }
2494
2690
 
@@ -2497,7 +2693,7 @@ impl App {
2497
2693
  key: KeyEvent,
2498
2694
  ws_client: Option<&WebSocketClient>,
2499
2695
  ) -> anyhow::Result<bool> {
2500
- use crate::ui::portal_monitoring::{portal_monitoring_scroll_max};
2696
+ use crate::ui::portal_monitoring::portal_monitoring_scroll_max;
2501
2697
  use crossterm::event::KeyModifiers;
2502
2698
  if key.modifiers.contains(KeyModifiers::CONTROL) {
2503
2699
  match key.code {
@@ -2508,6 +2704,9 @@ impl App {
2508
2704
  if key.code == KeyCode::F(10) {
2509
2705
  return Ok(true);
2510
2706
  }
2707
+ if key.kind != KeyEventKind::Press {
2708
+ return Ok(false);
2709
+ }
2511
2710
  if self.state.portal_monitoring.help_visible {
2512
2711
  if key.code == KeyCode::Esc || key.code == KeyCode::Char('?') {
2513
2712
  self.state.portal_monitoring.help_visible = false;
@@ -2540,13 +2739,18 @@ impl App {
2540
2739
  if key.code == KeyCode::Char('r') || key.code == KeyCode::Char('R') {
2541
2740
  if let Some(ws) = ws_client {
2542
2741
  let sections = MonitoringSection::all();
2543
- let idx = self.state.advanced_monitoring.monitoring_state.selected_index;
2742
+ let idx = self
2743
+ .state
2744
+ .advanced_monitoring
2745
+ .monitoring_state
2746
+ .selected_index;
2544
2747
  let current = sections.get(idx).copied();
2545
2748
  self.state.portal_observability_last_poll = None;
2546
2749
  self.state.pending_gateway_observability_id = None;
2547
2750
  self.state.pending_monitoring_refresh_id = None;
2548
2751
  self.state.pending_monitoring_logs_id = None;
2549
2752
  self.state.pending_monitoring_drill_id = None;
2753
+ self.state.portal_monitoring.loading = false;
2550
2754
  self.state.portal_monitoring.section_refresh_loading = None;
2551
2755
  self.state.portal_monitoring.logs_fetch_loading = false;
2552
2756
  self.state.portal_monitoring.metrics_drill = None;
@@ -2614,7 +2818,11 @@ impl App {
2614
2818
  // Phase 3: extended dependency view (GET /api/monitoring/dependencies/detail)
2615
2819
  if key.code == KeyCode::Char('t') || key.code == KeyCode::Char('T') {
2616
2820
  let sections = MonitoringSection::all();
2617
- let idx = self.state.advanced_monitoring.monitoring_state.selected_index;
2821
+ let idx = self
2822
+ .state
2823
+ .advanced_monitoring
2824
+ .monitoring_state
2825
+ .selected_index;
2618
2826
  let current = sections.get(idx).copied();
2619
2827
  let expanded = current
2620
2828
  .and_then(|sec| {
@@ -2642,7 +2850,11 @@ impl App {
2642
2850
  // Phase 4: diagnostics for the local CLI/TUI bridge + Gateway reachability.
2643
2851
  if key.code == KeyCode::Char('s') || key.code == KeyCode::Char('S') {
2644
2852
  let sections = MonitoringSection::all();
2645
- let idx = self.state.advanced_monitoring.monitoring_state.selected_index;
2853
+ let idx = self
2854
+ .state
2855
+ .advanced_monitoring
2856
+ .monitoring_state
2857
+ .selected_index;
2646
2858
  let current = sections.get(idx).copied();
2647
2859
  let expanded = current
2648
2860
  .and_then(|sec| {
@@ -2669,7 +2881,11 @@ impl App {
2669
2881
 
2670
2882
  if key.code == KeyCode::Right {
2671
2883
  let sections = MonitoringSection::all();
2672
- let idx = self.state.advanced_monitoring.monitoring_state.selected_index;
2884
+ let idx = self
2885
+ .state
2886
+ .advanced_monitoring
2887
+ .monitoring_state
2888
+ .selected_index;
2673
2889
  let current = sections.get(idx).copied();
2674
2890
  let expanded = current
2675
2891
  .and_then(|sec| {
@@ -2712,12 +2928,10 @@ impl App {
2712
2928
 
2713
2929
  // Toggle auto-refresh with 'A' key
2714
2930
  if key.code == KeyCode::Char('a') || key.code == KeyCode::Char('A') {
2715
- self.state.portal_monitoring.auto_refresh_enabled =
2931
+ self.state.portal_monitoring.auto_refresh_enabled =
2716
2932
  !self.state.portal_monitoring.auto_refresh_enabled;
2717
- self.state
2718
- .advanced_monitoring
2719
- .monitoring_state
2720
- .live_mode = self.state.portal_monitoring.auto_refresh_enabled;
2933
+ self.state.advanced_monitoring.monitoring_state.live_mode =
2934
+ self.state.portal_monitoring.auto_refresh_enabled;
2721
2935
  let status = if self.state.portal_monitoring.auto_refresh_enabled {
2722
2936
  "enabled"
2723
2937
  } else {
@@ -2730,7 +2944,11 @@ impl App {
2730
2944
 
2731
2945
  // Phase 1: Enter / Space — expand or collapse selected section
2732
2946
  if key.code == KeyCode::Enter || key.code == KeyCode::Char(' ') {
2733
- let idx = self.state.advanced_monitoring.monitoring_state.selected_index;
2947
+ let idx = self
2948
+ .state
2949
+ .advanced_monitoring
2950
+ .monitoring_state
2951
+ .selected_index;
2734
2952
  self.state
2735
2953
  .advanced_monitoring
2736
2954
  .monitoring_state
@@ -2820,7 +3038,8 @@ impl App {
2820
3038
  self.state.connection_portal.add_log_entry(
2821
3039
  now,
2822
3040
  LogLevel::Info,
2823
- "Type your Gateway URL (https://…), then press Enter to connect.".to_string()
3041
+ "Type your Gateway URL (https://…), then press Enter to connect."
3042
+ .to_string(),
2824
3043
  );
2825
3044
  }
2826
3045
  _ => {}
@@ -2830,7 +3049,7 @@ impl App {
2830
3049
  self.state.connection_portal.add_log_entry(
2831
3050
  now,
2832
3051
  LogLevel::Info,
2833
- "Type your Gateway URL, then press Enter to connect.".to_string()
3052
+ "Type your Gateway URL, then press Enter to connect.".to_string(),
2834
3053
  );
2835
3054
  }
2836
3055
 
@@ -2838,12 +3057,16 @@ impl App {
2838
3057
  .navigation
2839
3058
  .navigate_to_base(Screen::ConnectionPortal);
2840
3059
  }
2841
-
3060
+
2842
3061
  // ============================================================
2843
3062
  // SETUP PORTAL INPUT HANDLING
2844
3063
  // ============================================================
2845
-
2846
- fn handle_setup_portal_input(&mut self, key: KeyEvent, _ws_client: Option<&WebSocketClient>) -> anyhow::Result<bool> {
3064
+
3065
+ fn handle_setup_portal_input(
3066
+ &mut self,
3067
+ key: KeyEvent,
3068
+ _ws_client: Option<&WebSocketClient>,
3069
+ ) -> anyhow::Result<bool> {
2847
3070
  // ESC always closes the portal (even when detecting), so user can always get out
2848
3071
  if key.code == KeyCode::Esc {
2849
3072
  self.state.setup_portal.detecting = false;
@@ -2873,18 +3096,18 @@ impl App {
2873
3096
  self.add_log("[NAV] Opening Connection Portal (F2)...".to_string());
2874
3097
  self.request_immediate_render("open_connection_portal");
2875
3098
  }
2876
-
3099
+
2877
3100
  // Up/Down - Navigate options (immediate render so windowed terminals always redraw)
2878
3101
  KeyCode::Up => {
2879
3102
  self.state.setup_portal.cycle_option(-1);
2880
3103
  self.request_immediate_render("setup_nav_up");
2881
3104
  }
2882
-
3105
+
2883
3106
  KeyCode::Down => {
2884
3107
  self.state.setup_portal.cycle_option(1);
2885
3108
  self.request_immediate_render("setup_nav_down");
2886
3109
  }
2887
-
3110
+
2888
3111
  // Enter - Connection Portal with pre-filled URL and fresh portal state
2889
3112
  KeyCode::Enter => {
2890
3113
  self.state.setup_portal.error = None;
@@ -2896,11 +3119,10 @@ impl App {
2896
3119
  ));
2897
3120
  self.request_immediate_render("setup_enter");
2898
3121
  }
2899
-
3122
+
2900
3123
  _ => {}
2901
3124
  }
2902
-
3125
+
2903
3126
  Ok(false)
2904
3127
  }
2905
3128
  }
2906
-