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.
- package/mk3-tui/src/app/render_scheduler.rs +111 -112
- package/mk3-tui/src/app.rs +526 -304
- package/mk3-tui/src/debug_log.rs +131 -124
- package/mk3-tui/src/io/mod.rs +63 -66
- package/mk3-tui/src/io/protocol.rs +14 -15
- package/mk3-tui/src/io/stdio.rs +31 -32
- package/mk3-tui/src/io/ws.rs +25 -32
- package/mk3-tui/src/main.rs +593 -279
- package/mk3-tui/src/screens/mod.rs +53 -39
- package/mk3-tui/src/storage/cache.rs +221 -224
- package/mk3-tui/src/storage/mod.rs +5 -6
- package/mk3-tui/src/ui/agent_builder.rs +1148 -922
- package/mk3-tui/src/ui/agent_list.rs +344 -295
- package/mk3-tui/src/ui/boot.rs +145 -148
- package/mk3-tui/src/ui/connection_portal.rs +121 -98
- package/mk3-tui/src/ui/help.rs +340 -284
- package/mk3-tui/src/ui/layout.rs +966 -803
- package/mk3-tui/src/ui/mod.rs +1 -1
- package/mk3-tui/src/ui/portal_monitoring.rs +58 -50
- package/mk3-tui/src/ui/run_manager.rs +784 -764
- package/mk3-tui/src/ui/safe_viewport.rs +236 -235
- package/mk3-tui/src/ui/settings.rs +414 -362
- package/mk3-tui/src/ui/setup_portal.rs +158 -101
- package/mk3-tui/src/websocket.rs +315 -308
- package/package.json +2 -2
package/mk3-tui/src/app.rs
CHANGED
|
@@ -48,8 +48,8 @@ pub enum AppMode {
|
|
|
48
48
|
|
|
49
49
|
#[derive(Debug, Clone, PartialEq)]
|
|
50
50
|
pub enum OperationMode {
|
|
51
|
-
Local,
|
|
52
|
-
Connected,
|
|
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,
|
|
103
|
-
LocalBundle,
|
|
104
|
-
CustomUrl,
|
|
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,
|
|
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
|
|
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,
|
|
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>,
|
|
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>,
|
|
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,
|
|
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,
|
|
631
|
+
(CustomUrl, 1) => LocalBundle, // Wrap to top
|
|
630
632
|
// Backward (Up key, Scroll Up)
|
|
631
|
-
(LocalBundle, -1) => CustomUrl,
|
|
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>,
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
self.state.logs.push_back(
|
|
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(
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
self.state.logs.push_back(
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
self.state
|
|
1189
|
-
|
|
1190
|
-
|
|
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(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
self.state
|
|
1198
|
-
|
|
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(
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
self.state.logs.push_back(
|
|
1211
|
-
|
|
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(
|
|
1216
|
-
|
|
1217
|
-
|
|
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>()
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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!(
|
|
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!(
|
|
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
|
|
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
|
|
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!(
|
|
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
|
|
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
|
|
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(
|
|
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!(
|
|
1485
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
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(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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()
|
|
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(
|
|
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
|
-
|
|
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!(
|
|
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]
|
|
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!(
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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!(
|
|
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 =
|
|
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 =
|
|
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::
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
.
|
|
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
|
|
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."
|
|
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(
|
|
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
|
-
|