4runr-os 2.10.39 → 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/apps/gateway/dist/apps/gateway/src/index.js +14 -4
- package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
- package/apps/gateway/package-lock.json +204 -353
- package/apps/gateway/src/index.ts +27 -8
- package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
- package/apps/gateway/src/middleware/log-capture.ts +70 -0
- package/apps/gateway/src/routes/monitoring.ts +298 -0
- package/dist/gateway-client.d.ts +2 -0
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +22 -0
- package/dist/gateway-client.js.map +1 -1
- package/dist/tui-handlers.js +498 -0
- package/dist/tui-handlers.js.map +1 -1
- package/mk3-tui/src/app/render_scheduler.rs +111 -112
- package/mk3-tui/src/app.rs +1078 -295
- 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 +774 -212
- package/mk3-tui/src/monitoring/mod.rs +428 -0
- 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 +1027 -147
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use crate::io::IoHandler;
|
|
2
|
+
use crate::monitoring::{MetricsDrillPanel, MonitoringSection, MonitoringState};
|
|
2
3
|
use crate::screens::{NavigationState, Screen};
|
|
3
4
|
use crate::storage::Cache;
|
|
4
5
|
use crate::ui::agent_builder::AgentBuilderState;
|
|
@@ -8,7 +9,7 @@ use crate::websocket::WebSocketClient;
|
|
|
8
9
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
|
9
10
|
use ratatui::prelude::*;
|
|
10
11
|
use serde::{Deserialize, Serialize};
|
|
11
|
-
use std::collections::VecDeque;
|
|
12
|
+
use std::collections::{HashMap, VecDeque};
|
|
12
13
|
use std::time::{Duration, Instant};
|
|
13
14
|
|
|
14
15
|
/// `HH:MM:SS` for Connection Portal activity log (UTC clock; avoids pulling in `chrono`).
|
|
@@ -47,8 +48,8 @@ pub enum AppMode {
|
|
|
47
48
|
|
|
48
49
|
#[derive(Debug, Clone, PartialEq)]
|
|
49
50
|
pub enum OperationMode {
|
|
50
|
-
Local,
|
|
51
|
-
Connected,
|
|
51
|
+
Local, // No Gateway connection
|
|
52
|
+
Connected, // Connected to Gateway
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
impl OperationMode {
|
|
@@ -60,6 +61,36 @@ impl OperationMode {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
#[cfg(test)]
|
|
65
|
+
mod portal_monitoring_tests {
|
|
66
|
+
use super::*;
|
|
67
|
+
|
|
68
|
+
#[test]
|
|
69
|
+
fn metric_history_totals_contract_keys() {
|
|
70
|
+
let totals = serde_json::json!({
|
|
71
|
+
"httpRequests": 10,
|
|
72
|
+
"httpRequestErrors": 1,
|
|
73
|
+
"queueJobsWaiting": 2,
|
|
74
|
+
"queueJobsActive": 3,
|
|
75
|
+
"queueJobsFailed": 4,
|
|
76
|
+
"runsActive": 5,
|
|
77
|
+
"sseActive": 6
|
|
78
|
+
});
|
|
79
|
+
let entry = metric_history_entry_from_totals(
|
|
80
|
+
"2026-01-01T00:00:00.000Z".to_string(),
|
|
81
|
+
totals.as_object().expect("totals object"),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
assert_eq!(entry.http_requests, 10);
|
|
85
|
+
assert_eq!(entry.http_errors, 1);
|
|
86
|
+
assert_eq!(entry.queue_waiting, 2);
|
|
87
|
+
assert_eq!(entry.queue_active, 3);
|
|
88
|
+
assert_eq!(entry.queue_failed, 4);
|
|
89
|
+
assert_eq!(entry.runs_active, 5);
|
|
90
|
+
assert_eq!(entry.sse_active, 6);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
#[derive(Debug, Clone, PartialEq)]
|
|
64
95
|
pub enum PortalField {
|
|
65
96
|
GatewayUrl,
|
|
@@ -68,9 +99,9 @@ pub enum PortalField {
|
|
|
68
99
|
|
|
69
100
|
#[derive(Debug, Clone, PartialEq)]
|
|
70
101
|
pub enum GatewayType {
|
|
71
|
-
FourRunrServer,
|
|
72
|
-
LocalBundle,
|
|
73
|
-
CustomUrl,
|
|
102
|
+
FourRunrServer, // External 4Runr cloud server
|
|
103
|
+
LocalBundle, // Local Gateway bundle (included with Full OS)
|
|
104
|
+
CustomUrl, // User's own Gateway server
|
|
74
105
|
}
|
|
75
106
|
|
|
76
107
|
impl GatewayType {
|
|
@@ -81,7 +112,7 @@ impl GatewayType {
|
|
|
81
112
|
GatewayType::CustomUrl => "Custom URL",
|
|
82
113
|
}
|
|
83
114
|
}
|
|
84
|
-
|
|
115
|
+
|
|
85
116
|
pub fn default_url(&self) -> &str {
|
|
86
117
|
match self {
|
|
87
118
|
GatewayType::FourRunrServer => "https://gateway.4runr.com",
|
|
@@ -94,7 +125,7 @@ impl GatewayType {
|
|
|
94
125
|
// Activity Log for Connection Portal
|
|
95
126
|
#[derive(Debug, Clone)]
|
|
96
127
|
pub struct ActivityLogEntry {
|
|
97
|
-
pub timestamp: String,
|
|
128
|
+
pub timestamp: String, // HH:MM:SS format
|
|
98
129
|
pub level: LogLevel,
|
|
99
130
|
pub message: String,
|
|
100
131
|
}
|
|
@@ -121,7 +152,6 @@ pub struct ConnectionPortalState {
|
|
|
121
152
|
pub activity_log: Vec<ActivityLogEntry>,
|
|
122
153
|
}
|
|
123
154
|
|
|
124
|
-
|
|
125
155
|
impl Default for ConnectionPortalState {
|
|
126
156
|
fn default() -> Self {
|
|
127
157
|
Self {
|
|
@@ -140,7 +170,10 @@ impl Default for ConnectionPortalState {
|
|
|
140
170
|
|
|
141
171
|
impl ConnectionPortalState {
|
|
142
172
|
pub fn reset(&mut self) {
|
|
143
|
-
self.gateway_url = self
|
|
173
|
+
self.gateway_url = self
|
|
174
|
+
.last_successful_url
|
|
175
|
+
.clone()
|
|
176
|
+
.unwrap_or_else(|| "http://localhost:3001".to_string());
|
|
144
177
|
self.username = String::new();
|
|
145
178
|
self.focused_field = PortalField::GatewayUrl;
|
|
146
179
|
self.connecting = false;
|
|
@@ -159,21 +192,21 @@ impl ConnectionPortalState {
|
|
|
159
192
|
self.connecting = false;
|
|
160
193
|
self.connecting_started = None;
|
|
161
194
|
}
|
|
162
|
-
|
|
195
|
+
|
|
163
196
|
pub fn toggle_field(&mut self) {
|
|
164
197
|
self.focused_field = match self.focused_field {
|
|
165
198
|
PortalField::GatewayUrl => PortalField::Username,
|
|
166
199
|
PortalField::Username => PortalField::GatewayUrl,
|
|
167
200
|
};
|
|
168
201
|
}
|
|
169
|
-
|
|
202
|
+
|
|
170
203
|
pub fn add_log_entry(&mut self, timestamp: String, level: LogLevel, message: String) {
|
|
171
204
|
self.activity_log.push(ActivityLogEntry {
|
|
172
205
|
timestamp,
|
|
173
206
|
level,
|
|
174
207
|
message,
|
|
175
208
|
});
|
|
176
|
-
|
|
209
|
+
|
|
177
210
|
// Keep last N lines so the portal can show a full connect trace on small terminals
|
|
178
211
|
const MAX_ACTIVITY: usize = 80;
|
|
179
212
|
if self.activity_log.len() > MAX_ACTIVITY {
|
|
@@ -183,6 +216,50 @@ impl ConnectionPortalState {
|
|
|
183
216
|
}
|
|
184
217
|
|
|
185
218
|
/// Gateway traffic snapshot (Prometheus `/metrics` via CLI WebSocket handler).
|
|
219
|
+
#[derive(Debug, Clone, Serialize)]
|
|
220
|
+
pub struct MetricHistoryEntry {
|
|
221
|
+
pub timestamp: String,
|
|
222
|
+
pub http_requests: u64,
|
|
223
|
+
pub http_errors: u64,
|
|
224
|
+
pub queue_waiting: u64,
|
|
225
|
+
pub queue_active: u64,
|
|
226
|
+
pub queue_failed: u64,
|
|
227
|
+
pub runs_active: u64,
|
|
228
|
+
pub sse_active: u64,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// Phase 5 contract with `packages/os-cli/src/gateway-observability.ts` `totals`.
|
|
232
|
+
/// If CLI keys change, update this mapping and `metric_history_totals_contract_keys`.
|
|
233
|
+
pub fn metric_history_entry_from_totals(
|
|
234
|
+
timestamp: String,
|
|
235
|
+
totals: &serde_json::Map<String, serde_json::Value>,
|
|
236
|
+
) -> MetricHistoryEntry {
|
|
237
|
+
fn read_u64(totals: &serde_json::Map<String, serde_json::Value>, key: &str) -> u64 {
|
|
238
|
+
totals
|
|
239
|
+
.get(key)
|
|
240
|
+
.and_then(|v| v.as_u64().or_else(|| v.as_f64().map(|f| f.max(0.0) as u64)))
|
|
241
|
+
.unwrap_or(0)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
MetricHistoryEntry {
|
|
245
|
+
timestamp,
|
|
246
|
+
http_requests: read_u64(totals, "httpRequests"),
|
|
247
|
+
http_errors: read_u64(totals, "httpRequestErrors"),
|
|
248
|
+
queue_waiting: read_u64(totals, "queueJobsWaiting"),
|
|
249
|
+
queue_active: read_u64(totals, "queueJobsActive"),
|
|
250
|
+
queue_failed: read_u64(totals, "queueJobsFailed"),
|
|
251
|
+
runs_active: read_u64(totals, "runsActive"),
|
|
252
|
+
sse_active: read_u64(totals, "sseActive"),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[derive(Debug, Clone, Serialize)]
|
|
257
|
+
pub struct DependencyAlertEntry {
|
|
258
|
+
pub timestamp: String,
|
|
259
|
+
pub previous: String,
|
|
260
|
+
pub current: String,
|
|
261
|
+
}
|
|
262
|
+
|
|
186
263
|
#[derive(Debug, Clone)]
|
|
187
264
|
pub struct PortalMonitoringState {
|
|
188
265
|
pub loading: bool,
|
|
@@ -190,6 +267,8 @@ pub struct PortalMonitoringState {
|
|
|
190
267
|
pub last_updated: Option<String>,
|
|
191
268
|
pub content_lines: Vec<String>,
|
|
192
269
|
pub scroll_offset: usize,
|
|
270
|
+
/// Width-based clip for section summaries; updated each render (Phase 1).
|
|
271
|
+
pub summary_clip_width: usize,
|
|
193
272
|
/// Body inner height from last render (for scroll bounds).
|
|
194
273
|
pub viewport_lines: usize,
|
|
195
274
|
/// Auto-refresh timer
|
|
@@ -200,6 +279,24 @@ pub struct PortalMonitoringState {
|
|
|
200
279
|
pub last_http_for_delta: Option<u64>,
|
|
201
280
|
/// Wall clock at last successful observability poll (for rate estimate).
|
|
202
281
|
pub last_instant_for_delta: Option<Instant>,
|
|
282
|
+
/// Phase 2: per-section lines from `monitoring.refresh` / `monitoring.logs` (merged over snapshot).
|
|
283
|
+
pub section_overrides: HashMap<MonitoringSection, Vec<String>>,
|
|
284
|
+
/// Which section is waiting on `monitoring.refresh`.
|
|
285
|
+
pub section_refresh_loading: Option<MonitoringSection>,
|
|
286
|
+
pub logs_fetch_loading: bool,
|
|
287
|
+
/// Phase 3: drill-down inside Metrics (`→` / `←`); which prom-client slice is shown.
|
|
288
|
+
pub metrics_drill: Option<MetricsDrillPanel>,
|
|
289
|
+
pub metrics_drill_lines: Vec<String>,
|
|
290
|
+
pub metrics_drill_loading: bool,
|
|
291
|
+
/// Last N HTTP request totals from full snapshots (for ASCII sparkline).
|
|
292
|
+
pub http_total_sparkline: VecDeque<u64>,
|
|
293
|
+
/// Phase 5: bounded sample history (720 samples; wall-clock span depends on refresh cadence).
|
|
294
|
+
pub metric_history: VecDeque<MetricHistoryEntry>,
|
|
295
|
+
pub dependency_alerts: VecDeque<DependencyAlertEntry>,
|
|
296
|
+
pub last_dependency_status: Option<String>,
|
|
297
|
+
pub trend_view: bool,
|
|
298
|
+
pub last_export_path: Option<String>,
|
|
299
|
+
pub help_visible: bool,
|
|
203
300
|
}
|
|
204
301
|
|
|
205
302
|
impl Default for PortalMonitoringState {
|
|
@@ -210,12 +307,54 @@ impl Default for PortalMonitoringState {
|
|
|
210
307
|
last_updated: None,
|
|
211
308
|
content_lines: Vec::new(),
|
|
212
309
|
scroll_offset: 0,
|
|
310
|
+
summary_clip_width: 80,
|
|
213
311
|
viewport_lines: 18,
|
|
214
312
|
last_refresh: None,
|
|
215
313
|
auto_refresh_interval: Duration::from_secs(5),
|
|
216
314
|
auto_refresh_enabled: true,
|
|
217
315
|
last_http_for_delta: None,
|
|
218
316
|
last_instant_for_delta: None,
|
|
317
|
+
section_overrides: HashMap::new(),
|
|
318
|
+
section_refresh_loading: None,
|
|
319
|
+
logs_fetch_loading: false,
|
|
320
|
+
metrics_drill: None,
|
|
321
|
+
metrics_drill_lines: Vec::new(),
|
|
322
|
+
metrics_drill_loading: false,
|
|
323
|
+
http_total_sparkline: VecDeque::new(),
|
|
324
|
+
metric_history: VecDeque::new(),
|
|
325
|
+
dependency_alerts: VecDeque::new(),
|
|
326
|
+
last_dependency_status: None,
|
|
327
|
+
trend_view: false,
|
|
328
|
+
last_export_path: None,
|
|
329
|
+
help_visible: false,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Section-based monitoring UI state (Phase 1).
|
|
335
|
+
#[derive(Debug, Clone)]
|
|
336
|
+
pub struct AdvancedMonitoringState {
|
|
337
|
+
pub monitoring_state: MonitoringState,
|
|
338
|
+
/// Optional mirror of the connected Gateway URL at construction time. **Live URL for UI is always
|
|
339
|
+
/// `AppState.gateway_url`** (and `MonitoringState::gateway_url` is synced from there in Portal Monitoring render).
|
|
340
|
+
#[allow(dead_code)]
|
|
341
|
+
pub gateway_url: Option<String>,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
impl Default for AdvancedMonitoringState {
|
|
345
|
+
fn default() -> Self {
|
|
346
|
+
Self {
|
|
347
|
+
monitoring_state: MonitoringState::default(),
|
|
348
|
+
gateway_url: None,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
impl AdvancedMonitoringState {
|
|
354
|
+
pub fn new(gateway_url: Option<String>) -> Self {
|
|
355
|
+
Self {
|
|
356
|
+
monitoring_state: MonitoringState::new(gateway_url.clone()),
|
|
357
|
+
gateway_url,
|
|
219
358
|
}
|
|
220
359
|
}
|
|
221
360
|
}
|
|
@@ -224,12 +363,12 @@ impl Default for PortalMonitoringState {
|
|
|
224
363
|
pub struct AppState {
|
|
225
364
|
// Navigation state (NEW - replaces simple mode)
|
|
226
365
|
pub navigation: NavigationState,
|
|
227
|
-
|
|
366
|
+
|
|
228
367
|
// App mode (DEPRECATED - keeping for backward compatibility during transition)
|
|
229
368
|
pub mode: AppMode,
|
|
230
|
-
|
|
369
|
+
|
|
231
370
|
// Boot state
|
|
232
|
-
pub boot_progress: u16,
|
|
371
|
+
pub boot_progress: u16, // 0-100
|
|
233
372
|
pub boot_lines: VecDeque<String>,
|
|
234
373
|
pub boot_done: bool,
|
|
235
374
|
pub boot_started_at: Instant,
|
|
@@ -237,7 +376,7 @@ pub struct AppState {
|
|
|
237
376
|
pub connected: bool,
|
|
238
377
|
#[allow(dead_code)]
|
|
239
378
|
pub gateway_url: Option<String>,
|
|
240
|
-
|
|
379
|
+
|
|
241
380
|
// Real component status
|
|
242
381
|
#[allow(dead_code)]
|
|
243
382
|
pub gateway_healthy: bool,
|
|
@@ -248,35 +387,35 @@ pub struct AppState {
|
|
|
248
387
|
pub shield_detectors: Vec<String>, // ["pii", "injection", "hallucination"]
|
|
249
388
|
pub sentinel_state: String, // "idle", "watching", "triggered"
|
|
250
389
|
pub sentinel_active_runs: usize,
|
|
251
|
-
|
|
390
|
+
|
|
252
391
|
// Real metrics
|
|
253
392
|
pub total_runs: u64,
|
|
254
393
|
#[allow(dead_code)]
|
|
255
394
|
pub active_sse_connections: u64,
|
|
256
395
|
#[allow(dead_code)]
|
|
257
396
|
pub idempotency_store_size: u64,
|
|
258
|
-
|
|
397
|
+
|
|
259
398
|
// System resources
|
|
260
399
|
pub cpu: f64,
|
|
261
400
|
pub mem: f64,
|
|
262
401
|
pub network_status: String,
|
|
263
|
-
|
|
402
|
+
|
|
264
403
|
// Real logs
|
|
265
404
|
pub logs: VecDeque<String>,
|
|
266
|
-
|
|
405
|
+
|
|
267
406
|
// Capabilities
|
|
268
407
|
pub capabilities: Vec<String>,
|
|
269
|
-
|
|
408
|
+
|
|
270
409
|
// UI state
|
|
271
410
|
pub command_input: String,
|
|
272
411
|
pub command_focused: bool,
|
|
273
412
|
pub log_scroll: usize,
|
|
274
|
-
|
|
413
|
+
|
|
275
414
|
// Animation state
|
|
276
415
|
pub tick: u64,
|
|
277
416
|
pub spinner_frame: usize,
|
|
278
417
|
pub uptime_secs: u64, // Updated from real clock in main loop, not from tick()
|
|
279
|
-
|
|
418
|
+
|
|
280
419
|
// Perf overlay
|
|
281
420
|
pub perf_overlay: bool,
|
|
282
421
|
pub render_count: u64,
|
|
@@ -284,7 +423,7 @@ pub struct AppState {
|
|
|
284
423
|
pub render_durations: VecDeque<u64>, // ms
|
|
285
424
|
pub render_scheduled_count: u64,
|
|
286
425
|
pub log_write_count: u64,
|
|
287
|
-
|
|
426
|
+
|
|
288
427
|
// Screen-specific state
|
|
289
428
|
pub agent_builder: AgentBuilderState,
|
|
290
429
|
pub run_manager: RunManagerState,
|
|
@@ -295,28 +434,34 @@ pub struct AppState {
|
|
|
295
434
|
pub portal_monitoring: PortalMonitoringState,
|
|
296
435
|
/// Last successful observability pull (for auto-refresh interval).
|
|
297
436
|
pub portal_observability_last_poll: Option<Instant>,
|
|
298
|
-
|
|
437
|
+
|
|
438
|
+
/// Section selection / expand state for Portal Monitoring (Phase 1).
|
|
439
|
+
pub advanced_monitoring: AdvancedMonitoringState,
|
|
440
|
+
|
|
299
441
|
// Command tracking for response handling (Step 5.1)
|
|
300
442
|
pub pending_agent_create_id: Option<String>,
|
|
301
443
|
pub pending_agent_list_id: Option<String>,
|
|
302
444
|
pub pending_agent_delete_id: Option<String>,
|
|
303
445
|
pub pending_gateway_connect_id: Option<String>,
|
|
304
|
-
pub pending_gateway_health_id: Option<String>,
|
|
446
|
+
pub pending_gateway_health_id: Option<String>, // Phase 2.1: Track health checks
|
|
305
447
|
pub pending_gateway_observability_id: Option<String>,
|
|
306
|
-
pub
|
|
307
|
-
|
|
448
|
+
pub pending_monitoring_refresh_id: Option<String>,
|
|
449
|
+
pub pending_monitoring_logs_id: Option<String>,
|
|
450
|
+
pub pending_monitoring_drill_id: Option<String>,
|
|
451
|
+
pub pending_setup_detect_id: Option<String>, // Track setup.detect command
|
|
452
|
+
|
|
308
453
|
// Step 6: Gateway runs
|
|
309
454
|
pub pending_run_list_id: Option<String>,
|
|
310
455
|
pub pending_run_get_id: Option<String>,
|
|
311
456
|
pub pending_run_cancel_id: Option<String>,
|
|
312
457
|
pub pending_run_quick_id: Option<String>,
|
|
313
|
-
|
|
458
|
+
|
|
314
459
|
// Deletion confirmation (Step 5.5)
|
|
315
460
|
pub agent_delete_requested: bool,
|
|
316
|
-
|
|
461
|
+
|
|
317
462
|
// Mode tracking (Step 7)
|
|
318
463
|
pub operation_mode: OperationMode,
|
|
319
|
-
|
|
464
|
+
|
|
320
465
|
// Local cache
|
|
321
466
|
pub cache: Option<Cache>,
|
|
322
467
|
pub cache_loaded: bool,
|
|
@@ -380,12 +525,16 @@ impl Default for AppState {
|
|
|
380
525
|
setup_portal: SetupPortalState::default(),
|
|
381
526
|
portal_monitoring: PortalMonitoringState::default(),
|
|
382
527
|
portal_observability_last_poll: None,
|
|
528
|
+
advanced_monitoring: AdvancedMonitoringState::default(),
|
|
383
529
|
pending_agent_create_id: None,
|
|
384
530
|
pending_agent_list_id: None,
|
|
385
531
|
pending_agent_delete_id: None,
|
|
386
532
|
pending_gateway_connect_id: None,
|
|
387
|
-
pending_gateway_health_id: None,
|
|
533
|
+
pending_gateway_health_id: None, // Phase 2.1: Initialize health check tracking
|
|
388
534
|
pending_gateway_observability_id: None,
|
|
535
|
+
pending_monitoring_refresh_id: None,
|
|
536
|
+
pending_monitoring_logs_id: None,
|
|
537
|
+
pending_monitoring_drill_id: None,
|
|
389
538
|
pending_setup_detect_id: None,
|
|
390
539
|
pending_run_list_id: None,
|
|
391
540
|
pending_run_get_id: None,
|
|
@@ -414,7 +563,7 @@ impl GatewayOption {
|
|
|
414
563
|
GatewayOption::CustomUrl => "Custom URL",
|
|
415
564
|
}
|
|
416
565
|
}
|
|
417
|
-
|
|
566
|
+
|
|
418
567
|
pub fn default_url(&self) -> &str {
|
|
419
568
|
match self {
|
|
420
569
|
GatewayOption::LocalBundle => "http://localhost:3001",
|
|
@@ -479,15 +628,15 @@ impl SetupPortalState {
|
|
|
479
628
|
// Forward (Down key, Scroll Down)
|
|
480
629
|
(LocalBundle, 1) => CloudServer,
|
|
481
630
|
(CloudServer, 1) => CustomUrl,
|
|
482
|
-
(CustomUrl, 1) => LocalBundle,
|
|
631
|
+
(CustomUrl, 1) => LocalBundle, // Wrap to top
|
|
483
632
|
// Backward (Up key, Scroll Up)
|
|
484
|
-
(LocalBundle, -1) => CustomUrl,
|
|
633
|
+
(LocalBundle, -1) => CustomUrl, // Wrap to bottom
|
|
485
634
|
(CloudServer, -1) => LocalBundle,
|
|
486
635
|
(CustomUrl, -1) => CloudServer,
|
|
487
636
|
_ => return, // No change for invalid direction
|
|
488
637
|
};
|
|
489
638
|
}
|
|
490
|
-
|
|
639
|
+
|
|
491
640
|
/// Clear detection state and errors (full reset; prefer [`soft_open`](Self::soft_open) when reopening Setup).
|
|
492
641
|
#[allow(dead_code)]
|
|
493
642
|
pub fn reset(&mut self) {
|
|
@@ -502,7 +651,7 @@ impl SetupPortalState {
|
|
|
502
651
|
pub struct AgentListState {
|
|
503
652
|
pub agents: Vec<AgentInfo>,
|
|
504
653
|
pub selected_index: usize,
|
|
505
|
-
pub detail_view: Option<usize>,
|
|
654
|
+
pub detail_view: Option<usize>, // None = list view, Some(index) = detail popup
|
|
506
655
|
}
|
|
507
656
|
|
|
508
657
|
impl Default for AgentListState {
|
|
@@ -538,16 +687,16 @@ impl App {
|
|
|
538
687
|
pub fn new() -> Self {
|
|
539
688
|
let render_scheduler = RenderScheduler::new();
|
|
540
689
|
let run_mode = render_scheduler.run_mode();
|
|
541
|
-
|
|
690
|
+
|
|
542
691
|
// Input debounce: browser 10ms, local 5ms (minimal delay for smooth typing)
|
|
543
692
|
let input_debounce_duration = match run_mode {
|
|
544
693
|
RunMode::Browser => Duration::from_millis(10),
|
|
545
694
|
RunMode::Local => Duration::from_millis(5),
|
|
546
695
|
};
|
|
547
|
-
|
|
696
|
+
|
|
548
697
|
let mut state = AppState::default();
|
|
549
698
|
Self::load_cached_data(&mut state);
|
|
550
|
-
|
|
699
|
+
|
|
551
700
|
Self {
|
|
552
701
|
state,
|
|
553
702
|
render_scheduler,
|
|
@@ -557,10 +706,21 @@ impl App {
|
|
|
557
706
|
}
|
|
558
707
|
|
|
559
708
|
/// Ask CLI to pull Gateway `GET /metrics` and return a condensed snapshot for Portal Monitoring.
|
|
709
|
+
///
|
|
710
|
+
/// **Intentional command split:** the TUI uses this `gateway.observability` path for the
|
|
711
|
+
/// **Overview** row (full health + Prometheus snapshot in one response). Per-section refresh
|
|
712
|
+
/// uses `begin_monitoring_refresh_request` → `monitoring.refresh` instead. The CLI also maps
|
|
713
|
+
/// `monitoring.refresh` with `section: "overview"` to the same handler for non-TUI callers.
|
|
560
714
|
pub fn begin_portal_observability_request(&mut self, ws: &WebSocketClient) {
|
|
561
715
|
if self.state.pending_gateway_observability_id.is_some() {
|
|
562
716
|
return;
|
|
563
717
|
}
|
|
718
|
+
if self.state.pending_monitoring_refresh_id.is_some()
|
|
719
|
+
|| self.state.pending_monitoring_logs_id.is_some()
|
|
720
|
+
|| self.state.pending_monitoring_drill_id.is_some()
|
|
721
|
+
{
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
564
724
|
self.state.portal_monitoring.loading = true;
|
|
565
725
|
self.state.portal_monitoring.error = None;
|
|
566
726
|
let nonce = std::time::SystemTime::now()
|
|
@@ -580,6 +740,188 @@ impl App {
|
|
|
580
740
|
}
|
|
581
741
|
}
|
|
582
742
|
|
|
743
|
+
/// Phase 2: refresh one Portal Monitoring section via CLI (`monitoring.refresh` → Gateway APIs).
|
|
744
|
+
pub fn begin_monitoring_refresh_request(
|
|
745
|
+
&mut self,
|
|
746
|
+
ws: &WebSocketClient,
|
|
747
|
+
section: MonitoringSection,
|
|
748
|
+
) {
|
|
749
|
+
if self.state.pending_gateway_observability_id.is_some()
|
|
750
|
+
|| self.state.pending_monitoring_refresh_id.is_some()
|
|
751
|
+
|| self.state.pending_monitoring_logs_id.is_some()
|
|
752
|
+
|| self.state.pending_monitoring_drill_id.is_some()
|
|
753
|
+
{
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
let nonce = std::time::SystemTime::now()
|
|
757
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
758
|
+
.map(|d| d.as_millis())
|
|
759
|
+
.unwrap_or(0);
|
|
760
|
+
let slug = section.cli_slug();
|
|
761
|
+
let data = serde_json::json!({ "section": slug, "nonce": nonce });
|
|
762
|
+
match ws.send_command("monitoring.refresh", Some(data)) {
|
|
763
|
+
Ok(id) => {
|
|
764
|
+
self.state.pending_monitoring_refresh_id = Some(id);
|
|
765
|
+
self.state.portal_monitoring.section_refresh_loading = Some(section);
|
|
766
|
+
self.state.portal_monitoring.error = None;
|
|
767
|
+
}
|
|
768
|
+
Err(e) => {
|
|
769
|
+
self.state.portal_monitoring.section_refresh_loading = None;
|
|
770
|
+
self.state.portal_monitoring.error = Some(format!("{}", e));
|
|
771
|
+
self.request_immediate_render("monitoring_refresh_send_err");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/// Phase 2: fetch Gateway log buffer (`monitoring.logs`).
|
|
777
|
+
pub fn begin_monitoring_logs_request(&mut self, ws: &WebSocketClient) {
|
|
778
|
+
if self.state.pending_gateway_observability_id.is_some()
|
|
779
|
+
|| self.state.pending_monitoring_refresh_id.is_some()
|
|
780
|
+
|| self.state.pending_monitoring_logs_id.is_some()
|
|
781
|
+
|| self.state.pending_monitoring_drill_id.is_some()
|
|
782
|
+
{
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
let nonce = std::time::SystemTime::now()
|
|
786
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
787
|
+
.map(|d| d.as_millis())
|
|
788
|
+
.unwrap_or(0);
|
|
789
|
+
let data = serde_json::json!({ "limit": 100u32, "nonce": nonce });
|
|
790
|
+
match ws.send_command("monitoring.logs", Some(data)) {
|
|
791
|
+
Ok(id) => {
|
|
792
|
+
self.state.pending_monitoring_logs_id = Some(id);
|
|
793
|
+
self.state.portal_monitoring.logs_fetch_loading = true;
|
|
794
|
+
self.state.portal_monitoring.error = None;
|
|
795
|
+
}
|
|
796
|
+
Err(e) => {
|
|
797
|
+
self.state.portal_monitoring.logs_fetch_loading = false;
|
|
798
|
+
self.state.portal_monitoring.error = Some(format!("{}", e));
|
|
799
|
+
self.request_immediate_render("monitoring_logs_send_err");
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/// Phase 3: Gateway drill-down (`monitoring.drill`) for Metrics sub-panels (HTTP / Runs / Queue / SSE).
|
|
805
|
+
pub fn begin_monitoring_drill_request(
|
|
806
|
+
&mut self,
|
|
807
|
+
ws: &WebSocketClient,
|
|
808
|
+
panel: MetricsDrillPanel,
|
|
809
|
+
) {
|
|
810
|
+
if self.state.pending_gateway_observability_id.is_some()
|
|
811
|
+
|| self.state.pending_monitoring_refresh_id.is_some()
|
|
812
|
+
|| self.state.pending_monitoring_logs_id.is_some()
|
|
813
|
+
|| self.state.pending_monitoring_drill_id.is_some()
|
|
814
|
+
{
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
let nonce = std::time::SystemTime::now()
|
|
818
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
819
|
+
.map(|d| d.as_millis())
|
|
820
|
+
.unwrap_or(0);
|
|
821
|
+
let target = panel.cli_target();
|
|
822
|
+
let data = serde_json::json!({ "target": target, "nonce": nonce });
|
|
823
|
+
match ws.send_command("monitoring.drill", Some(data)) {
|
|
824
|
+
Ok(id) => {
|
|
825
|
+
self.state.pending_monitoring_drill_id = Some(id);
|
|
826
|
+
self.state.portal_monitoring.metrics_drill = Some(panel);
|
|
827
|
+
self.state.portal_monitoring.metrics_drill_loading = true;
|
|
828
|
+
self.state.portal_monitoring.metrics_drill_lines.clear();
|
|
829
|
+
self.state.portal_monitoring.error = None;
|
|
830
|
+
}
|
|
831
|
+
Err(e) => {
|
|
832
|
+
self.state.portal_monitoring.metrics_drill_loading = false;
|
|
833
|
+
self.state.portal_monitoring.error = Some(format!("{}", e));
|
|
834
|
+
self.request_immediate_render("monitoring_drill_send_err");
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/// Phase 3: dependency pools + queue counts (`dependencies/detail`).
|
|
840
|
+
pub fn begin_dependencies_detail_drill(&mut self, ws: &WebSocketClient) {
|
|
841
|
+
if self.state.pending_gateway_observability_id.is_some()
|
|
842
|
+
|| self.state.pending_monitoring_refresh_id.is_some()
|
|
843
|
+
|| self.state.pending_monitoring_logs_id.is_some()
|
|
844
|
+
|| self.state.pending_monitoring_drill_id.is_some()
|
|
845
|
+
{
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
let nonce = std::time::SystemTime::now()
|
|
849
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
850
|
+
.map(|d| d.as_millis())
|
|
851
|
+
.unwrap_or(0);
|
|
852
|
+
let data = serde_json::json!({ "target": "dependencies_detail", "nonce": nonce });
|
|
853
|
+
match ws.send_command("monitoring.drill", Some(data)) {
|
|
854
|
+
Ok(id) => {
|
|
855
|
+
self.state.pending_monitoring_drill_id = Some(id);
|
|
856
|
+
self.state.portal_monitoring.section_refresh_loading =
|
|
857
|
+
Some(MonitoringSection::Dependencies);
|
|
858
|
+
self.state.portal_monitoring.error = None;
|
|
859
|
+
}
|
|
860
|
+
Err(e) => {
|
|
861
|
+
self.state.portal_monitoring.section_refresh_loading = None;
|
|
862
|
+
self.state.portal_monitoring.error = Some(format!("{}", e));
|
|
863
|
+
self.request_immediate_render("deps_drill_send_err");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/// Phase 4: run local CLI/host diagnostics for the System section.
|
|
869
|
+
pub fn begin_system_diagnostics_request(&mut self, ws: &WebSocketClient) {
|
|
870
|
+
if self.state.pending_gateway_observability_id.is_some()
|
|
871
|
+
|| self.state.pending_monitoring_refresh_id.is_some()
|
|
872
|
+
|| self.state.pending_monitoring_logs_id.is_some()
|
|
873
|
+
|| self.state.pending_monitoring_drill_id.is_some()
|
|
874
|
+
{
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
let nonce = std::time::SystemTime::now()
|
|
878
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
879
|
+
.map(|d| d.as_millis())
|
|
880
|
+
.unwrap_or(0);
|
|
881
|
+
let data = serde_json::json!({ "target": "system_diagnostics", "nonce": nonce });
|
|
882
|
+
match ws.send_command("monitoring.drill", Some(data)) {
|
|
883
|
+
Ok(id) => {
|
|
884
|
+
self.state.pending_monitoring_drill_id = Some(id);
|
|
885
|
+
self.state.portal_monitoring.section_refresh_loading =
|
|
886
|
+
Some(MonitoringSection::System);
|
|
887
|
+
self.state.portal_monitoring.error = None;
|
|
888
|
+
}
|
|
889
|
+
Err(e) => {
|
|
890
|
+
self.state.portal_monitoring.section_refresh_loading = None;
|
|
891
|
+
self.state.portal_monitoring.error = Some(format!("{}", e));
|
|
892
|
+
self.request_immediate_render("system_diagnostics_send_err");
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
pub fn export_portal_monitoring_snapshot(&mut self) -> anyhow::Result<String> {
|
|
898
|
+
let mut overrides = serde_json::Map::new();
|
|
899
|
+
for (section, lines) in &self.state.portal_monitoring.section_overrides {
|
|
900
|
+
overrides.insert(section.cli_slug().to_string(), serde_json::json!(lines));
|
|
901
|
+
}
|
|
902
|
+
let payload = serde_json::json!({
|
|
903
|
+
"exportedAt": wall_clock_hms(),
|
|
904
|
+
"gatewayUrl": self.state.gateway_url.clone(),
|
|
905
|
+
"lastUpdated": self.state.portal_monitoring.last_updated.clone(),
|
|
906
|
+
"contentLines": self.state.portal_monitoring.content_lines.clone(),
|
|
907
|
+
"sectionOverrides": overrides,
|
|
908
|
+
"metricHistory": self.state.portal_monitoring.metric_history.clone(),
|
|
909
|
+
"dependencyAlerts": self.state.portal_monitoring.dependency_alerts.clone(),
|
|
910
|
+
"trendView": self.state.portal_monitoring.trend_view,
|
|
911
|
+
});
|
|
912
|
+
let unix = std::time::SystemTime::now()
|
|
913
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
914
|
+
.map(|d| d.as_secs())
|
|
915
|
+
.unwrap_or(0);
|
|
916
|
+
let mut path = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir());
|
|
917
|
+
path.push(format!("4runr-monitoring-export-{unix}.json"));
|
|
918
|
+
let body = serde_json::to_string_pretty(&payload)?;
|
|
919
|
+
std::fs::write(&path, body)?;
|
|
920
|
+
let display = path.display().to_string();
|
|
921
|
+
self.state.portal_monitoring.last_export_path = Some(display.clone());
|
|
922
|
+
Ok(display)
|
|
923
|
+
}
|
|
924
|
+
|
|
583
925
|
/// CLI ↔ TUI WebSocket lost: drop Gateway link UI and leave Portal Monitoring / Connection Portal so the session does not appear still "connected".
|
|
584
926
|
pub fn on_cli_backend_disconnect(&mut self) {
|
|
585
927
|
use crate::screens::Screen;
|
|
@@ -591,8 +933,12 @@ impl App {
|
|
|
591
933
|
self.state.pending_gateway_connect_id = None;
|
|
592
934
|
self.state.pending_gateway_health_id = None;
|
|
593
935
|
self.state.pending_gateway_observability_id = None;
|
|
936
|
+
self.state.pending_monitoring_refresh_id = None;
|
|
937
|
+
self.state.pending_monitoring_logs_id = None;
|
|
938
|
+
self.state.pending_monitoring_drill_id = None;
|
|
594
939
|
self.state.portal_observability_last_poll = None;
|
|
595
940
|
self.state.portal_monitoring = PortalMonitoringState::default();
|
|
941
|
+
self.state.advanced_monitoring = AdvancedMonitoringState::default();
|
|
596
942
|
self.state.connection_portal.finish_connecting();
|
|
597
943
|
self.state.connection_portal.connection_success = false;
|
|
598
944
|
self.state.connection_portal.connecting = false;
|
|
@@ -615,33 +961,35 @@ impl App {
|
|
|
615
961
|
// Load cached agents
|
|
616
962
|
let agents = cache.get_agents();
|
|
617
963
|
if !agents.is_empty() {
|
|
618
|
-
state.capabilities = agents.iter()
|
|
619
|
-
.map(|a| a.name.clone())
|
|
620
|
-
.collect();
|
|
964
|
+
state.capabilities = agents.iter().map(|a| a.name.clone()).collect();
|
|
621
965
|
state.cache_loaded = true;
|
|
622
966
|
}
|
|
623
|
-
|
|
967
|
+
|
|
624
968
|
// Load cached system status
|
|
625
969
|
if let Some(status) = cache.get_system_status() {
|
|
626
|
-
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
|
+
};
|
|
627
975
|
state.posture_status = format!("{} (cached)", status.posture);
|
|
628
976
|
}
|
|
629
977
|
}
|
|
630
978
|
}
|
|
631
|
-
|
|
979
|
+
|
|
632
980
|
pub fn request_render(&mut self, reason: &str) -> bool {
|
|
633
981
|
self.render_scheduler.request_render(reason)
|
|
634
982
|
}
|
|
635
|
-
|
|
983
|
+
|
|
636
984
|
/// Request immediate render (bypass throttling) - for critical input events
|
|
637
985
|
pub fn request_immediate_render(&mut self, reason: &str) -> bool {
|
|
638
986
|
self.render_scheduler.request_immediate_render(reason)
|
|
639
987
|
}
|
|
640
|
-
|
|
988
|
+
|
|
641
989
|
pub fn should_render(&mut self) -> bool {
|
|
642
990
|
self.render_scheduler.should_render()
|
|
643
991
|
}
|
|
644
|
-
|
|
992
|
+
|
|
645
993
|
pub fn record_render(&mut self, duration_ms: u64) {
|
|
646
994
|
self.state.render_count += 1;
|
|
647
995
|
self.state.render_durations.push_back(duration_ms);
|
|
@@ -650,30 +998,30 @@ impl App {
|
|
|
650
998
|
}
|
|
651
999
|
self.state.last_render_time = Instant::now();
|
|
652
1000
|
}
|
|
653
|
-
|
|
1001
|
+
|
|
654
1002
|
pub fn run_mode(&self) -> RunMode {
|
|
655
1003
|
self.render_scheduler.run_mode()
|
|
656
1004
|
}
|
|
657
|
-
|
|
1005
|
+
|
|
658
1006
|
pub fn render_scheduler_stats(&self) -> (u64, u64) {
|
|
659
1007
|
(
|
|
660
1008
|
self.render_scheduler.render_scheduled_count(),
|
|
661
1009
|
self.render_scheduler.min_render_interval_ms(),
|
|
662
1010
|
)
|
|
663
1011
|
}
|
|
664
|
-
|
|
1012
|
+
|
|
665
1013
|
/// IMPORTANT: Only call tick() when poll() times out (NO keyboard input)!
|
|
666
1014
|
/// Otherwise animations will flash weirdly when typing.
|
|
667
|
-
///
|
|
1015
|
+
///
|
|
668
1016
|
/// NOTE: Uptime is now tracked via Instant::now() in main loop,
|
|
669
1017
|
/// so we don't increment it here anymore.
|
|
670
1018
|
pub fn tick(&mut self) {
|
|
671
1019
|
self.state.tick = self.state.tick.wrapping_add(1);
|
|
672
|
-
|
|
1020
|
+
|
|
673
1021
|
// Update spinner frame (for "Processing..." indicator only)
|
|
674
1022
|
// Cycle through 8 braille spinner frames
|
|
675
1023
|
self.state.spinner_frame = (self.state.spinner_frame + 1) % 8;
|
|
676
|
-
|
|
1024
|
+
|
|
677
1025
|
// Boot timeline: update progress every ~150-250ms
|
|
678
1026
|
if self.state.mode == AppMode::Boot && !self.state.boot_done {
|
|
679
1027
|
let elapsed = self.state.boot_started_at.elapsed();
|
|
@@ -684,15 +1032,15 @@ impl App {
|
|
|
684
1032
|
(Duration::from_millis(1100), "Telemetry connected…"),
|
|
685
1033
|
(Duration::from_millis(1400), "System ready."),
|
|
686
1034
|
];
|
|
687
|
-
|
|
1035
|
+
|
|
688
1036
|
let mut current_progress = 0;
|
|
689
1037
|
let mut step_idx = 0;
|
|
690
|
-
|
|
1038
|
+
|
|
691
1039
|
for (i, (delay, msg)) in boot_steps.iter().enumerate() {
|
|
692
1040
|
if elapsed >= *delay {
|
|
693
1041
|
step_idx = i + 1;
|
|
694
1042
|
current_progress = ((i + 1) * 100 / boot_steps.len()) as u16;
|
|
695
|
-
|
|
1043
|
+
|
|
696
1044
|
// Add line if not already added
|
|
697
1045
|
if self.state.boot_lines.len() <= i {
|
|
698
1046
|
self.state.boot_lines.push_back(msg.to_string());
|
|
@@ -700,23 +1048,28 @@ impl App {
|
|
|
700
1048
|
}
|
|
701
1049
|
}
|
|
702
1050
|
}
|
|
703
|
-
|
|
1051
|
+
|
|
704
1052
|
self.state.boot_progress = current_progress.min(100);
|
|
705
|
-
|
|
1053
|
+
|
|
706
1054
|
// Mark boot done after last step
|
|
707
1055
|
if step_idx >= boot_steps.len() && self.state.boot_progress >= 100 {
|
|
708
1056
|
self.state.boot_done = true;
|
|
709
1057
|
self.request_render("boot_done");
|
|
710
1058
|
}
|
|
711
1059
|
}
|
|
712
|
-
|
|
1060
|
+
|
|
713
1061
|
// NOTE: pulse_frame is NOT updated here - we use static dots for status indicators
|
|
714
1062
|
// to prevent flashing when typing. Only use animated pulse for non-status elements.
|
|
715
1063
|
}
|
|
716
1064
|
|
|
717
|
-
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> {
|
|
718
1071
|
use crossterm::event::KeyModifiers;
|
|
719
|
-
|
|
1072
|
+
|
|
720
1073
|
// === EXIT SHORTCUTS (always checked first) ===
|
|
721
1074
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
722
1075
|
match key.code {
|
|
@@ -726,45 +1079,45 @@ impl App {
|
|
|
726
1079
|
_ => return Ok(false), // Ignore other Ctrl combos
|
|
727
1080
|
}
|
|
728
1081
|
}
|
|
729
|
-
|
|
1082
|
+
|
|
730
1083
|
if key.code == KeyCode::F(10) {
|
|
731
1084
|
return Ok(true); // Exit
|
|
732
1085
|
}
|
|
733
|
-
|
|
1086
|
+
|
|
734
1087
|
// === BOOT MODE: Any key to continue ===
|
|
735
1088
|
if self.state.mode == AppMode::Boot && self.state.boot_done {
|
|
736
1089
|
// Any key press switches to main dashboard
|
|
737
1090
|
self.navigate_to(Screen::Main);
|
|
738
1091
|
return Ok(false);
|
|
739
1092
|
}
|
|
740
|
-
|
|
1093
|
+
|
|
741
1094
|
// In boot mode, ignore other input until boot is done
|
|
742
1095
|
if self.state.mode == AppMode::Boot {
|
|
743
1096
|
return Ok(false);
|
|
744
1097
|
}
|
|
745
|
-
|
|
1098
|
+
|
|
746
1099
|
// F12 - Toggle perf overlay
|
|
747
1100
|
if key.code == KeyCode::F(12) {
|
|
748
1101
|
self.state.perf_overlay = !self.state.perf_overlay;
|
|
749
1102
|
self.request_render("perf_toggle");
|
|
750
1103
|
return Ok(false);
|
|
751
1104
|
}
|
|
752
|
-
|
|
1105
|
+
|
|
753
1106
|
// === AGENT BUILDER INPUT HANDLING ===
|
|
754
1107
|
if self.state.navigation.current_screen() == &Screen::AgentBuilder {
|
|
755
1108
|
return self.handle_agent_builder_input(key, ws_client);
|
|
756
1109
|
}
|
|
757
|
-
|
|
1110
|
+
|
|
758
1111
|
// === RUN MANAGER INPUT HANDLING ===
|
|
759
1112
|
if self.state.navigation.current_screen() == &Screen::RunManager {
|
|
760
1113
|
return self.handle_run_manager_input(key, ws_client);
|
|
761
1114
|
}
|
|
762
|
-
|
|
1115
|
+
|
|
763
1116
|
// === SETTINGS INPUT HANDLING ===
|
|
764
1117
|
if self.state.navigation.current_screen() == &Screen::Settings {
|
|
765
1118
|
return self.handle_settings_input(key);
|
|
766
1119
|
}
|
|
767
|
-
|
|
1120
|
+
|
|
768
1121
|
// === CONNECTION PORTAL INPUT HANDLING ===
|
|
769
1122
|
if self.state.navigation.current_screen() == &Screen::ConnectionPortal {
|
|
770
1123
|
return self.handle_connection_portal_input(key, ws_client);
|
|
@@ -774,17 +1127,17 @@ impl App {
|
|
|
774
1127
|
if self.state.navigation.current_screen() == &Screen::PortalMonitoring {
|
|
775
1128
|
return self.handle_portal_monitoring_input(key, ws_client);
|
|
776
1129
|
}
|
|
777
|
-
|
|
1130
|
+
|
|
778
1131
|
// === SETUP PORTAL INPUT HANDLING ===
|
|
779
1132
|
if self.state.navigation.current_screen() == &Screen::SetupPortal {
|
|
780
1133
|
return self.handle_setup_portal_input(key, ws_client);
|
|
781
1134
|
}
|
|
782
|
-
|
|
1135
|
+
|
|
783
1136
|
// === AGENT LIST INPUT HANDLING ===
|
|
784
1137
|
if self.state.navigation.current_screen() == &Screen::AgentList {
|
|
785
1138
|
return self.handle_agent_list_input(key, ws_client);
|
|
786
1139
|
}
|
|
787
|
-
|
|
1140
|
+
|
|
788
1141
|
// === MAIN INPUT HANDLING ===
|
|
789
1142
|
match key.code {
|
|
790
1143
|
// F2 - Open Setup Portal (Gateway options) from main screen
|
|
@@ -800,7 +1153,8 @@ impl App {
|
|
|
800
1153
|
} else {
|
|
801
1154
|
self.state.setup_portal.detecting = false;
|
|
802
1155
|
self.state.setup_portal.detecting_since = None;
|
|
803
|
-
self.state.setup_portal.error =
|
|
1156
|
+
self.state.setup_portal.error =
|
|
1157
|
+
Some("Failed to start detection".to_string());
|
|
804
1158
|
}
|
|
805
1159
|
} else {
|
|
806
1160
|
self.state.setup_portal.error = Some(
|
|
@@ -808,7 +1162,9 @@ impl App {
|
|
|
808
1162
|
.into(),
|
|
809
1163
|
);
|
|
810
1164
|
}
|
|
811
|
-
self.state
|
|
1165
|
+
self.state
|
|
1166
|
+
.logs
|
|
1167
|
+
.push_back("[NAV] Opening Setup Portal (F2)...".into());
|
|
812
1168
|
self.request_immediate_render("open_setup_portal");
|
|
813
1169
|
}
|
|
814
1170
|
// Typing - ALL characters go to command input (immediate render for responsiveness)
|
|
@@ -819,14 +1175,14 @@ impl App {
|
|
|
819
1175
|
// OPTIMIZATION: Immediate render for instant typing feedback
|
|
820
1176
|
self.request_immediate_render("typing_input");
|
|
821
1177
|
}
|
|
822
|
-
|
|
1178
|
+
|
|
823
1179
|
// Submit command
|
|
824
1180
|
KeyCode::Enter => {
|
|
825
1181
|
if !self.state.command_input.is_empty() {
|
|
826
1182
|
let cmd: String = self.state.command_input.drain(..).collect();
|
|
827
1183
|
self.state.command_focused = false;
|
|
828
1184
|
self.input_debounce = None;
|
|
829
|
-
|
|
1185
|
+
|
|
830
1186
|
match cmd.to_lowercase().as_str() {
|
|
831
1187
|
"quit" | "exit" => return Ok(true),
|
|
832
1188
|
"clear" => self.state.logs.clear(),
|
|
@@ -834,59 +1190,131 @@ impl App {
|
|
|
834
1190
|
// Display help in operations log - clean and organized
|
|
835
1191
|
// NOTE: VecDeque displays newest items first, so we push in REVERSE order
|
|
836
1192
|
// (Last item pushed appears at top of log)
|
|
837
|
-
|
|
1193
|
+
|
|
838
1194
|
// Push Keyboard Shortcuts FIRST (will appear at BOTTOM)
|
|
839
|
-
self.state.logs.push_back(
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
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());
|
|
844
1214
|
self.state.logs.push_back("".to_string());
|
|
845
|
-
|
|
1215
|
+
|
|
846
1216
|
// WebSocket Commands
|
|
847
|
-
self.state.logs.push_back(
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
self.state.logs.push_back(
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
self.state
|
|
854
|
-
|
|
855
|
-
|
|
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
|
+
);
|
|
856
1247
|
self.state.logs.push_back("".to_string());
|
|
857
|
-
|
|
1248
|
+
|
|
858
1249
|
// Local Commands
|
|
859
|
-
self.state.logs.push_back(
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
self.state
|
|
863
|
-
|
|
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
|
+
);
|
|
864
1266
|
self.state.logs.push_back("💻 LOCAL COMMANDS".to_string());
|
|
865
1267
|
self.state.logs.push_back("".to_string());
|
|
866
|
-
|
|
1268
|
+
|
|
867
1269
|
// Navigation Commands
|
|
868
1270
|
self.state.logs.push_back(" disconnect - Disconnect from Gateway (return to LOCAL mode)".to_string());
|
|
869
1271
|
self.state.logs.push_back(" portal monitoring - Gateway /metrics traffic (R refresh, ↑↓ scroll)".to_string());
|
|
870
1272
|
self.state.logs.push_back(" connect portal - Open Connection Portal (connect to Gateway)".to_string());
|
|
871
|
-
self.state.logs.push_back(
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
self.state.logs.push_back(
|
|
876
|
-
|
|
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());
|
|
877
1296
|
self.state.logs.push_back("".to_string());
|
|
878
|
-
|
|
1297
|
+
|
|
879
1298
|
// Push Header LAST (will appear at TOP)
|
|
880
|
-
self.state.logs.push_back(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
+
|
|
884
1311
|
self.request_render("help_command");
|
|
885
1312
|
}
|
|
886
1313
|
":perf" => {
|
|
887
1314
|
// Perf self-check command
|
|
888
1315
|
let rps = if !self.state.render_durations.is_empty() {
|
|
889
|
-
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
|
|
890
1318
|
/ self.state.render_durations.len() as f64;
|
|
891
1319
|
if avg_ms > 0.0 {
|
|
892
1320
|
(1000.0 / avg_ms) as u64
|
|
@@ -896,30 +1324,31 @@ impl App {
|
|
|
896
1324
|
} else {
|
|
897
1325
|
0
|
|
898
1326
|
};
|
|
899
|
-
|
|
1327
|
+
|
|
900
1328
|
let mode_str = match self.run_mode() {
|
|
901
1329
|
RunMode::Browser => "browser",
|
|
902
1330
|
RunMode::Local => "local",
|
|
903
1331
|
};
|
|
904
|
-
|
|
1332
|
+
|
|
905
1333
|
let (scheduled_count, interval_ms) = self.render_scheduler_stats();
|
|
906
|
-
|
|
1334
|
+
|
|
907
1335
|
self.state.logs.push_back(format!(
|
|
908
1336
|
"[PERF] Mode: {} | RPS: {} | Render interval: {}ms | Scheduled: {}",
|
|
909
|
-
mode_str,
|
|
910
|
-
rps,
|
|
911
|
-
interval_ms,
|
|
912
|
-
scheduled_count
|
|
1337
|
+
mode_str, rps, interval_ms, scheduled_count
|
|
913
1338
|
));
|
|
914
1339
|
}
|
|
915
1340
|
// Navigation commands
|
|
916
1341
|
"build" | "build new" | "agent new" => {
|
|
917
1342
|
self.push_overlay(Screen::AgentBuilder);
|
|
918
|
-
self.state
|
|
1343
|
+
self.state
|
|
1344
|
+
.logs
|
|
1345
|
+
.push_back("[NAV] Opening Agent Builder...".into());
|
|
919
1346
|
}
|
|
920
1347
|
"runs" | "run list" | "run manager" => {
|
|
921
1348
|
self.push_overlay(Screen::RunManager);
|
|
922
|
-
self.state
|
|
1349
|
+
self.state
|
|
1350
|
+
.logs
|
|
1351
|
+
.push_back("[NAV] Opening Run Manager...".into());
|
|
923
1352
|
if self.state.operation_mode == OperationMode::Connected {
|
|
924
1353
|
if let Some(ws) = ws_client {
|
|
925
1354
|
let data = serde_json::json!({ "limit": 50 });
|
|
@@ -929,16 +1358,23 @@ impl App {
|
|
|
929
1358
|
}
|
|
930
1359
|
}
|
|
931
1360
|
} else {
|
|
932
|
-
self.add_log(
|
|
1361
|
+
self.add_log(
|
|
1362
|
+
"[RUN] Connect to Gateway (connect portal) to load live runs."
|
|
1363
|
+
.to_string(),
|
|
1364
|
+
);
|
|
933
1365
|
}
|
|
934
1366
|
}
|
|
935
1367
|
"config" | "settings" => {
|
|
936
1368
|
self.push_overlay(Screen::Settings);
|
|
937
|
-
self.state
|
|
1369
|
+
self.state
|
|
1370
|
+
.logs
|
|
1371
|
+
.push_back("[NAV] Opening Settings...".into());
|
|
938
1372
|
}
|
|
939
1373
|
"a" | "agents" | "agent list" => {
|
|
940
1374
|
self.push_overlay(Screen::AgentList);
|
|
941
|
-
self.state
|
|
1375
|
+
self.state
|
|
1376
|
+
.logs
|
|
1377
|
+
.push_back("[NAV] Opening Agent List...".into());
|
|
942
1378
|
// Request agent list from backend
|
|
943
1379
|
if let Some(ws) = ws_client {
|
|
944
1380
|
if let Ok(list_id) = ws.send_command("agent.list", None) {
|
|
@@ -951,7 +1387,12 @@ impl App {
|
|
|
951
1387
|
self.state
|
|
952
1388
|
.navigation
|
|
953
1389
|
.navigate_to_base(Screen::PortalMonitoring);
|
|
954
|
-
self.state.
|
|
1390
|
+
self.state.advanced_monitoring =
|
|
1391
|
+
AdvancedMonitoringState::new(self.state.gateway_url.clone());
|
|
1392
|
+
self.state.portal_monitoring.scroll_offset = 0;
|
|
1393
|
+
self.state
|
|
1394
|
+
.logs
|
|
1395
|
+
.push_back("[NAV] Opening Portal Monitoring...".into());
|
|
955
1396
|
if let Some(ws) = ws_client {
|
|
956
1397
|
self.begin_portal_observability_request(ws);
|
|
957
1398
|
}
|
|
@@ -969,25 +1410,31 @@ impl App {
|
|
|
969
1410
|
self.state
|
|
970
1411
|
.navigation
|
|
971
1412
|
.navigate_to_base(Screen::ConnectionPortal);
|
|
972
|
-
self.state
|
|
1413
|
+
self.state
|
|
1414
|
+
.logs
|
|
1415
|
+
.push_back("[NAV] Opening Connection Portal...".into());
|
|
973
1416
|
self.request_immediate_render("open_connection_portal");
|
|
974
1417
|
}
|
|
975
1418
|
"setup" | "setup gateway" | "setup portal" => {
|
|
976
1419
|
// Navigate to portal as base screen (standalone, not overlay)
|
|
977
1420
|
self.state.navigation.navigate_to_base(Screen::SetupPortal);
|
|
978
1421
|
self.state.setup_portal.soft_open();
|
|
979
|
-
|
|
1422
|
+
|
|
980
1423
|
// Auto-detect Gateway options when portal opens
|
|
981
1424
|
if let Some(ws) = ws_client {
|
|
982
1425
|
self.state.setup_portal.detecting = true;
|
|
983
|
-
self.state.setup_portal.detecting_since =
|
|
1426
|
+
self.state.setup_portal.detecting_since =
|
|
1427
|
+
Some(std::time::Instant::now());
|
|
984
1428
|
if let Ok(id) = ws.send_command("setup.detect", None) {
|
|
985
1429
|
self.state.pending_setup_detect_id = Some(id);
|
|
986
|
-
self.add_log(
|
|
1430
|
+
self.add_log(
|
|
1431
|
+
"[SETUP] Detecting Gateway options...".to_string(),
|
|
1432
|
+
);
|
|
987
1433
|
} else {
|
|
988
1434
|
self.state.setup_portal.detecting = false;
|
|
989
1435
|
self.state.setup_portal.detecting_since = None;
|
|
990
|
-
self.state.setup_portal.error =
|
|
1436
|
+
self.state.setup_portal.error =
|
|
1437
|
+
Some("Failed to start detection".to_string());
|
|
991
1438
|
}
|
|
992
1439
|
} else {
|
|
993
1440
|
self.state.setup_portal.error = Some(
|
|
@@ -995,8 +1442,10 @@ impl App {
|
|
|
995
1442
|
.into(),
|
|
996
1443
|
);
|
|
997
1444
|
}
|
|
998
|
-
|
|
999
|
-
self.state
|
|
1445
|
+
|
|
1446
|
+
self.state
|
|
1447
|
+
.logs
|
|
1448
|
+
.push_back("[NAV] Opening Setup Portal...".into());
|
|
1000
1449
|
self.request_immediate_render("open_setup_portal");
|
|
1001
1450
|
}
|
|
1002
1451
|
"disconnect" => {
|
|
@@ -1005,15 +1454,23 @@ impl App {
|
|
|
1005
1454
|
match ws.send_command("gateway.disconnect", None) {
|
|
1006
1455
|
Ok(id) => {
|
|
1007
1456
|
let short_id = &id[id.len().saturating_sub(8)..];
|
|
1008
|
-
self.state.logs.push_back(format!(
|
|
1457
|
+
self.state.logs.push_back(format!(
|
|
1458
|
+
"[GATEWAY] Disconnecting (id: {})",
|
|
1459
|
+
short_id
|
|
1460
|
+
));
|
|
1009
1461
|
// Mode will be updated when response is received
|
|
1010
1462
|
}
|
|
1011
1463
|
Err(e) => {
|
|
1012
|
-
self.state.logs.push_back(format!(
|
|
1464
|
+
self.state.logs.push_back(format!(
|
|
1465
|
+
"[ERROR] Failed to disconnect: {}",
|
|
1466
|
+
e
|
|
1467
|
+
));
|
|
1013
1468
|
}
|
|
1014
1469
|
}
|
|
1015
1470
|
} else {
|
|
1016
|
-
self.state
|
|
1471
|
+
self.state
|
|
1472
|
+
.logs
|
|
1473
|
+
.push_back("[ERROR] WebSocket not connected".into());
|
|
1017
1474
|
}
|
|
1018
1475
|
}
|
|
1019
1476
|
_ => {
|
|
@@ -1025,29 +1482,40 @@ impl App {
|
|
|
1025
1482
|
match ws.send_command(&cmd, None) {
|
|
1026
1483
|
Ok(id) => {
|
|
1027
1484
|
let short_id = &id[id.len().saturating_sub(8)..];
|
|
1028
|
-
self.state
|
|
1485
|
+
self.state
|
|
1486
|
+
.logs
|
|
1487
|
+
.push_back(format!("> {} [{}]", cmd, short_id));
|
|
1029
1488
|
}
|
|
1030
1489
|
Err(e) => {
|
|
1031
|
-
self.state.logs.push_back(format!(
|
|
1490
|
+
self.state.logs.push_back(format!(
|
|
1491
|
+
"[ERROR] Failed to send '{}': {}",
|
|
1492
|
+
cmd, e
|
|
1493
|
+
));
|
|
1032
1494
|
}
|
|
1033
1495
|
}
|
|
1034
1496
|
} else {
|
|
1035
1497
|
// Unknown local command - provide helpful guidance
|
|
1036
|
-
self.state
|
|
1498
|
+
self.state
|
|
1499
|
+
.logs
|
|
1500
|
+
.push_back(format!("> {} (unknown command)", cmd));
|
|
1037
1501
|
self.state.logs.push_back("[HELP] Try: help, build, runs, config, agent.list, system.status".into());
|
|
1038
1502
|
}
|
|
1039
1503
|
} else {
|
|
1040
1504
|
// No WebSocket connection - guide to local commands
|
|
1041
|
-
self.state
|
|
1505
|
+
self.state
|
|
1506
|
+
.logs
|
|
1507
|
+
.push_back(format!("> {} (WebSocket not connected)", cmd));
|
|
1042
1508
|
self.state.logs.push_back("[WARN] Backend not running. Start backend or use local commands.".into());
|
|
1043
|
-
self.state.logs.push_back(
|
|
1509
|
+
self.state.logs.push_back(
|
|
1510
|
+
"[HELP] Local: help, build, runs, config, clear".into(),
|
|
1511
|
+
);
|
|
1044
1512
|
}
|
|
1045
1513
|
}
|
|
1046
1514
|
}
|
|
1047
1515
|
self.request_render("command_submit");
|
|
1048
1516
|
}
|
|
1049
1517
|
}
|
|
1050
|
-
|
|
1518
|
+
|
|
1051
1519
|
// Delete character
|
|
1052
1520
|
KeyCode::Backspace => {
|
|
1053
1521
|
self.state.command_input.pop();
|
|
@@ -1057,7 +1525,7 @@ impl App {
|
|
|
1057
1525
|
// OPTIMIZATION: Immediate render for instant feedback
|
|
1058
1526
|
self.request_immediate_render("backspace_input");
|
|
1059
1527
|
}
|
|
1060
|
-
|
|
1528
|
+
|
|
1061
1529
|
// Clear input or close overlay/popup
|
|
1062
1530
|
KeyCode::Esc => {
|
|
1063
1531
|
// Priority: close popup > close overlay > clear input
|
|
@@ -1072,41 +1540,41 @@ impl App {
|
|
|
1072
1540
|
self.request_render("clear_input");
|
|
1073
1541
|
}
|
|
1074
1542
|
}
|
|
1075
|
-
|
|
1543
|
+
|
|
1076
1544
|
// === SCROLL: Arrow keys (only when not typing) ===
|
|
1077
1545
|
// Calculate max valid scroll based on visible height
|
|
1078
1546
|
KeyCode::Up if self.state.command_input.is_empty() => {
|
|
1079
1547
|
let visible_height = 15; // Approximate visible log height (adjust based on actual panel size)
|
|
1080
1548
|
let total_logs = self.state.logs.len();
|
|
1081
1549
|
let max_scroll = total_logs.saturating_sub(visible_height.max(1));
|
|
1082
|
-
|
|
1550
|
+
|
|
1083
1551
|
// Clamp scroll to valid range [0, max_scroll]
|
|
1084
1552
|
self.state.log_scroll = (self.state.log_scroll + 1).min(max_scroll);
|
|
1085
1553
|
self.request_render("scroll_up");
|
|
1086
1554
|
}
|
|
1087
|
-
|
|
1555
|
+
|
|
1088
1556
|
KeyCode::Down if self.state.command_input.is_empty() => {
|
|
1089
1557
|
self.state.log_scroll = self.state.log_scroll.saturating_sub(1);
|
|
1090
1558
|
self.request_render("scroll_down");
|
|
1091
1559
|
}
|
|
1092
|
-
|
|
1560
|
+
|
|
1093
1561
|
KeyCode::PageUp if self.state.command_input.is_empty() => {
|
|
1094
1562
|
let visible_height = 15;
|
|
1095
1563
|
let total_logs = self.state.logs.len();
|
|
1096
1564
|
let max_scroll = total_logs.saturating_sub(visible_height.max(1));
|
|
1097
|
-
|
|
1565
|
+
|
|
1098
1566
|
self.state.log_scroll = (self.state.log_scroll + 10).min(max_scroll);
|
|
1099
1567
|
self.request_render("scroll_page_up");
|
|
1100
1568
|
}
|
|
1101
|
-
|
|
1569
|
+
|
|
1102
1570
|
KeyCode::PageDown if self.state.command_input.is_empty() => {
|
|
1103
1571
|
self.state.log_scroll = self.state.log_scroll.saturating_sub(10);
|
|
1104
1572
|
self.request_render("scroll_page_down");
|
|
1105
1573
|
}
|
|
1106
|
-
|
|
1574
|
+
|
|
1107
1575
|
_ => {}
|
|
1108
1576
|
}
|
|
1109
|
-
|
|
1577
|
+
|
|
1110
1578
|
Ok(false)
|
|
1111
1579
|
}
|
|
1112
1580
|
|
|
@@ -1138,15 +1606,21 @@ impl App {
|
|
|
1138
1606
|
pub fn render(&mut self, f: &mut Frame) {
|
|
1139
1607
|
// Use new navigation system - render based on current screen
|
|
1140
1608
|
let current = self.state.navigation.current_screen();
|
|
1141
|
-
|
|
1609
|
+
|
|
1142
1610
|
// DEBUG: Log render decisions
|
|
1143
1611
|
#[cfg(debug_assertions)]
|
|
1144
1612
|
{
|
|
1145
1613
|
eprintln!("[RENDER] Current screen: {:?}", current);
|
|
1146
|
-
eprintln!(
|
|
1147
|
-
|
|
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
|
+
);
|
|
1148
1622
|
}
|
|
1149
|
-
|
|
1623
|
+
|
|
1150
1624
|
match current {
|
|
1151
1625
|
Screen::Boot => {
|
|
1152
1626
|
use crate::ui::boot;
|
|
@@ -1210,7 +1684,7 @@ impl App {
|
|
|
1210
1684
|
}
|
|
1211
1685
|
}
|
|
1212
1686
|
}
|
|
1213
|
-
|
|
1687
|
+
|
|
1214
1688
|
/// Add a log message
|
|
1215
1689
|
pub fn add_log(&mut self, message: String) {
|
|
1216
1690
|
self.state.logs.push_back(message);
|
|
@@ -1219,27 +1693,27 @@ impl App {
|
|
|
1219
1693
|
}
|
|
1220
1694
|
self.state.log_write_count += 1;
|
|
1221
1695
|
}
|
|
1222
|
-
|
|
1696
|
+
|
|
1223
1697
|
// ============================================================
|
|
1224
1698
|
// NAVIGATION METHODS (NEW - Step 4.1)
|
|
1225
1699
|
// ============================================================
|
|
1226
|
-
|
|
1700
|
+
|
|
1227
1701
|
/// Navigate to a base screen (Boot or Main)
|
|
1228
1702
|
pub fn navigate_to(&mut self, screen: Screen) {
|
|
1229
1703
|
if screen.is_base() {
|
|
1230
1704
|
self.state.navigation.navigate_to_base(screen.clone());
|
|
1231
|
-
|
|
1705
|
+
|
|
1232
1706
|
// Update legacy mode field for backward compatibility
|
|
1233
1707
|
self.state.mode = match screen {
|
|
1234
1708
|
Screen::Boot => AppMode::Boot,
|
|
1235
1709
|
Screen::Main => AppMode::Main,
|
|
1236
1710
|
_ => self.state.mode.clone(),
|
|
1237
1711
|
};
|
|
1238
|
-
|
|
1712
|
+
|
|
1239
1713
|
self.request_render("navigate_to");
|
|
1240
1714
|
}
|
|
1241
1715
|
}
|
|
1242
|
-
|
|
1716
|
+
|
|
1243
1717
|
/// Push an overlay screen (Agent Builder, Run Manager, Settings)
|
|
1244
1718
|
pub fn push_overlay(&mut self, screen: Screen) {
|
|
1245
1719
|
if screen.is_overlay() {
|
|
@@ -1247,7 +1721,7 @@ impl App {
|
|
|
1247
1721
|
self.request_render("push_overlay");
|
|
1248
1722
|
}
|
|
1249
1723
|
}
|
|
1250
|
-
|
|
1724
|
+
|
|
1251
1725
|
/// Pop the current overlay and return to previous screen
|
|
1252
1726
|
pub fn pop_overlay(&mut self) -> Option<Screen> {
|
|
1253
1727
|
let result = self.state.navigation.pop_overlay();
|
|
@@ -1256,7 +1730,7 @@ impl App {
|
|
|
1256
1730
|
}
|
|
1257
1731
|
result
|
|
1258
1732
|
}
|
|
1259
|
-
|
|
1733
|
+
|
|
1260
1734
|
/// Push a popup screen (Confirmation, Alert, Help)
|
|
1261
1735
|
pub fn push_popup(&mut self, screen: Screen) {
|
|
1262
1736
|
if screen.is_popup() {
|
|
@@ -1264,7 +1738,7 @@ impl App {
|
|
|
1264
1738
|
self.request_render("push_popup");
|
|
1265
1739
|
}
|
|
1266
1740
|
}
|
|
1267
|
-
|
|
1741
|
+
|
|
1268
1742
|
/// Pop the current popup
|
|
1269
1743
|
pub fn pop_popup(&mut self) -> Option<Screen> {
|
|
1270
1744
|
let result = self.state.navigation.pop_popup();
|
|
@@ -1273,34 +1747,38 @@ impl App {
|
|
|
1273
1747
|
}
|
|
1274
1748
|
result
|
|
1275
1749
|
}
|
|
1276
|
-
|
|
1750
|
+
|
|
1277
1751
|
/// Close all overlays and popups (return to base screen)
|
|
1278
1752
|
#[allow(dead_code)]
|
|
1279
1753
|
pub fn close_all_overlays(&mut self) {
|
|
1280
1754
|
self.state.navigation.close_all();
|
|
1281
1755
|
self.request_render("close_all");
|
|
1282
1756
|
}
|
|
1283
|
-
|
|
1757
|
+
|
|
1284
1758
|
/// Get the currently visible screen
|
|
1285
1759
|
#[allow(dead_code)]
|
|
1286
1760
|
pub fn current_screen(&self) -> &Screen {
|
|
1287
1761
|
self.state.navigation.current_screen()
|
|
1288
1762
|
}
|
|
1289
|
-
|
|
1763
|
+
|
|
1290
1764
|
/// Check if we're on the base screen (no overlays/popups)
|
|
1291
1765
|
#[allow(dead_code)]
|
|
1292
1766
|
pub fn is_on_base_screen(&self) -> bool {
|
|
1293
1767
|
self.state.navigation.is_on_base()
|
|
1294
1768
|
}
|
|
1295
|
-
|
|
1769
|
+
|
|
1296
1770
|
// ============================================================
|
|
1297
1771
|
// AGENT BUILDER INPUT HANDLING (Step 4.5)
|
|
1298
1772
|
// ============================================================
|
|
1299
|
-
|
|
1773
|
+
|
|
1300
1774
|
/// Handle input when Agent Builder screen is active
|
|
1301
|
-
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> {
|
|
1302
1780
|
use crossterm::event::KeyModifiers;
|
|
1303
|
-
|
|
1781
|
+
|
|
1304
1782
|
// Ctrl+C/Q to exit
|
|
1305
1783
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
1306
1784
|
match key.code {
|
|
@@ -1308,7 +1786,7 @@ impl App {
|
|
|
1308
1786
|
_ => {}
|
|
1309
1787
|
}
|
|
1310
1788
|
}
|
|
1311
|
-
|
|
1789
|
+
|
|
1312
1790
|
match key.code {
|
|
1313
1791
|
// Character input - route to appropriate field
|
|
1314
1792
|
KeyCode::Char(c) => {
|
|
@@ -1385,15 +1863,15 @@ impl App {
|
|
|
1385
1863
|
}
|
|
1386
1864
|
_ => {}
|
|
1387
1865
|
}
|
|
1388
|
-
|
|
1866
|
+
|
|
1389
1867
|
Ok(false)
|
|
1390
1868
|
}
|
|
1391
|
-
|
|
1869
|
+
|
|
1392
1870
|
/// Handle character input for Agent Builder fields
|
|
1393
1871
|
fn handle_agent_builder_char_input(&mut self, c: char) {
|
|
1394
1872
|
let step = self.state.agent_builder.current_step;
|
|
1395
1873
|
let field = self.state.agent_builder.focused_field;
|
|
1396
|
-
|
|
1874
|
+
|
|
1397
1875
|
match step {
|
|
1398
1876
|
1 => {
|
|
1399
1877
|
// Step 1: Basic Info
|
|
@@ -1451,7 +1929,10 @@ impl App {
|
|
|
1451
1929
|
if field < tools.len() {
|
|
1452
1930
|
let tool = tools[field].clone();
|
|
1453
1931
|
if self.state.agent_builder.selected_tools.contains(&tool) {
|
|
1454
|
-
self.state
|
|
1932
|
+
self.state
|
|
1933
|
+
.agent_builder
|
|
1934
|
+
.selected_tools
|
|
1935
|
+
.retain(|t| t != &tool);
|
|
1455
1936
|
} else {
|
|
1456
1937
|
self.state.agent_builder.selected_tools.push(tool);
|
|
1457
1938
|
}
|
|
@@ -1461,30 +1942,40 @@ impl App {
|
|
|
1461
1942
|
_ => {}
|
|
1462
1943
|
}
|
|
1463
1944
|
}
|
|
1464
|
-
|
|
1945
|
+
|
|
1465
1946
|
/// Handle backspace for Agent Builder fields
|
|
1466
1947
|
fn handle_agent_builder_backspace(&mut self) {
|
|
1467
1948
|
let step = self.state.agent_builder.current_step;
|
|
1468
1949
|
let field = self.state.agent_builder.focused_field;
|
|
1469
|
-
|
|
1950
|
+
|
|
1470
1951
|
match step {
|
|
1471
|
-
1 => {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
1 => { self.state.agent_builder.description.pop(); }
|
|
1475
|
-
_ => {}
|
|
1952
|
+
1 => match field {
|
|
1953
|
+
0 => {
|
|
1954
|
+
self.state.agent_builder.name.pop();
|
|
1476
1955
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
match field {
|
|
1480
|
-
0 => { self.state.agent_builder.provider.pop(); }
|
|
1481
|
-
1 => { self.state.agent_builder.model.pop(); }
|
|
1482
|
-
2 => { self.state.agent_builder.local_provider.pop(); }
|
|
1483
|
-
3 => { self.state.agent_builder.local_model.pop(); }
|
|
1484
|
-
4 => { self.state.agent_builder.local_url.pop(); }
|
|
1485
|
-
_ => {}
|
|
1956
|
+
1 => {
|
|
1957
|
+
self.state.agent_builder.description.pop();
|
|
1486
1958
|
}
|
|
1487
|
-
|
|
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
|
+
},
|
|
1488
1979
|
3 => {
|
|
1489
1980
|
self.state.agent_builder.system_prompt.pop();
|
|
1490
1981
|
}
|
|
@@ -1515,12 +2006,14 @@ impl App {
|
|
|
1515
2006
|
_ => {}
|
|
1516
2007
|
}
|
|
1517
2008
|
}
|
|
1518
|
-
|
|
2009
|
+
|
|
1519
2010
|
/// Create agent from builder state and send to backend
|
|
1520
2011
|
fn create_agent_from_builder(&mut self, ws_client: Option<&WebSocketClient>) {
|
|
1521
2012
|
// Validate one final time
|
|
1522
2013
|
if !self.state.agent_builder.validate_step() {
|
|
1523
|
-
self.add_log(
|
|
2014
|
+
self.add_log(
|
|
2015
|
+
"[ERROR] Agent validation failed. Please fix errors and try again.".to_string(),
|
|
2016
|
+
);
|
|
1524
2017
|
// Clone errors to avoid borrow checker issues
|
|
1525
2018
|
let errors = self.state.agent_builder.validation_errors.clone();
|
|
1526
2019
|
for error in &errors {
|
|
@@ -1528,16 +2021,16 @@ impl App {
|
|
|
1528
2021
|
}
|
|
1529
2022
|
return;
|
|
1530
2023
|
}
|
|
1531
|
-
|
|
2024
|
+
|
|
1532
2025
|
// Clone name before borrowing mutably
|
|
1533
2026
|
let agent_name = self.state.agent_builder.name.clone();
|
|
1534
|
-
|
|
2027
|
+
|
|
1535
2028
|
// Validate name is not empty
|
|
1536
2029
|
if agent_name.trim().is_empty() {
|
|
1537
2030
|
self.add_log("[ERROR] Agent name cannot be empty".to_string());
|
|
1538
2031
|
return;
|
|
1539
2032
|
}
|
|
1540
|
-
|
|
2033
|
+
|
|
1541
2034
|
// Build agent data
|
|
1542
2035
|
let builder = &self.state.agent_builder;
|
|
1543
2036
|
let agent_data = serde_json::json!({
|
|
@@ -1572,10 +2065,10 @@ impl App {
|
|
|
1572
2065
|
"presencePenalty": builder.presence_penalty,
|
|
1573
2066
|
"tools": builder.selected_tools,
|
|
1574
2067
|
});
|
|
1575
|
-
|
|
2068
|
+
|
|
1576
2069
|
// Log the creation attempt
|
|
1577
2070
|
self.add_log(format!("[AGENT] Creating agent '{}'...", agent_name));
|
|
1578
|
-
|
|
2071
|
+
|
|
1579
2072
|
// Send agent.create command via WebSocket
|
|
1580
2073
|
if let Some(ws) = ws_client {
|
|
1581
2074
|
match ws.send_command("agent.create", Some(agent_data)) {
|
|
@@ -1595,21 +2088,21 @@ impl App {
|
|
|
1595
2088
|
self.add_log("[HELP] Start the backend server and reconnect.".to_string());
|
|
1596
2089
|
return;
|
|
1597
2090
|
}
|
|
1598
|
-
|
|
2091
|
+
|
|
1599
2092
|
// Close the builder
|
|
1600
2093
|
self.pop_overlay();
|
|
1601
|
-
|
|
2094
|
+
|
|
1602
2095
|
// Reset wizard state
|
|
1603
2096
|
self.state.agent_builder = AgentBuilderState::default();
|
|
1604
|
-
|
|
2097
|
+
|
|
1605
2098
|
// Suppress unused variable warning
|
|
1606
2099
|
let _ = agent_data;
|
|
1607
2100
|
}
|
|
1608
|
-
|
|
2101
|
+
|
|
1609
2102
|
// ============================================================
|
|
1610
2103
|
// RUN MANAGER INPUT HANDLING (Step 4.7)
|
|
1611
2104
|
// ============================================================
|
|
1612
|
-
|
|
2105
|
+
|
|
1613
2106
|
/// Handle input when Run Manager screen is active
|
|
1614
2107
|
fn handle_run_manager_input(
|
|
1615
2108
|
&mut self,
|
|
@@ -1617,7 +2110,7 @@ impl App {
|
|
|
1617
2110
|
ws_client: Option<&WebSocketClient>,
|
|
1618
2111
|
) -> anyhow::Result<bool> {
|
|
1619
2112
|
use crossterm::event::KeyModifiers;
|
|
1620
|
-
|
|
2113
|
+
|
|
1621
2114
|
// Ctrl+C/Q to exit
|
|
1622
2115
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
1623
2116
|
match key.code {
|
|
@@ -1625,7 +2118,7 @@ impl App {
|
|
|
1625
2118
|
_ => return Ok(false),
|
|
1626
2119
|
}
|
|
1627
2120
|
}
|
|
1628
|
-
|
|
2121
|
+
|
|
1629
2122
|
match key.code {
|
|
1630
2123
|
// Navigation
|
|
1631
2124
|
KeyCode::Up => {
|
|
@@ -1636,19 +2129,19 @@ impl App {
|
|
|
1636
2129
|
self.state.run_manager.select_next();
|
|
1637
2130
|
self.request_render("run_manager_down");
|
|
1638
2131
|
}
|
|
1639
|
-
|
|
2132
|
+
|
|
1640
2133
|
// Filter
|
|
1641
2134
|
KeyCode::Char('f') | KeyCode::Char('F') => {
|
|
1642
2135
|
self.state.run_manager.next_filter();
|
|
1643
2136
|
self.request_render("run_manager_filter");
|
|
1644
2137
|
}
|
|
1645
|
-
|
|
2138
|
+
|
|
1646
2139
|
// Sort
|
|
1647
2140
|
KeyCode::Char('s') | KeyCode::Char('S') => {
|
|
1648
2141
|
self.state.run_manager.next_sort();
|
|
1649
2142
|
self.request_render("run_manager_sort");
|
|
1650
2143
|
}
|
|
1651
|
-
|
|
2144
|
+
|
|
1652
2145
|
// Refresh
|
|
1653
2146
|
KeyCode::Char('r') | KeyCode::Char('R') => {
|
|
1654
2147
|
if self.state.operation_mode == OperationMode::Connected {
|
|
@@ -1667,7 +2160,7 @@ impl App {
|
|
|
1667
2160
|
}
|
|
1668
2161
|
self.request_render("run_manager_refresh");
|
|
1669
2162
|
}
|
|
1670
|
-
|
|
2163
|
+
|
|
1671
2164
|
// View details / Close detail view
|
|
1672
2165
|
KeyCode::Enter => {
|
|
1673
2166
|
if self.state.run_manager.is_detail_view() {
|
|
@@ -1695,11 +2188,15 @@ impl App {
|
|
|
1695
2188
|
}
|
|
1696
2189
|
self.request_render("run_manager_view");
|
|
1697
2190
|
}
|
|
1698
|
-
|
|
2191
|
+
|
|
1699
2192
|
// Cancel run
|
|
1700
2193
|
KeyCode::Char('c') | KeyCode::Char('C') => {
|
|
1701
2194
|
if let Some(run) = self.state.run_manager.selected_run() {
|
|
1702
|
-
if matches!(
|
|
2195
|
+
if matches!(
|
|
2196
|
+
run.status,
|
|
2197
|
+
crate::ui::run_manager::RunStatus::Running
|
|
2198
|
+
| crate::ui::run_manager::RunStatus::Pending
|
|
2199
|
+
) {
|
|
1703
2200
|
if self.state.operation_mode == OperationMode::Connected {
|
|
1704
2201
|
if let Some(ws) = ws_client {
|
|
1705
2202
|
let data = serde_json::json!({ "runId": run.id.clone() });
|
|
@@ -1719,13 +2216,13 @@ impl App {
|
|
|
1719
2216
|
}
|
|
1720
2217
|
self.request_render("run_manager_cancel");
|
|
1721
2218
|
}
|
|
1722
|
-
|
|
2219
|
+
|
|
1723
2220
|
// Delete run
|
|
1724
2221
|
KeyCode::Char('d') | KeyCode::Char('D') => {
|
|
1725
2222
|
self.add_log("[RUN] Delete run is not supported by the Gateway API (use cancel for active runs).".to_string());
|
|
1726
2223
|
self.request_render("run_manager_delete");
|
|
1727
2224
|
}
|
|
1728
|
-
|
|
2225
|
+
|
|
1729
2226
|
// Close detail view or Run Manager
|
|
1730
2227
|
KeyCode::Esc => {
|
|
1731
2228
|
if self.state.run_manager.is_detail_view() {
|
|
@@ -1738,21 +2235,21 @@ impl App {
|
|
|
1738
2235
|
self.pop_overlay();
|
|
1739
2236
|
}
|
|
1740
2237
|
}
|
|
1741
|
-
|
|
2238
|
+
|
|
1742
2239
|
_ => {}
|
|
1743
2240
|
}
|
|
1744
|
-
|
|
2241
|
+
|
|
1745
2242
|
Ok(false)
|
|
1746
2243
|
}
|
|
1747
|
-
|
|
2244
|
+
|
|
1748
2245
|
// ============================================================
|
|
1749
2246
|
// SETTINGS INPUT HANDLING (Step 4.8)
|
|
1750
2247
|
// ============================================================
|
|
1751
|
-
|
|
2248
|
+
|
|
1752
2249
|
/// Handle input when Settings screen is active
|
|
1753
2250
|
fn handle_settings_input(&mut self, key: KeyEvent) -> anyhow::Result<bool> {
|
|
1754
2251
|
use crossterm::event::KeyModifiers;
|
|
1755
|
-
|
|
2252
|
+
|
|
1756
2253
|
// Ctrl+C/Q to exit
|
|
1757
2254
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
1758
2255
|
match key.code {
|
|
@@ -1760,7 +2257,7 @@ impl App {
|
|
|
1760
2257
|
_ => return Ok(false),
|
|
1761
2258
|
}
|
|
1762
2259
|
}
|
|
1763
|
-
|
|
2260
|
+
|
|
1764
2261
|
match key.code {
|
|
1765
2262
|
// Navigation
|
|
1766
2263
|
KeyCode::Up => {
|
|
@@ -1771,27 +2268,36 @@ impl App {
|
|
|
1771
2268
|
self.state.settings.next_section();
|
|
1772
2269
|
self.request_render("settings_down");
|
|
1773
2270
|
}
|
|
1774
|
-
|
|
2271
|
+
|
|
1775
2272
|
// Toggle current setting
|
|
1776
2273
|
KeyCode::Char(' ') => {
|
|
1777
2274
|
match self.state.settings.focused_section {
|
|
1778
2275
|
crate::ui::settings::SettingsSection::Mode => {
|
|
1779
2276
|
self.state.settings.toggle_mode();
|
|
1780
|
-
self.add_log(format!(
|
|
2277
|
+
self.add_log(format!(
|
|
2278
|
+
"[SETTINGS] Mode: {}",
|
|
2279
|
+
self.state.settings.mode.as_str()
|
|
2280
|
+
));
|
|
1781
2281
|
}
|
|
1782
2282
|
crate::ui::settings::SettingsSection::AIProvider => {
|
|
1783
2283
|
self.state.settings.next_provider();
|
|
1784
|
-
self.add_log(format!(
|
|
2284
|
+
self.add_log(format!(
|
|
2285
|
+
"[SETTINGS] Provider: {}",
|
|
2286
|
+
self.state.settings.ai_provider
|
|
2287
|
+
));
|
|
1785
2288
|
}
|
|
1786
2289
|
crate::ui::settings::SettingsSection::AutoUpdate => {
|
|
1787
2290
|
self.state.settings.toggle_auto_update();
|
|
1788
|
-
self.add_log(format!(
|
|
2291
|
+
self.add_log(format!(
|
|
2292
|
+
"[SETTINGS] Auto-update: {}",
|
|
2293
|
+
self.state.settings.auto_update
|
|
2294
|
+
));
|
|
1789
2295
|
}
|
|
1790
2296
|
_ => {}
|
|
1791
2297
|
}
|
|
1792
2298
|
self.request_render("settings_toggle");
|
|
1793
2299
|
}
|
|
1794
|
-
|
|
2300
|
+
|
|
1795
2301
|
// Save settings
|
|
1796
2302
|
KeyCode::Enter => {
|
|
1797
2303
|
if self.state.settings.has_changes {
|
|
@@ -1801,7 +2307,7 @@ impl App {
|
|
|
1801
2307
|
self.request_render("settings_save");
|
|
1802
2308
|
}
|
|
1803
2309
|
}
|
|
1804
|
-
|
|
2310
|
+
|
|
1805
2311
|
// Close (with discard warning if changes)
|
|
1806
2312
|
KeyCode::Esc => {
|
|
1807
2313
|
if self.state.settings.has_changes {
|
|
@@ -1810,13 +2316,13 @@ impl App {
|
|
|
1810
2316
|
}
|
|
1811
2317
|
self.pop_overlay();
|
|
1812
2318
|
}
|
|
1813
|
-
|
|
2319
|
+
|
|
1814
2320
|
_ => {}
|
|
1815
2321
|
}
|
|
1816
|
-
|
|
2322
|
+
|
|
1817
2323
|
Ok(false)
|
|
1818
2324
|
}
|
|
1819
|
-
|
|
2325
|
+
|
|
1820
2326
|
/// Handle input when Agent List screen is active
|
|
1821
2327
|
fn handle_agent_list_input(
|
|
1822
2328
|
&mut self,
|
|
@@ -1824,7 +2330,7 @@ impl App {
|
|
|
1824
2330
|
ws_client: Option<&WebSocketClient>,
|
|
1825
2331
|
) -> anyhow::Result<bool> {
|
|
1826
2332
|
use crossterm::event::KeyModifiers;
|
|
1827
|
-
|
|
2333
|
+
|
|
1828
2334
|
// Ctrl+C/Q to exit
|
|
1829
2335
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
1830
2336
|
match key.code {
|
|
@@ -1832,7 +2338,7 @@ impl App {
|
|
|
1832
2338
|
_ => return Ok(false),
|
|
1833
2339
|
}
|
|
1834
2340
|
}
|
|
1835
|
-
|
|
2341
|
+
|
|
1836
2342
|
match key.code {
|
|
1837
2343
|
// Navigation
|
|
1838
2344
|
KeyCode::Up => {
|
|
@@ -1848,7 +2354,7 @@ impl App {
|
|
|
1848
2354
|
}
|
|
1849
2355
|
self.request_render("agent_list_down");
|
|
1850
2356
|
}
|
|
1851
|
-
|
|
2357
|
+
|
|
1852
2358
|
// View details / Close detail view
|
|
1853
2359
|
KeyCode::Enter => {
|
|
1854
2360
|
if self.state.agent_list.detail_view.is_some() {
|
|
@@ -1860,7 +2366,7 @@ impl App {
|
|
|
1860
2366
|
}
|
|
1861
2367
|
self.request_render("agent_list_toggle_detail");
|
|
1862
2368
|
}
|
|
1863
|
-
|
|
2369
|
+
|
|
1864
2370
|
// Close
|
|
1865
2371
|
KeyCode::Esc => {
|
|
1866
2372
|
if self.state.agent_list.detail_view.is_some() {
|
|
@@ -1873,24 +2379,30 @@ impl App {
|
|
|
1873
2379
|
self.request_render("agent_list_close");
|
|
1874
2380
|
}
|
|
1875
2381
|
}
|
|
1876
|
-
|
|
2382
|
+
|
|
1877
2383
|
// Delete agent (Step 5.5)
|
|
1878
2384
|
KeyCode::Delete | KeyCode::Char('d') | KeyCode::Char('D') => {
|
|
1879
2385
|
// Only allow deletion in list view (not in detail view)
|
|
1880
|
-
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
|
+
{
|
|
1881
2389
|
self.state.agent_delete_requested = true;
|
|
1882
2390
|
self.request_render("agent_delete_requested");
|
|
1883
2391
|
}
|
|
1884
2392
|
}
|
|
1885
|
-
|
|
2393
|
+
|
|
1886
2394
|
// Step 6: queue a Gateway run (uses built-in `test` agent executor on the server)
|
|
1887
2395
|
KeyCode::Char('x') | KeyCode::Char('X') => {
|
|
1888
2396
|
if self.state.operation_mode != OperationMode::Connected {
|
|
1889
|
-
self.add_log(
|
|
2397
|
+
self.add_log(
|
|
2398
|
+
"[AGENT] Connect to Gateway first to run on the server (key x)."
|
|
2399
|
+
.to_string(),
|
|
2400
|
+
);
|
|
1890
2401
|
} else if self.state.agent_list.agents.is_empty() {
|
|
1891
2402
|
self.add_log("[AGENT] No agents in list.".to_string());
|
|
1892
|
-
} else if let Some((agent_name, agent_model)) =
|
|
1893
|
-
|
|
2403
|
+
} else if let Some((agent_name, agent_model)) = self
|
|
2404
|
+
.get_selected_agent()
|
|
2405
|
+
.map(|a| (a.name.clone(), a.model.clone()))
|
|
1894
2406
|
{
|
|
1895
2407
|
if let Some(ws) = ws_client {
|
|
1896
2408
|
let name = format!("TUI: {}", agent_name);
|
|
@@ -1904,7 +2416,10 @@ impl App {
|
|
|
1904
2416
|
match ws.send_command("run.quick", Some(data)) {
|
|
1905
2417
|
Ok(id) => {
|
|
1906
2418
|
self.state.pending_run_quick_id = Some(id);
|
|
1907
|
-
self.add_log(format!(
|
|
2419
|
+
self.add_log(format!(
|
|
2420
|
+
"[AGENT] Queued Gateway run for profile '{}' (x)",
|
|
2421
|
+
agent_name
|
|
2422
|
+
));
|
|
1908
2423
|
}
|
|
1909
2424
|
Err(e) => self.add_log(format!("[ERROR] run.quick: {}", e)),
|
|
1910
2425
|
}
|
|
@@ -1912,31 +2427,36 @@ impl App {
|
|
|
1912
2427
|
}
|
|
1913
2428
|
self.request_render("agent_list_quick_run");
|
|
1914
2429
|
}
|
|
1915
|
-
|
|
2430
|
+
|
|
1916
2431
|
_ => {}
|
|
1917
2432
|
}
|
|
1918
|
-
|
|
2433
|
+
|
|
1919
2434
|
Ok(false)
|
|
1920
2435
|
}
|
|
1921
|
-
|
|
2436
|
+
|
|
1922
2437
|
/// Delete selected agent (Step 5.5)
|
|
1923
2438
|
pub fn delete_selected_agent(&mut self, ws_client: &Option<crate::websocket::WebSocketClient>) {
|
|
1924
2439
|
if self.state.agent_list.agents.is_empty() {
|
|
1925
2440
|
return;
|
|
1926
2441
|
}
|
|
1927
|
-
|
|
1928
|
-
let agent_name = self.state.agent_list.agents[self.state.agent_list.selected_index]
|
|
1929
|
-
|
|
2442
|
+
|
|
2443
|
+
let agent_name = self.state.agent_list.agents[self.state.agent_list.selected_index]
|
|
2444
|
+
.name
|
|
2445
|
+
.clone();
|
|
2446
|
+
|
|
1930
2447
|
// Send agent.delete command via WebSocket
|
|
1931
2448
|
if let Some(ws) = ws_client {
|
|
1932
2449
|
let delete_data = serde_json::json!({
|
|
1933
2450
|
"name": agent_name
|
|
1934
2451
|
});
|
|
1935
|
-
|
|
2452
|
+
|
|
1936
2453
|
match ws.send_command("agent.delete", Some(delete_data)) {
|
|
1937
2454
|
Ok(id) => {
|
|
1938
2455
|
let short_id = &id[id.len().saturating_sub(8)..];
|
|
1939
|
-
self.add_log(format!(
|
|
2456
|
+
self.add_log(format!(
|
|
2457
|
+
"[AGENT] Sent agent.delete for '{}' (id: {})",
|
|
2458
|
+
agent_name, short_id
|
|
2459
|
+
));
|
|
1940
2460
|
// Store command ID for response tracking
|
|
1941
2461
|
self.state.pending_agent_delete_id = Some(id);
|
|
1942
2462
|
}
|
|
@@ -1948,24 +2468,31 @@ impl App {
|
|
|
1948
2468
|
self.add_log("[ERROR] WebSocket not connected. Cannot delete agent.".to_string());
|
|
1949
2469
|
}
|
|
1950
2470
|
}
|
|
1951
|
-
|
|
2471
|
+
|
|
1952
2472
|
/// Get selected agent (Step 5.6)
|
|
1953
2473
|
pub fn get_selected_agent(&self) -> Option<&AgentInfo> {
|
|
1954
2474
|
if self.state.agent_list.agents.is_empty() {
|
|
1955
2475
|
return None;
|
|
1956
2476
|
}
|
|
1957
|
-
self.state
|
|
2477
|
+
self.state
|
|
2478
|
+
.agent_list
|
|
2479
|
+
.agents
|
|
2480
|
+
.get(self.state.agent_list.selected_index)
|
|
1958
2481
|
}
|
|
1959
|
-
|
|
2482
|
+
|
|
1960
2483
|
/// Get selected agent name (Step 5.6)
|
|
1961
2484
|
pub fn get_selected_agent_name(&self) -> Option<String> {
|
|
1962
2485
|
self.get_selected_agent().map(|agent| agent.name.clone())
|
|
1963
2486
|
}
|
|
1964
|
-
|
|
2487
|
+
|
|
1965
2488
|
/// Handle input when Connection Portal screen is active (Step 7)
|
|
1966
|
-
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> {
|
|
1967
2494
|
use crossterm::event::KeyModifiers;
|
|
1968
|
-
|
|
2495
|
+
|
|
1969
2496
|
// Ctrl+C/Q to exit
|
|
1970
2497
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
1971
2498
|
match key.code {
|
|
@@ -1973,12 +2500,12 @@ impl App {
|
|
|
1973
2500
|
_ => return Ok(false),
|
|
1974
2501
|
}
|
|
1975
2502
|
}
|
|
1976
|
-
|
|
2503
|
+
|
|
1977
2504
|
// Disable input if connecting
|
|
1978
2505
|
if self.state.connection_portal.connecting {
|
|
1979
2506
|
return Ok(false);
|
|
1980
2507
|
}
|
|
1981
|
-
|
|
2508
|
+
|
|
1982
2509
|
match key.code {
|
|
1983
2510
|
// ESC - Close portal and return to Main
|
|
1984
2511
|
KeyCode::Esc => {
|
|
@@ -1999,13 +2526,16 @@ impl App {
|
|
|
1999
2526
|
self.state
|
|
2000
2527
|
.navigation
|
|
2001
2528
|
.navigate_to_base(Screen::PortalMonitoring);
|
|
2529
|
+
self.state.advanced_monitoring =
|
|
2530
|
+
AdvancedMonitoringState::new(self.state.gateway_url.clone());
|
|
2531
|
+
self.state.portal_monitoring.scroll_offset = 0;
|
|
2002
2532
|
self.add_log("[NAV] Opening Portal Monitoring".to_string());
|
|
2003
2533
|
if let Some(ws) = ws_client {
|
|
2004
2534
|
self.begin_portal_observability_request(ws);
|
|
2005
2535
|
}
|
|
2006
2536
|
self.request_immediate_render("open_portal_monitoring");
|
|
2007
2537
|
}
|
|
2008
|
-
|
|
2538
|
+
|
|
2009
2539
|
// L - Continue in local-only mode (no Gateway connection)
|
|
2010
2540
|
KeyCode::Char('l') | KeyCode::Char('L') => {
|
|
2011
2541
|
use crate::app::OperationMode;
|
|
@@ -2020,13 +2550,17 @@ impl App {
|
|
|
2020
2550
|
self.state.pending_gateway_connect_id = None;
|
|
2021
2551
|
self.state.pending_gateway_health_id = None;
|
|
2022
2552
|
self.state.pending_gateway_observability_id = None;
|
|
2553
|
+
self.state.pending_monitoring_refresh_id = None;
|
|
2554
|
+
self.state.pending_monitoring_logs_id = None;
|
|
2555
|
+
self.state.pending_monitoring_drill_id = None;
|
|
2023
2556
|
self.state.portal_monitoring = PortalMonitoringState::default();
|
|
2557
|
+
self.state.advanced_monitoring = AdvancedMonitoringState::default();
|
|
2024
2558
|
self.state.portal_observability_last_poll = None;
|
|
2025
2559
|
self.state.navigation.navigate_to_base(Screen::Main);
|
|
2026
2560
|
self.add_log("[MODE] Continuing in LOCAL mode (no Gateway connection)".to_string());
|
|
2027
2561
|
self.request_immediate_render("portal_local_mode");
|
|
2028
2562
|
}
|
|
2029
|
-
|
|
2563
|
+
|
|
2030
2564
|
// Tab - Switch between URL and Username fields
|
|
2031
2565
|
KeyCode::Tab | KeyCode::BackTab => {
|
|
2032
2566
|
if self.state.connection_portal.connection_success {
|
|
@@ -2035,7 +2569,7 @@ impl App {
|
|
|
2035
2569
|
self.state.connection_portal.toggle_field();
|
|
2036
2570
|
self.request_immediate_render("portal_field_toggle");
|
|
2037
2571
|
}
|
|
2038
|
-
|
|
2572
|
+
|
|
2039
2573
|
// Up/Down - Navigate between fields
|
|
2040
2574
|
KeyCode::Up | KeyCode::Down => {
|
|
2041
2575
|
if self.state.connection_portal.connection_success {
|
|
@@ -2044,7 +2578,7 @@ impl App {
|
|
|
2044
2578
|
self.state.connection_portal.toggle_field();
|
|
2045
2579
|
self.request_immediate_render("portal_navigate");
|
|
2046
2580
|
}
|
|
2047
|
-
|
|
2581
|
+
|
|
2048
2582
|
// Enter - Connect to Gateway
|
|
2049
2583
|
KeyCode::Enter => {
|
|
2050
2584
|
let raw = self.state.connection_portal.gateway_url.clone();
|
|
@@ -2058,49 +2592,56 @@ impl App {
|
|
|
2058
2592
|
|
|
2059
2593
|
// Validate URL
|
|
2060
2594
|
if url.is_empty() {
|
|
2061
|
-
self.state.connection_portal.error =
|
|
2595
|
+
self.state.connection_portal.error =
|
|
2596
|
+
Some("Gateway URL is required".to_string());
|
|
2062
2597
|
self.request_immediate_render("portal_error");
|
|
2063
2598
|
return Ok(false);
|
|
2064
2599
|
}
|
|
2065
|
-
|
|
2600
|
+
|
|
2066
2601
|
if !url.starts_with("http://") && !url.starts_with("https://") {
|
|
2067
|
-
self.state.connection_portal.error =
|
|
2602
|
+
self.state.connection_portal.error =
|
|
2603
|
+
Some("URL must start with http:// or https://".to_string());
|
|
2068
2604
|
self.request_immediate_render("portal_error");
|
|
2069
2605
|
return Ok(false);
|
|
2070
2606
|
}
|
|
2071
|
-
|
|
2607
|
+
|
|
2072
2608
|
// Connect to Gateway
|
|
2073
2609
|
self.state.connection_portal.error = None;
|
|
2074
2610
|
self.state.connection_portal.begin_connecting();
|
|
2075
|
-
|
|
2611
|
+
|
|
2076
2612
|
if let Some(ws) = ws_client {
|
|
2077
2613
|
let connect_data = serde_json::json!({
|
|
2078
2614
|
"url": url,
|
|
2079
2615
|
"username": self.state.connection_portal.username
|
|
2080
2616
|
});
|
|
2081
|
-
|
|
2617
|
+
|
|
2082
2618
|
match ws.send_command("gateway.connect", Some(connect_data)) {
|
|
2083
2619
|
Ok(id) => {
|
|
2084
2620
|
let short_id = &id[id.len().saturating_sub(8)..];
|
|
2085
|
-
self.add_log(format!(
|
|
2621
|
+
self.add_log(format!(
|
|
2622
|
+
"[GATEWAY] Connecting to {} (id: {})",
|
|
2623
|
+
url, short_id
|
|
2624
|
+
));
|
|
2086
2625
|
self.state.pending_gateway_connect_id = Some(id);
|
|
2087
2626
|
}
|
|
2088
2627
|
Err(e) => {
|
|
2089
2628
|
self.state.connection_portal.finish_connecting();
|
|
2090
|
-
self.state.connection_portal.error =
|
|
2629
|
+
self.state.connection_portal.error =
|
|
2630
|
+
Some(format!("Failed to connect: {}", e));
|
|
2091
2631
|
self.add_log(format!("[ERROR] Gateway connection failed: {}", e));
|
|
2092
2632
|
}
|
|
2093
2633
|
}
|
|
2094
2634
|
} else {
|
|
2095
2635
|
self.state.connection_portal.finish_connecting();
|
|
2096
|
-
self.state.connection_portal.error =
|
|
2636
|
+
self.state.connection_portal.error =
|
|
2637
|
+
Some("WebSocket not connected".to_string());
|
|
2097
2638
|
self.add_log("[ERROR] WebSocket not connected".to_string());
|
|
2098
2639
|
}
|
|
2099
|
-
|
|
2640
|
+
|
|
2100
2641
|
// CRITICAL: Use immediate render for instant status update
|
|
2101
2642
|
self.request_immediate_render("portal_connect");
|
|
2102
2643
|
}
|
|
2103
|
-
|
|
2644
|
+
|
|
2104
2645
|
// Typing - edit focused field (immediate render for responsiveness)
|
|
2105
2646
|
KeyCode::Char(c) => {
|
|
2106
2647
|
if self.state.connection_portal.connection_success {
|
|
@@ -2122,7 +2663,7 @@ impl App {
|
|
|
2122
2663
|
}
|
|
2123
2664
|
}
|
|
2124
2665
|
}
|
|
2125
|
-
|
|
2666
|
+
|
|
2126
2667
|
// Backspace - delete character (immediate render for responsiveness)
|
|
2127
2668
|
KeyCode::Backspace => {
|
|
2128
2669
|
if self.state.connection_portal.connection_success {
|
|
@@ -2140,10 +2681,10 @@ impl App {
|
|
|
2140
2681
|
// OPTIMIZATION: Immediate render for instant feedback
|
|
2141
2682
|
self.request_immediate_render("portal_delete");
|
|
2142
2683
|
}
|
|
2143
|
-
|
|
2684
|
+
|
|
2144
2685
|
_ => {}
|
|
2145
2686
|
}
|
|
2146
|
-
|
|
2687
|
+
|
|
2147
2688
|
Ok(false)
|
|
2148
2689
|
}
|
|
2149
2690
|
|
|
@@ -2152,6 +2693,7 @@ impl App {
|
|
|
2152
2693
|
key: KeyEvent,
|
|
2153
2694
|
ws_client: Option<&WebSocketClient>,
|
|
2154
2695
|
) -> anyhow::Result<bool> {
|
|
2696
|
+
use crate::ui::portal_monitoring::portal_monitoring_scroll_max;
|
|
2155
2697
|
use crossterm::event::KeyModifiers;
|
|
2156
2698
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
2157
2699
|
match key.code {
|
|
@@ -2162,19 +2704,67 @@ impl App {
|
|
|
2162
2704
|
if key.code == KeyCode::F(10) {
|
|
2163
2705
|
return Ok(true);
|
|
2164
2706
|
}
|
|
2707
|
+
if key.kind != KeyEventKind::Press {
|
|
2708
|
+
return Ok(false);
|
|
2709
|
+
}
|
|
2710
|
+
if self.state.portal_monitoring.help_visible {
|
|
2711
|
+
if key.code == KeyCode::Esc || key.code == KeyCode::Char('?') {
|
|
2712
|
+
self.state.portal_monitoring.help_visible = false;
|
|
2713
|
+
self.request_immediate_render("portal_help_close");
|
|
2714
|
+
}
|
|
2715
|
+
return Ok(false);
|
|
2716
|
+
}
|
|
2165
2717
|
if key.code == KeyCode::Esc {
|
|
2166
2718
|
self.state.pending_gateway_observability_id = None;
|
|
2719
|
+
self.state.pending_monitoring_refresh_id = None;
|
|
2720
|
+
self.state.pending_monitoring_logs_id = None;
|
|
2721
|
+
self.state.pending_monitoring_drill_id = None;
|
|
2722
|
+
self.state.portal_monitoring.section_refresh_loading = None;
|
|
2723
|
+
self.state.portal_monitoring.logs_fetch_loading = false;
|
|
2724
|
+
self.state.portal_monitoring.metrics_drill = None;
|
|
2725
|
+
self.state.portal_monitoring.metrics_drill_lines.clear();
|
|
2726
|
+
self.state.portal_monitoring.metrics_drill_loading = false;
|
|
2167
2727
|
self.state.navigation.navigate_to_base(Screen::Main);
|
|
2168
2728
|
self.add_log("[NAV] Portal Monitoring closed".to_string());
|
|
2169
2729
|
self.request_immediate_render("portal_monitoring_close");
|
|
2170
2730
|
return Ok(false);
|
|
2171
2731
|
}
|
|
2732
|
+
if key.code == KeyCode::Char('?') {
|
|
2733
|
+
self.state.portal_monitoring.help_visible = true;
|
|
2734
|
+
self.request_immediate_render("portal_help_open");
|
|
2735
|
+
return Ok(false);
|
|
2736
|
+
}
|
|
2737
|
+
// R is context-sensitive: Overview → full `gateway.observability` snapshot; any other
|
|
2738
|
+
// section → `monitoring.refresh` for that section only (cheaper; matches expanded detail).
|
|
2172
2739
|
if key.code == KeyCode::Char('r') || key.code == KeyCode::Char('R') {
|
|
2173
2740
|
if let Some(ws) = ws_client {
|
|
2741
|
+
let sections = MonitoringSection::all();
|
|
2742
|
+
let idx = self
|
|
2743
|
+
.state
|
|
2744
|
+
.advanced_monitoring
|
|
2745
|
+
.monitoring_state
|
|
2746
|
+
.selected_index;
|
|
2747
|
+
let current = sections.get(idx).copied();
|
|
2174
2748
|
self.state.portal_observability_last_poll = None;
|
|
2175
2749
|
self.state.pending_gateway_observability_id = None;
|
|
2750
|
+
self.state.pending_monitoring_refresh_id = None;
|
|
2751
|
+
self.state.pending_monitoring_logs_id = None;
|
|
2752
|
+
self.state.pending_monitoring_drill_id = None;
|
|
2753
|
+
self.state.portal_monitoring.loading = false;
|
|
2754
|
+
self.state.portal_monitoring.section_refresh_loading = None;
|
|
2755
|
+
self.state.portal_monitoring.logs_fetch_loading = false;
|
|
2756
|
+
self.state.portal_monitoring.metrics_drill = None;
|
|
2757
|
+
self.state.portal_monitoring.metrics_drill_lines.clear();
|
|
2758
|
+
self.state.portal_monitoring.metrics_drill_loading = false;
|
|
2176
2759
|
self.state.portal_monitoring.last_refresh = Some(std::time::Instant::now());
|
|
2177
|
-
|
|
2760
|
+
match current {
|
|
2761
|
+
Some(MonitoringSection::Overview) | None => {
|
|
2762
|
+
self.begin_portal_observability_request(ws);
|
|
2763
|
+
}
|
|
2764
|
+
Some(sec) => {
|
|
2765
|
+
self.begin_monitoring_refresh_request(ws, sec);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2178
2768
|
self.request_immediate_render("portal_obs_refresh");
|
|
2179
2769
|
} else {
|
|
2180
2770
|
self.state.portal_monitoring.error =
|
|
@@ -2183,10 +2773,165 @@ impl App {
|
|
|
2183
2773
|
}
|
|
2184
2774
|
return Ok(false);
|
|
2185
2775
|
}
|
|
2776
|
+
if key.code == KeyCode::Char('l') || key.code == KeyCode::Char('L') {
|
|
2777
|
+
if let Some(ws) = ws_client {
|
|
2778
|
+
self.begin_monitoring_logs_request(ws);
|
|
2779
|
+
self.state
|
|
2780
|
+
.advanced_monitoring
|
|
2781
|
+
.monitoring_state
|
|
2782
|
+
.expand_section(MonitoringSection::Logs);
|
|
2783
|
+
self.request_immediate_render("portal_monitoring_logs");
|
|
2784
|
+
} else {
|
|
2785
|
+
self.state.portal_monitoring.error =
|
|
2786
|
+
Some("WebSocket to CLI is not connected.".to_string());
|
|
2787
|
+
self.request_immediate_render("portal_obs_no_ws");
|
|
2788
|
+
}
|
|
2789
|
+
return Ok(false);
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
if key.code == KeyCode::Char('h') || key.code == KeyCode::Char('H') {
|
|
2793
|
+
self.state.portal_monitoring.trend_view = !self.state.portal_monitoring.trend_view;
|
|
2794
|
+
let status = if self.state.portal_monitoring.trend_view {
|
|
2795
|
+
"enabled"
|
|
2796
|
+
} else {
|
|
2797
|
+
"disabled"
|
|
2798
|
+
};
|
|
2799
|
+
self.add_log(format!("[PORTAL] Trend/history view {}", status));
|
|
2800
|
+
self.request_immediate_render("portal_trend_toggle");
|
|
2801
|
+
return Ok(false);
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
if key.code == KeyCode::Char('e') || key.code == KeyCode::Char('E') {
|
|
2805
|
+
match self.export_portal_monitoring_snapshot() {
|
|
2806
|
+
Ok(path) => {
|
|
2807
|
+
self.add_log(format!("[PORTAL] Exported monitoring snapshot: {}", path));
|
|
2808
|
+
self.state.portal_monitoring.error = None;
|
|
2809
|
+
}
|
|
2810
|
+
Err(e) => {
|
|
2811
|
+
self.state.portal_monitoring.error = Some(format!("Export failed: {}", e));
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
self.request_immediate_render("portal_export");
|
|
2815
|
+
return Ok(false);
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// Phase 3: extended dependency view (GET /api/monitoring/dependencies/detail)
|
|
2819
|
+
if key.code == KeyCode::Char('t') || key.code == KeyCode::Char('T') {
|
|
2820
|
+
let sections = MonitoringSection::all();
|
|
2821
|
+
let idx = self
|
|
2822
|
+
.state
|
|
2823
|
+
.advanced_monitoring
|
|
2824
|
+
.monitoring_state
|
|
2825
|
+
.selected_index;
|
|
2826
|
+
let current = sections.get(idx).copied();
|
|
2827
|
+
let expanded = current
|
|
2828
|
+
.and_then(|sec| {
|
|
2829
|
+
self.state
|
|
2830
|
+
.advanced_monitoring
|
|
2831
|
+
.monitoring_state
|
|
2832
|
+
.sections
|
|
2833
|
+
.get(&sec)
|
|
2834
|
+
.map(|s| s.expanded)
|
|
2835
|
+
})
|
|
2836
|
+
.unwrap_or(false);
|
|
2837
|
+
if current == Some(MonitoringSection::Dependencies) && expanded {
|
|
2838
|
+
if let Some(ws) = ws_client {
|
|
2839
|
+
self.begin_dependencies_detail_drill(ws);
|
|
2840
|
+
self.request_immediate_render("portal_deps_drill");
|
|
2841
|
+
} else {
|
|
2842
|
+
self.state.portal_monitoring.error =
|
|
2843
|
+
Some("WebSocket to CLI is not connected.".to_string());
|
|
2844
|
+
self.request_immediate_render("portal_obs_no_ws");
|
|
2845
|
+
}
|
|
2846
|
+
return Ok(false);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// Phase 4: diagnostics for the local CLI/TUI bridge + Gateway reachability.
|
|
2851
|
+
if key.code == KeyCode::Char('s') || key.code == KeyCode::Char('S') {
|
|
2852
|
+
let sections = MonitoringSection::all();
|
|
2853
|
+
let idx = self
|
|
2854
|
+
.state
|
|
2855
|
+
.advanced_monitoring
|
|
2856
|
+
.monitoring_state
|
|
2857
|
+
.selected_index;
|
|
2858
|
+
let current = sections.get(idx).copied();
|
|
2859
|
+
let expanded = current
|
|
2860
|
+
.and_then(|sec| {
|
|
2861
|
+
self.state
|
|
2862
|
+
.advanced_monitoring
|
|
2863
|
+
.monitoring_state
|
|
2864
|
+
.sections
|
|
2865
|
+
.get(&sec)
|
|
2866
|
+
.map(|s| s.expanded)
|
|
2867
|
+
})
|
|
2868
|
+
.unwrap_or(false);
|
|
2869
|
+
if current == Some(MonitoringSection::System) && expanded {
|
|
2870
|
+
if let Some(ws) = ws_client {
|
|
2871
|
+
self.begin_system_diagnostics_request(ws);
|
|
2872
|
+
self.request_immediate_render("portal_system_diagnostics");
|
|
2873
|
+
} else {
|
|
2874
|
+
self.state.portal_monitoring.error =
|
|
2875
|
+
Some("WebSocket to CLI is not connected.".to_string());
|
|
2876
|
+
self.request_immediate_render("portal_obs_no_ws");
|
|
2877
|
+
}
|
|
2878
|
+
return Ok(false);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
if key.code == KeyCode::Right {
|
|
2883
|
+
let sections = MonitoringSection::all();
|
|
2884
|
+
let idx = self
|
|
2885
|
+
.state
|
|
2886
|
+
.advanced_monitoring
|
|
2887
|
+
.monitoring_state
|
|
2888
|
+
.selected_index;
|
|
2889
|
+
let current = sections.get(idx).copied();
|
|
2890
|
+
let expanded = current
|
|
2891
|
+
.and_then(|sec| {
|
|
2892
|
+
self.state
|
|
2893
|
+
.advanced_monitoring
|
|
2894
|
+
.monitoring_state
|
|
2895
|
+
.sections
|
|
2896
|
+
.get(&sec)
|
|
2897
|
+
.map(|s| s.expanded)
|
|
2898
|
+
})
|
|
2899
|
+
.unwrap_or(false);
|
|
2900
|
+
if current == Some(MonitoringSection::Metrics) && expanded {
|
|
2901
|
+
if let Some(ws) = ws_client {
|
|
2902
|
+
let panel = self
|
|
2903
|
+
.state
|
|
2904
|
+
.portal_monitoring
|
|
2905
|
+
.metrics_drill
|
|
2906
|
+
.map(|p| p.next())
|
|
2907
|
+
.unwrap_or(MetricsDrillPanel::Http);
|
|
2908
|
+
self.begin_monitoring_drill_request(ws, panel);
|
|
2909
|
+
self.request_immediate_render("portal_metrics_drill");
|
|
2910
|
+
} else {
|
|
2911
|
+
self.state.portal_monitoring.error =
|
|
2912
|
+
Some("WebSocket to CLI is not connected.".to_string());
|
|
2913
|
+
self.request_immediate_render("portal_obs_no_ws");
|
|
2914
|
+
}
|
|
2915
|
+
return Ok(false);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
if key.code == KeyCode::Left {
|
|
2920
|
+
if self.state.portal_monitoring.metrics_drill.is_some() {
|
|
2921
|
+
self.state.portal_monitoring.metrics_drill = None;
|
|
2922
|
+
self.state.portal_monitoring.metrics_drill_lines.clear();
|
|
2923
|
+
self.state.portal_monitoring.metrics_drill_loading = false;
|
|
2924
|
+
self.request_immediate_render("portal_metrics_drill_exit");
|
|
2925
|
+
return Ok(false);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2186
2929
|
// Toggle auto-refresh with 'A' key
|
|
2187
2930
|
if key.code == KeyCode::Char('a') || key.code == KeyCode::Char('A') {
|
|
2188
|
-
self.state.portal_monitoring.auto_refresh_enabled =
|
|
2931
|
+
self.state.portal_monitoring.auto_refresh_enabled =
|
|
2189
2932
|
!self.state.portal_monitoring.auto_refresh_enabled;
|
|
2933
|
+
self.state.advanced_monitoring.monitoring_state.live_mode =
|
|
2934
|
+
self.state.portal_monitoring.auto_refresh_enabled;
|
|
2190
2935
|
let status = if self.state.portal_monitoring.auto_refresh_enabled {
|
|
2191
2936
|
"enabled"
|
|
2192
2937
|
} else {
|
|
@@ -2196,20 +2941,54 @@ impl App {
|
|
|
2196
2941
|
self.request_immediate_render("portal_obs_auto_toggle");
|
|
2197
2942
|
return Ok(false);
|
|
2198
2943
|
}
|
|
2944
|
+
|
|
2945
|
+
// Phase 1: Enter / Space — expand or collapse selected section
|
|
2946
|
+
if key.code == KeyCode::Enter || key.code == KeyCode::Char(' ') {
|
|
2947
|
+
let idx = self
|
|
2948
|
+
.state
|
|
2949
|
+
.advanced_monitoring
|
|
2950
|
+
.monitoring_state
|
|
2951
|
+
.selected_index;
|
|
2952
|
+
self.state
|
|
2953
|
+
.advanced_monitoring
|
|
2954
|
+
.monitoring_state
|
|
2955
|
+
.toggle_section_at_index(idx);
|
|
2956
|
+
self.request_immediate_render("portal_monitoring_toggle_section");
|
|
2957
|
+
return Ok(false);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2199
2960
|
let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
|
|
2200
|
-
let
|
|
2201
|
-
let max_scroll =
|
|
2961
|
+
let sw = self.state.portal_monitoring.summary_clip_width.max(24);
|
|
2962
|
+
let max_scroll = portal_monitoring_scroll_max(&self.state, sw, inner_h);
|
|
2963
|
+
|
|
2202
2964
|
match key.code {
|
|
2203
|
-
KeyCode::Up
|
|
2965
|
+
KeyCode::Up => {
|
|
2966
|
+
self.state
|
|
2967
|
+
.advanced_monitoring
|
|
2968
|
+
.monitoring_state
|
|
2969
|
+
.select_previous_wrapped();
|
|
2970
|
+
self.state.portal_monitoring.scroll_offset = 0;
|
|
2971
|
+
self.request_immediate_render("portal_monitoring_select");
|
|
2972
|
+
}
|
|
2973
|
+
KeyCode::Down => {
|
|
2974
|
+
self.state
|
|
2975
|
+
.advanced_monitoring
|
|
2976
|
+
.monitoring_state
|
|
2977
|
+
.select_next_wrapped();
|
|
2978
|
+
self.state.portal_monitoring.scroll_offset = 0;
|
|
2979
|
+
self.request_immediate_render("portal_monitoring_select");
|
|
2980
|
+
}
|
|
2981
|
+
KeyCode::PageUp => {
|
|
2982
|
+
let step = inner_h.saturating_sub(3).max(1);
|
|
2204
2983
|
self.state.portal_monitoring.scroll_offset = self
|
|
2205
2984
|
.state
|
|
2206
2985
|
.portal_monitoring
|
|
2207
2986
|
.scroll_offset
|
|
2208
|
-
.saturating_sub(
|
|
2987
|
+
.saturating_sub(step);
|
|
2209
2988
|
self.request_immediate_render("portal_obs_scroll");
|
|
2210
2989
|
}
|
|
2211
|
-
KeyCode::
|
|
2212
|
-
let step =
|
|
2990
|
+
KeyCode::PageDown => {
|
|
2991
|
+
let step = inner_h.saturating_sub(3).max(1);
|
|
2213
2992
|
self.state.portal_monitoring.scroll_offset =
|
|
2214
2993
|
(self.state.portal_monitoring.scroll_offset + step).min(max_scroll);
|
|
2215
2994
|
self.request_immediate_render("portal_obs_scroll");
|
|
@@ -2259,7 +3038,8 @@ impl App {
|
|
|
2259
3038
|
self.state.connection_portal.add_log_entry(
|
|
2260
3039
|
now,
|
|
2261
3040
|
LogLevel::Info,
|
|
2262
|
-
"Type your Gateway URL (https://…), then press Enter to connect."
|
|
3041
|
+
"Type your Gateway URL (https://…), then press Enter to connect."
|
|
3042
|
+
.to_string(),
|
|
2263
3043
|
);
|
|
2264
3044
|
}
|
|
2265
3045
|
_ => {}
|
|
@@ -2269,7 +3049,7 @@ impl App {
|
|
|
2269
3049
|
self.state.connection_portal.add_log_entry(
|
|
2270
3050
|
now,
|
|
2271
3051
|
LogLevel::Info,
|
|
2272
|
-
"Type your Gateway URL, then press Enter to connect.".to_string()
|
|
3052
|
+
"Type your Gateway URL, then press Enter to connect.".to_string(),
|
|
2273
3053
|
);
|
|
2274
3054
|
}
|
|
2275
3055
|
|
|
@@ -2277,12 +3057,16 @@ impl App {
|
|
|
2277
3057
|
.navigation
|
|
2278
3058
|
.navigate_to_base(Screen::ConnectionPortal);
|
|
2279
3059
|
}
|
|
2280
|
-
|
|
3060
|
+
|
|
2281
3061
|
// ============================================================
|
|
2282
3062
|
// SETUP PORTAL INPUT HANDLING
|
|
2283
3063
|
// ============================================================
|
|
2284
|
-
|
|
2285
|
-
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> {
|
|
2286
3070
|
// ESC always closes the portal (even when detecting), so user can always get out
|
|
2287
3071
|
if key.code == KeyCode::Esc {
|
|
2288
3072
|
self.state.setup_portal.detecting = false;
|
|
@@ -2312,18 +3096,18 @@ impl App {
|
|
|
2312
3096
|
self.add_log("[NAV] Opening Connection Portal (F2)...".to_string());
|
|
2313
3097
|
self.request_immediate_render("open_connection_portal");
|
|
2314
3098
|
}
|
|
2315
|
-
|
|
3099
|
+
|
|
2316
3100
|
// Up/Down - Navigate options (immediate render so windowed terminals always redraw)
|
|
2317
3101
|
KeyCode::Up => {
|
|
2318
3102
|
self.state.setup_portal.cycle_option(-1);
|
|
2319
3103
|
self.request_immediate_render("setup_nav_up");
|
|
2320
3104
|
}
|
|
2321
|
-
|
|
3105
|
+
|
|
2322
3106
|
KeyCode::Down => {
|
|
2323
3107
|
self.state.setup_portal.cycle_option(1);
|
|
2324
3108
|
self.request_immediate_render("setup_nav_down");
|
|
2325
3109
|
}
|
|
2326
|
-
|
|
3110
|
+
|
|
2327
3111
|
// Enter - Connection Portal with pre-filled URL and fresh portal state
|
|
2328
3112
|
KeyCode::Enter => {
|
|
2329
3113
|
self.state.setup_portal.error = None;
|
|
@@ -2335,11 +3119,10 @@ impl App {
|
|
|
2335
3119
|
));
|
|
2336
3120
|
self.request_immediate_render("setup_enter");
|
|
2337
3121
|
}
|
|
2338
|
-
|
|
3122
|
+
|
|
2339
3123
|
_ => {}
|
|
2340
3124
|
}
|
|
2341
|
-
|
|
3125
|
+
|
|
2342
3126
|
Ok(false)
|
|
2343
3127
|
}
|
|
2344
3128
|
}
|
|
2345
|
-
|