4runr-os 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mk3-tui/src/app.rs +307 -2
- package/mk3-tui/src/main.rs +123 -20
- package/mk3-tui/src/screens/mod.rs +3 -1
- package/mk3-tui/src/ui/agent_list.rs +2 -0
- package/mk3-tui/src/ui/connection_portal.rs +232 -0
- package/mk3-tui/src/ui/layout.rs +12 -3
- package/mk3-tui/src/ui/mod.rs +1 -0
- package/package.json +2 -2
package/mk3-tui/src/app.rs
CHANGED
|
@@ -29,6 +29,67 @@ pub enum AppMode {
|
|
|
29
29
|
Main,
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
33
|
+
pub enum OperationMode {
|
|
34
|
+
Local, // No Gateway connection
|
|
35
|
+
Connected, // Connected to Gateway
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl OperationMode {
|
|
39
|
+
pub fn as_str(&self) -> &str {
|
|
40
|
+
match self {
|
|
41
|
+
OperationMode::Local => "LOCAL",
|
|
42
|
+
OperationMode::Connected => "CONNECTED",
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
48
|
+
pub enum PortalField {
|
|
49
|
+
GatewayUrl,
|
|
50
|
+
Username,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[derive(Debug, Clone)]
|
|
54
|
+
pub struct ConnectionPortalState {
|
|
55
|
+
pub gateway_url: String,
|
|
56
|
+
pub username: String,
|
|
57
|
+
pub focused_field: PortalField,
|
|
58
|
+
pub connecting: bool,
|
|
59
|
+
pub error: Option<String>,
|
|
60
|
+
pub last_successful_url: Option<String>,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl Default for ConnectionPortalState {
|
|
64
|
+
fn default() -> Self {
|
|
65
|
+
Self {
|
|
66
|
+
gateway_url: "http://localhost:3001".to_string(),
|
|
67
|
+
username: String::new(),
|
|
68
|
+
focused_field: PortalField::GatewayUrl,
|
|
69
|
+
connecting: false,
|
|
70
|
+
error: None,
|
|
71
|
+
last_successful_url: None,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl ConnectionPortalState {
|
|
77
|
+
pub fn reset(&mut self) {
|
|
78
|
+
self.gateway_url = self.last_successful_url.clone().unwrap_or_else(|| "http://localhost:3001".to_string());
|
|
79
|
+
self.username = String::new();
|
|
80
|
+
self.focused_field = PortalField::GatewayUrl;
|
|
81
|
+
self.connecting = false;
|
|
82
|
+
self.error = None;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub fn toggle_field(&mut self) {
|
|
86
|
+
self.focused_field = match self.focused_field {
|
|
87
|
+
PortalField::GatewayUrl => PortalField::Username,
|
|
88
|
+
PortalField::Username => PortalField::GatewayUrl,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
32
93
|
#[derive(Debug, Clone)]
|
|
33
94
|
pub struct AppState {
|
|
34
95
|
// Navigation state (NEW - replaces simple mode)
|
|
@@ -97,6 +158,19 @@ pub struct AppState {
|
|
|
97
158
|
pub run_manager: RunManagerState,
|
|
98
159
|
pub settings: SettingsState,
|
|
99
160
|
pub agent_list: AgentListState,
|
|
161
|
+
pub connection_portal: ConnectionPortalState,
|
|
162
|
+
|
|
163
|
+
// Command tracking for response handling (Step 5.1)
|
|
164
|
+
pub pending_agent_create_id: Option<String>,
|
|
165
|
+
pub pending_agent_list_id: Option<String>,
|
|
166
|
+
pub pending_agent_delete_id: Option<String>,
|
|
167
|
+
pub pending_gateway_connect_id: Option<String>,
|
|
168
|
+
|
|
169
|
+
// Deletion confirmation (Step 5.5)
|
|
170
|
+
pub agent_delete_requested: bool,
|
|
171
|
+
|
|
172
|
+
// Mode tracking (Step 7)
|
|
173
|
+
pub operation_mode: OperationMode,
|
|
100
174
|
|
|
101
175
|
// Local cache
|
|
102
176
|
pub cache: Option<Cache>,
|
|
@@ -128,11 +202,12 @@ impl Default for AppState {
|
|
|
128
202
|
network_status: "Not Connected".to_string(),
|
|
129
203
|
logs: VecDeque::from([
|
|
130
204
|
"[SYSTEM] 4Runr AI Agent OS initialized".to_string(),
|
|
205
|
+
"[MODE] Starting in LOCAL mode (offline)".to_string(),
|
|
131
206
|
"[SHIELD] Safety layer active (enforce mode)".to_string(),
|
|
132
207
|
"[SHIELD] Detectors: PII, Injection, Hallucination".to_string(),
|
|
133
208
|
"[SENTINEL] Real-time monitoring ready".to_string(),
|
|
134
|
-
"[
|
|
135
|
-
"[
|
|
209
|
+
"[HELP] Type 'connect portal' to connect to Gateway server".to_string(),
|
|
210
|
+
"[HELP] Type 'help' for available commands".to_string(),
|
|
136
211
|
]),
|
|
137
212
|
capabilities: vec![
|
|
138
213
|
"text-analysis".to_string(),
|
|
@@ -155,6 +230,13 @@ impl Default for AppState {
|
|
|
155
230
|
run_manager: RunManagerState::default(),
|
|
156
231
|
settings: SettingsState::default(),
|
|
157
232
|
agent_list: AgentListState::default(),
|
|
233
|
+
connection_portal: ConnectionPortalState::default(),
|
|
234
|
+
pending_agent_create_id: None,
|
|
235
|
+
pending_agent_list_id: None,
|
|
236
|
+
pending_agent_delete_id: None,
|
|
237
|
+
pending_gateway_connect_id: None,
|
|
238
|
+
agent_delete_requested: false,
|
|
239
|
+
operation_mode: OperationMode::Local,
|
|
158
240
|
cache: Cache::new().ok(),
|
|
159
241
|
cache_loaded: false,
|
|
160
242
|
}
|
|
@@ -370,6 +452,11 @@ impl App {
|
|
|
370
452
|
return self.handle_settings_input(key);
|
|
371
453
|
}
|
|
372
454
|
|
|
455
|
+
// === CONNECTION PORTAL INPUT HANDLING ===
|
|
456
|
+
if self.state.navigation.current_screen() == &Screen::ConnectionPortal {
|
|
457
|
+
return self.handle_connection_portal_input(key, ws_client);
|
|
458
|
+
}
|
|
459
|
+
|
|
373
460
|
// === AGENT LIST INPUT HANDLING ===
|
|
374
461
|
if self.state.navigation.current_screen() == &Screen::AgentList {
|
|
375
462
|
return self.handle_agent_list_input(key);
|
|
@@ -432,6 +519,9 @@ impl App {
|
|
|
432
519
|
self.state.logs.push_back("".to_string());
|
|
433
520
|
|
|
434
521
|
// Navigation Commands
|
|
522
|
+
self.state.logs.push_back(" disconnect - Disconnect from Gateway (return to LOCAL mode)".to_string());
|
|
523
|
+
self.state.logs.push_back(" connect portal - Open Connection Portal (connect to Gateway)".to_string());
|
|
524
|
+
self.state.logs.push_back(" a, agents - Open Agent List (view, select, delete)".to_string());
|
|
435
525
|
self.state.logs.push_back(" config, settings - Open Settings (mode, AI provider)".to_string());
|
|
436
526
|
self.state.logs.push_back(" runs - Open Run Manager (list, filter, sort)".to_string());
|
|
437
527
|
self.state.logs.push_back(" build - Open Agent Builder (6-step wizard)".to_string());
|
|
@@ -488,6 +578,38 @@ impl App {
|
|
|
488
578
|
self.push_overlay(Screen::Settings);
|
|
489
579
|
self.state.logs.push_back("[NAV] Opening Settings...".into());
|
|
490
580
|
}
|
|
581
|
+
"a" | "agents" | "agent list" => {
|
|
582
|
+
self.push_overlay(Screen::AgentList);
|
|
583
|
+
self.state.logs.push_back("[NAV] Opening Agent List...".into());
|
|
584
|
+
// Request agent list from backend
|
|
585
|
+
if let Some(ws) = ws_client {
|
|
586
|
+
if let Ok(list_id) = ws.send_command("agent.list", None) {
|
|
587
|
+
self.state.pending_agent_list_id = Some(list_id);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
"connect portal" | "portal" => {
|
|
592
|
+
self.push_overlay(Screen::ConnectionPortal);
|
|
593
|
+
self.state.connection_portal.reset();
|
|
594
|
+
self.state.logs.push_back("[NAV] Opening Connection Portal...".into());
|
|
595
|
+
}
|
|
596
|
+
"disconnect" => {
|
|
597
|
+
// Disconnect from Gateway
|
|
598
|
+
if let Some(ws) = ws_client {
|
|
599
|
+
match ws.send_command("gateway.disconnect", None) {
|
|
600
|
+
Ok(id) => {
|
|
601
|
+
let short_id = &id[id.len().saturating_sub(8)..];
|
|
602
|
+
self.state.logs.push_back(format!("[GATEWAY] Disconnecting (id: {})", short_id));
|
|
603
|
+
// Mode will be updated when response is received
|
|
604
|
+
}
|
|
605
|
+
Err(e) => {
|
|
606
|
+
self.state.logs.push_back(format!("[ERROR] Failed to disconnect: {}", e));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
self.state.logs.push_back("[ERROR] WebSocket not connected".into());
|
|
611
|
+
}
|
|
612
|
+
}
|
|
491
613
|
_ => {
|
|
492
614
|
// Try to parse as WebSocket command
|
|
493
615
|
if let Some(ws) = ws_client {
|
|
@@ -636,6 +758,10 @@ impl App {
|
|
|
636
758
|
use crate::ui::agent_list;
|
|
637
759
|
agent_list::render(f, &self.state);
|
|
638
760
|
}
|
|
761
|
+
Screen::ConnectionPortal => {
|
|
762
|
+
use crate::ui::connection_portal;
|
|
763
|
+
connection_portal::render(f, &self.state);
|
|
764
|
+
}
|
|
639
765
|
Screen::Confirmation { message, action } => {
|
|
640
766
|
// TODO: Implement confirmation popup
|
|
641
767
|
// For now, just render the base screen
|
|
@@ -1024,6 +1150,8 @@ impl App {
|
|
|
1024
1150
|
Ok(id) => {
|
|
1025
1151
|
let short_id = &id[id.len().saturating_sub(8)..];
|
|
1026
1152
|
self.add_log(format!("[AGENT] Sent agent.create (id: {})", short_id));
|
|
1153
|
+
// Store command ID for response tracking (Step 5.1)
|
|
1154
|
+
self.state.pending_agent_create_id = Some(id);
|
|
1027
1155
|
}
|
|
1028
1156
|
Err(e) => {
|
|
1029
1157
|
self.add_log(format!("[ERROR] Failed to create agent: {}", e));
|
|
@@ -1275,6 +1403,183 @@ impl App {
|
|
|
1275
1403
|
}
|
|
1276
1404
|
}
|
|
1277
1405
|
|
|
1406
|
+
// Delete agent (Step 5.5)
|
|
1407
|
+
KeyCode::Delete | KeyCode::Char('d') | KeyCode::Char('D') => {
|
|
1408
|
+
// Only allow deletion in list view (not in detail view)
|
|
1409
|
+
if self.state.agent_list.detail_view.is_none() && !self.state.agent_list.agents.is_empty() {
|
|
1410
|
+
self.state.agent_delete_requested = true;
|
|
1411
|
+
self.request_render("agent_delete_requested");
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
_ => {}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
Ok(false)
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/// Delete selected agent (Step 5.5)
|
|
1422
|
+
pub fn delete_selected_agent(&mut self, ws_client: &Option<crate::websocket::WebSocketClient>) {
|
|
1423
|
+
if self.state.agent_list.agents.is_empty() {
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
let agent_name = self.state.agent_list.agents[self.state.agent_list.selected_index].name.clone();
|
|
1428
|
+
|
|
1429
|
+
// Send agent.delete command via WebSocket
|
|
1430
|
+
if let Some(ws) = ws_client {
|
|
1431
|
+
let delete_data = serde_json::json!({
|
|
1432
|
+
"name": agent_name
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
match ws.send_command("agent.delete", Some(delete_data)) {
|
|
1436
|
+
Ok(id) => {
|
|
1437
|
+
let short_id = &id[id.len().saturating_sub(8)..];
|
|
1438
|
+
self.add_log(format!("[AGENT] Sent agent.delete for '{}' (id: {})", agent_name, short_id));
|
|
1439
|
+
// Store command ID for response tracking
|
|
1440
|
+
self.state.pending_agent_delete_id = Some(id);
|
|
1441
|
+
}
|
|
1442
|
+
Err(e) => {
|
|
1443
|
+
self.add_log(format!("[ERROR] Failed to delete agent: {}", e));
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
} else {
|
|
1447
|
+
self.add_log("[ERROR] WebSocket not connected. Cannot delete agent.".to_string());
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/// Get selected agent (Step 5.6)
|
|
1452
|
+
pub fn get_selected_agent(&self) -> Option<&AgentInfo> {
|
|
1453
|
+
if self.state.agent_list.agents.is_empty() {
|
|
1454
|
+
return None;
|
|
1455
|
+
}
|
|
1456
|
+
self.state.agent_list.agents.get(self.state.agent_list.selected_index)
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/// Get selected agent name (Step 5.6)
|
|
1460
|
+
pub fn get_selected_agent_name(&self) -> Option<String> {
|
|
1461
|
+
self.get_selected_agent().map(|agent| agent.name.clone())
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/// Handle input when Connection Portal screen is active (Step 7)
|
|
1465
|
+
fn handle_connection_portal_input(&mut self, key: KeyEvent, ws_client: Option<&WebSocketClient>) -> anyhow::Result<bool> {
|
|
1466
|
+
use crossterm::event::KeyModifiers;
|
|
1467
|
+
|
|
1468
|
+
// Ctrl+C/Q to exit
|
|
1469
|
+
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
1470
|
+
match key.code {
|
|
1471
|
+
KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
|
|
1472
|
+
_ => return Ok(false),
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// Disable input if connecting
|
|
1477
|
+
if self.state.connection_portal.connecting {
|
|
1478
|
+
return Ok(false);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
match key.code {
|
|
1482
|
+
// ESC - Cancel and close portal
|
|
1483
|
+
KeyCode::Esc => {
|
|
1484
|
+
self.pop_overlay();
|
|
1485
|
+
self.add_log("[NAV] Connection Portal closed".to_string());
|
|
1486
|
+
self.request_render("portal_close");
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Tab - Switch between fields
|
|
1490
|
+
KeyCode::Tab | KeyCode::BackTab => {
|
|
1491
|
+
self.state.connection_portal.toggle_field();
|
|
1492
|
+
self.request_render("portal_field_toggle");
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Enter - Connect to Gateway
|
|
1496
|
+
KeyCode::Enter => {
|
|
1497
|
+
let url = self.state.connection_portal.gateway_url.clone();
|
|
1498
|
+
let username = if self.state.connection_portal.username.is_empty() {
|
|
1499
|
+
None
|
|
1500
|
+
} else {
|
|
1501
|
+
Some(self.state.connection_portal.username.clone())
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
// Validate URL format
|
|
1505
|
+
if url.is_empty() {
|
|
1506
|
+
self.state.connection_portal.error = Some("Gateway URL is required".to_string());
|
|
1507
|
+
self.request_render("portal_error");
|
|
1508
|
+
return Ok(false);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if !url.starts_with("http://") && !url.starts_with("https://") {
|
|
1512
|
+
self.state.connection_portal.error = Some("URL must start with http:// or https://".to_string());
|
|
1513
|
+
self.request_render("portal_error");
|
|
1514
|
+
return Ok(false);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Set connecting state
|
|
1518
|
+
self.state.connection_portal.connecting = true;
|
|
1519
|
+
self.state.connection_portal.error = None;
|
|
1520
|
+
|
|
1521
|
+
// Send gateway.connect command
|
|
1522
|
+
if let Some(ws) = ws_client {
|
|
1523
|
+
let connect_data = if let Some(username) = username {
|
|
1524
|
+
serde_json::json!({
|
|
1525
|
+
"url": url,
|
|
1526
|
+
"username": username
|
|
1527
|
+
})
|
|
1528
|
+
} else {
|
|
1529
|
+
serde_json::json!({
|
|
1530
|
+
"url": url
|
|
1531
|
+
})
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
match ws.send_command("gateway.connect", Some(connect_data)) {
|
|
1535
|
+
Ok(id) => {
|
|
1536
|
+
let short_id = &id[id.len().saturating_sub(8)..];
|
|
1537
|
+
self.add_log(format!("[GATEWAY] Connecting to {} (id: {})", url, short_id));
|
|
1538
|
+
self.state.pending_gateway_connect_id = Some(id);
|
|
1539
|
+
}
|
|
1540
|
+
Err(e) => {
|
|
1541
|
+
self.state.connection_portal.connecting = false;
|
|
1542
|
+
self.state.connection_portal.error = Some(format!("Failed to send command: {}", e));
|
|
1543
|
+
self.add_log(format!("[ERROR] Gateway connection failed: {}", e));
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
} else {
|
|
1547
|
+
self.state.connection_portal.connecting = false;
|
|
1548
|
+
self.state.connection_portal.error = Some("WebSocket not connected".to_string());
|
|
1549
|
+
self.add_log("[ERROR] WebSocket not connected. Cannot connect to Gateway.".to_string());
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
self.request_render("portal_connect");
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// Typing - edit focused field
|
|
1556
|
+
KeyCode::Char(c) => {
|
|
1557
|
+
match self.state.connection_portal.focused_field {
|
|
1558
|
+
PortalField::GatewayUrl => {
|
|
1559
|
+
self.state.connection_portal.gateway_url.push(c);
|
|
1560
|
+
}
|
|
1561
|
+
PortalField::Username => {
|
|
1562
|
+
self.state.connection_portal.username.push(c);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
self.state.connection_portal.error = None;
|
|
1566
|
+
self.request_render("portal_input");
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Backspace - delete character
|
|
1570
|
+
KeyCode::Backspace => {
|
|
1571
|
+
match self.state.connection_portal.focused_field {
|
|
1572
|
+
PortalField::GatewayUrl => {
|
|
1573
|
+
self.state.connection_portal.gateway_url.pop();
|
|
1574
|
+
}
|
|
1575
|
+
PortalField::Username => {
|
|
1576
|
+
self.state.connection_portal.username.pop();
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
self.state.connection_portal.error = None;
|
|
1580
|
+
self.request_render("portal_delete");
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1278
1583
|
_ => {}
|
|
1279
1584
|
}
|
|
1280
1585
|
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -192,6 +192,11 @@ fn main() -> Result<()> {
|
|
|
192
192
|
app.state.agent_list.selected_index = 0;
|
|
193
193
|
app.state.agent_list.detail_view = None;
|
|
194
194
|
|
|
195
|
+
// Clear pending agent.list ID if this response matches (Step 5.2)
|
|
196
|
+
if Some(&resp.id) == app.state.pending_agent_list_id.as_ref() {
|
|
197
|
+
app.state.pending_agent_list_id = None;
|
|
198
|
+
}
|
|
199
|
+
|
|
195
200
|
// Only open AgentList overlay if we're on Main screen (not during boot)
|
|
196
201
|
use crate::screens::Screen;
|
|
197
202
|
if app.state.navigation.current_screen() == &Screen::Main {
|
|
@@ -199,6 +204,78 @@ fn main() -> Result<()> {
|
|
|
199
204
|
app.request_render("agent_list_opened");
|
|
200
205
|
}
|
|
201
206
|
}
|
|
207
|
+
}
|
|
208
|
+
// Handle agent.create response (Step 5.1)
|
|
209
|
+
else if let Some(agent) = obj.get("agent") {
|
|
210
|
+
// Check if this is a response to our agent.create command
|
|
211
|
+
if Some(&resp.id) == app.state.pending_agent_create_id.as_ref() {
|
|
212
|
+
app.state.pending_agent_create_id = None;
|
|
213
|
+
|
|
214
|
+
// Extract agent name for success message
|
|
215
|
+
let agent_name = agent.as_object()
|
|
216
|
+
.and_then(|a| a.get("name"))
|
|
217
|
+
.and_then(|n| n.as_str())
|
|
218
|
+
.unwrap_or("unknown");
|
|
219
|
+
|
|
220
|
+
app.add_log(format!("✓ [{}] Agent '{}' created successfully", short_id, agent_name));
|
|
221
|
+
|
|
222
|
+
// Refresh agent list (Step 5.3)
|
|
223
|
+
if let Some(ref ws) = ws_client {
|
|
224
|
+
if let Ok(list_id) = ws.send_command("agent.list", None) {
|
|
225
|
+
app.state.pending_agent_list_id = Some(list_id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
app.add_log(format!("✓ [{}] Agent response received", short_id));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Handle agent.delete response (Step 5.5)
|
|
233
|
+
else if let Some(deleted) = obj.get("deleted") {
|
|
234
|
+
if deleted.as_bool().unwrap_or(false) {
|
|
235
|
+
if Some(&resp.id) == app.state.pending_agent_delete_id.as_ref() {
|
|
236
|
+
app.state.pending_agent_delete_id = None;
|
|
237
|
+
app.add_log(format!("✓ [{}] Agent deleted successfully", short_id));
|
|
238
|
+
|
|
239
|
+
// Refresh agent list (Step 5.5)
|
|
240
|
+
if let Some(ref ws) = ws_client {
|
|
241
|
+
if let Ok(list_id) = ws.send_command("agent.list", None) {
|
|
242
|
+
app.state.pending_agent_list_id = Some(list_id);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Handle gateway.connect response (Step 7.5)
|
|
249
|
+
else if let Some(connected) = obj.get("connected") {
|
|
250
|
+
if connected.as_bool().unwrap_or(false) {
|
|
251
|
+
if Some(&resp.id) == app.state.pending_gateway_connect_id.as_ref() {
|
|
252
|
+
app.state.pending_gateway_connect_id = None;
|
|
253
|
+
|
|
254
|
+
use crate::app::OperationMode;
|
|
255
|
+
app.state.operation_mode = OperationMode::Connected;
|
|
256
|
+
|
|
257
|
+
let url = obj.get("url")
|
|
258
|
+
.and_then(|v| v.as_str())
|
|
259
|
+
.unwrap_or("unknown");
|
|
260
|
+
|
|
261
|
+
app.state.gateway_url = Some(url.to_string());
|
|
262
|
+
app.state.connection_portal.last_successful_url = Some(url.to_string());
|
|
263
|
+
app.state.connection_portal.connecting = false;
|
|
264
|
+
|
|
265
|
+
app.add_log(format!("✓ [{}] Connected to Gateway: {}", short_id, url));
|
|
266
|
+
app.add_log(format!("[MODE] Switched to CONNECTED mode"));
|
|
267
|
+
|
|
268
|
+
// Close portal
|
|
269
|
+
app.pop_overlay();
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
// Handle disconnect response (Step 7.8)
|
|
273
|
+
use crate::app::OperationMode;
|
|
274
|
+
app.state.operation_mode = OperationMode::Local;
|
|
275
|
+
app.state.gateway_url = None;
|
|
276
|
+
app.add_log(format!("✓ [{}] Disconnected from Gateway", short_id));
|
|
277
|
+
app.add_log(format!("[MODE] Switched to LOCAL mode"));
|
|
278
|
+
}
|
|
202
279
|
} else {
|
|
203
280
|
app.add_log(format!("✓ [{}] Success", short_id));
|
|
204
281
|
}
|
|
@@ -212,25 +289,45 @@ fn main() -> Result<()> {
|
|
|
212
289
|
let short_id = &resp.id[resp.id.len().saturating_sub(8)..];
|
|
213
290
|
let error_msg = resp.payload.error.unwrap_or_else(|| "Unknown error".to_string());
|
|
214
291
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
short_id,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
292
|
+
// Check if this is a response to agent.create command (Step 5.1)
|
|
293
|
+
if Some(&resp.id) == app.state.pending_agent_create_id.as_ref() {
|
|
294
|
+
app.state.pending_agent_create_id = None;
|
|
295
|
+
app.add_log(format!("✗ [{}] Agent creation failed: {}", short_id, error_msg));
|
|
296
|
+
}
|
|
297
|
+
// Check if this is a response to agent.delete command (Step 5.5)
|
|
298
|
+
else if Some(&resp.id) == app.state.pending_agent_delete_id.as_ref() {
|
|
299
|
+
app.state.pending_agent_delete_id = None;
|
|
300
|
+
app.add_log(format!("✗ [{}] Agent deletion failed: {}", short_id, error_msg));
|
|
301
|
+
}
|
|
302
|
+
// Check if this is a response to gateway.connect command (Step 7.5)
|
|
303
|
+
else if Some(&resp.id) == app.state.pending_gateway_connect_id.as_ref() {
|
|
304
|
+
app.state.pending_gateway_connect_id = None;
|
|
305
|
+
app.state.connection_portal.connecting = false;
|
|
306
|
+
app.state.connection_portal.error = Some(error_msg.clone());
|
|
307
|
+
app.add_log(format!("✗ [{}] Gateway connection failed: {}", short_id, error_msg));
|
|
308
|
+
}
|
|
309
|
+
// Generic error handling
|
|
310
|
+
else {
|
|
311
|
+
// Try to extract command name from response for better error messages
|
|
312
|
+
let command_hint = if resp.id.contains("agent") {
|
|
313
|
+
" (agent command)"
|
|
314
|
+
} else if resp.id.contains("system") {
|
|
315
|
+
" (system command)"
|
|
316
|
+
} else if resp.id.contains("run") {
|
|
317
|
+
" (run command)"
|
|
318
|
+
} else if resp.id.contains("tool") {
|
|
319
|
+
" (tool command)"
|
|
320
|
+
} else {
|
|
321
|
+
""
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
app.add_log(format!(
|
|
325
|
+
"✗ [{}]{} Error: {}",
|
|
326
|
+
short_id,
|
|
327
|
+
command_hint,
|
|
328
|
+
error_msg
|
|
329
|
+
));
|
|
330
|
+
}
|
|
234
331
|
}
|
|
235
332
|
}
|
|
236
333
|
WsClientMessage::Event(event) => {
|
|
@@ -279,7 +376,7 @@ fn main() -> Result<()> {
|
|
|
279
376
|
// └─────────────────────────────────────────────────────────┘
|
|
280
377
|
// poll() with short timeout (16ms) - returns immediately if no input
|
|
281
378
|
// This allows loop to run ~60 times/second for smooth animations
|
|
282
|
-
|
|
379
|
+
if crossterm::event::poll(Duration::from_millis(16))? {
|
|
283
380
|
if let Event::Key(key) = event::read()? {
|
|
284
381
|
if key.kind == KeyEventKind::Press {
|
|
285
382
|
if app.handle_input(key, &mut io_handler, ws_client.as_ref())? {
|
|
@@ -290,6 +387,12 @@ fn main() -> Result<()> {
|
|
|
290
387
|
}
|
|
291
388
|
// If no input, loop continues immediately - animations keep running!
|
|
292
389
|
|
|
390
|
+
// Check for pending agent deletion (Step 5.5)
|
|
391
|
+
if app.state.agent_delete_requested {
|
|
392
|
+
app.state.agent_delete_requested = false;
|
|
393
|
+
app.delete_selected_agent(&ws_client);
|
|
394
|
+
}
|
|
395
|
+
|
|
293
396
|
// Check for IO updates (non-blocking)
|
|
294
397
|
io_handler.update(&mut app).ok();
|
|
295
398
|
}
|
|
@@ -20,6 +20,7 @@ pub enum Screen {
|
|
|
20
20
|
RunManager,
|
|
21
21
|
Settings,
|
|
22
22
|
AgentList,
|
|
23
|
+
ConnectionPortal,
|
|
23
24
|
|
|
24
25
|
// Popup screens (small overlays)
|
|
25
26
|
Confirmation { message: String, action: String },
|
|
@@ -31,7 +32,7 @@ impl Screen {
|
|
|
31
32
|
pub fn is_overlay(&self) -> bool {
|
|
32
33
|
matches!(
|
|
33
34
|
self,
|
|
34
|
-
Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
|
|
35
|
+
Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList | Screen::ConnectionPortal
|
|
35
36
|
)
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -58,6 +59,7 @@ impl Screen {
|
|
|
58
59
|
Screen::RunManager => "Run Manager",
|
|
59
60
|
Screen::Settings => "Settings",
|
|
60
61
|
Screen::AgentList => "Agent List",
|
|
62
|
+
Screen::ConnectionPortal => "Connection Portal",
|
|
61
63
|
Screen::Confirmation { .. } => "Confirmation",
|
|
62
64
|
Screen::Alert { .. } => "Alert",
|
|
63
65
|
}
|
|
@@ -120,6 +120,8 @@ fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
|
|
|
120
120
|
Span::styled(" Navigate | ", Style::default().fg(TEXT_DIM)),
|
|
121
121
|
Span::styled("Enter", Style::default().fg(NEON_GREEN)),
|
|
122
122
|
Span::styled(" View Details | ", Style::default().fg(TEXT_DIM)),
|
|
123
|
+
Span::styled("D/Del", Style::default().fg(Color::Rgb(255, 100, 100))),
|
|
124
|
+
Span::styled(" Delete | ", Style::default().fg(TEXT_DIM)),
|
|
123
125
|
Span::styled("ESC", Style::default().fg(BRAND_PURPLE)),
|
|
124
126
|
Span::styled(" Close", Style::default().fg(TEXT_DIM)),
|
|
125
127
|
]);
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/// Connection Portal Screen
|
|
2
|
+
/// UI for connecting to Gateway server and switching to Connected mode
|
|
3
|
+
|
|
4
|
+
use ratatui::prelude::*;
|
|
5
|
+
use ratatui::widgets::{Block, Borders, Paragraph, Wrap, Clear};
|
|
6
|
+
use crate::app::{AppState, PortalField};
|
|
7
|
+
|
|
8
|
+
// === 4RUNR BRAND COLORS (matching layout.rs) ===
|
|
9
|
+
const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
|
|
10
|
+
const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
|
|
11
|
+
const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
|
|
12
|
+
const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
|
|
13
|
+
const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
|
|
14
|
+
const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
|
|
15
|
+
const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
|
|
16
|
+
const BG_PANEL: Color = Color::Rgb(18, 18, 25);
|
|
17
|
+
const ERROR_RED: Color = Color::Rgb(255, 69, 69);
|
|
18
|
+
|
|
19
|
+
/// Render the Connection Portal screen (full-screen overlay)
|
|
20
|
+
pub fn render(f: &mut Frame, state: &AppState) {
|
|
21
|
+
let area = f.size();
|
|
22
|
+
|
|
23
|
+
// Clear the entire screen for modal effect
|
|
24
|
+
f.render_widget(Clear, area);
|
|
25
|
+
|
|
26
|
+
// Center the portal dialog
|
|
27
|
+
let portal_width = area.width.min(70);
|
|
28
|
+
let portal_height = area.height.min(25);
|
|
29
|
+
let portal_x = (area.width.saturating_sub(portal_width)) / 2;
|
|
30
|
+
let portal_y = (area.height.saturating_sub(portal_height)) / 2;
|
|
31
|
+
|
|
32
|
+
let portal_area = Rect {
|
|
33
|
+
x: portal_x,
|
|
34
|
+
y: portal_y,
|
|
35
|
+
width: portal_width,
|
|
36
|
+
height: portal_height,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Split portal into sections
|
|
40
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
41
|
+
|
|
42
|
+
let chunks = Layout::default()
|
|
43
|
+
.direction(Direction::Vertical)
|
|
44
|
+
.constraints([
|
|
45
|
+
Constraint::Length(3), // Header
|
|
46
|
+
Constraint::Length(4), // Description
|
|
47
|
+
Constraint::Length(7), // Input fields
|
|
48
|
+
Constraint::Length(3), // Status
|
|
49
|
+
Constraint::Min(1), // Spacer
|
|
50
|
+
Constraint::Length(3), // Actions
|
|
51
|
+
])
|
|
52
|
+
.split(portal_area);
|
|
53
|
+
|
|
54
|
+
render_header(f, chunks[0], state);
|
|
55
|
+
render_description(f, chunks[1]);
|
|
56
|
+
render_input_fields(f, chunks[2], state);
|
|
57
|
+
render_status(f, chunks[3], state);
|
|
58
|
+
render_actions(f, chunks[5], state);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn render_header(f: &mut Frame, area: Rect, state: &AppState) {
|
|
62
|
+
let title = if state.connection_portal.connecting {
|
|
63
|
+
" 🔌 Connection Portal - Connecting... "
|
|
64
|
+
} else {
|
|
65
|
+
" 🔌 Connection Portal "
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let block = Block::default()
|
|
69
|
+
.title(title)
|
|
70
|
+
.borders(Borders::ALL)
|
|
71
|
+
.border_style(Style::default().fg(if state.connection_portal.connecting {
|
|
72
|
+
AMBER_WARN
|
|
73
|
+
} else if state.connection_portal.error.is_some() {
|
|
74
|
+
ERROR_RED
|
|
75
|
+
} else {
|
|
76
|
+
BRAND_PURPLE
|
|
77
|
+
}))
|
|
78
|
+
.style(Style::default().bg(BG_PANEL));
|
|
79
|
+
|
|
80
|
+
f.render_widget(block, area);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn render_description(f: &mut Frame, area: Rect) {
|
|
84
|
+
let block = Block::default()
|
|
85
|
+
.borders(Borders::LEFT | Borders::RIGHT)
|
|
86
|
+
.style(Style::default().bg(BG_PANEL));
|
|
87
|
+
|
|
88
|
+
let inner = block.inner(area);
|
|
89
|
+
f.render_widget(block, area);
|
|
90
|
+
|
|
91
|
+
let text = vec![
|
|
92
|
+
Line::from(Span::styled(
|
|
93
|
+
"Connect to Gateway Server to enable full OS features including",
|
|
94
|
+
Style::default().fg(TEXT_PRIMARY)
|
|
95
|
+
)),
|
|
96
|
+
Line::from(Span::styled(
|
|
97
|
+
"Shield, Sentinel, and server tools.",
|
|
98
|
+
Style::default().fg(TEXT_PRIMARY)
|
|
99
|
+
)),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
let paragraph = Paragraph::new(text)
|
|
103
|
+
.wrap(Wrap { trim: false })
|
|
104
|
+
.alignment(Alignment::Center);
|
|
105
|
+
|
|
106
|
+
f.render_widget(paragraph, inner);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn render_input_fields(f: &mut Frame, area: Rect, state: &AppState) {
|
|
110
|
+
let block = Block::default()
|
|
111
|
+
.borders(Borders::LEFT | Borders::RIGHT)
|
|
112
|
+
.style(Style::default().bg(BG_PANEL));
|
|
113
|
+
|
|
114
|
+
let inner = block.inner(area);
|
|
115
|
+
f.render_widget(block, area);
|
|
116
|
+
|
|
117
|
+
let portal = &state.connection_portal;
|
|
118
|
+
let url_focused = portal.focused_field == PortalField::GatewayUrl;
|
|
119
|
+
let username_focused = portal.focused_field == PortalField::Username;
|
|
120
|
+
|
|
121
|
+
// Disable input if connecting
|
|
122
|
+
let input_enabled = !portal.connecting;
|
|
123
|
+
|
|
124
|
+
let text = vec![
|
|
125
|
+
Line::from(""),
|
|
126
|
+
// Gateway URL field
|
|
127
|
+
Line::from(vec![
|
|
128
|
+
Span::styled(" ", Style::default()),
|
|
129
|
+
Span::styled(
|
|
130
|
+
if url_focused && input_enabled { "▶ " } else { " " },
|
|
131
|
+
Style::default().fg(BRAND_PURPLE).bold()
|
|
132
|
+
),
|
|
133
|
+
Span::styled("Gateway URL:", Style::default().fg(TEXT_DIM)),
|
|
134
|
+
]),
|
|
135
|
+
Line::from(vec![
|
|
136
|
+
Span::styled(" ", Style::default()),
|
|
137
|
+
Span::styled(
|
|
138
|
+
format!("[{}]", portal.gateway_url),
|
|
139
|
+
Style::default()
|
|
140
|
+
.fg(if url_focused && input_enabled { CYBER_CYAN } else { TEXT_PRIMARY })
|
|
141
|
+
.bold()
|
|
142
|
+
),
|
|
143
|
+
]),
|
|
144
|
+
Line::from(""),
|
|
145
|
+
// Username field
|
|
146
|
+
Line::from(vec![
|
|
147
|
+
Span::styled(" ", Style::default()),
|
|
148
|
+
Span::styled(
|
|
149
|
+
if username_focused && input_enabled { "▶ " } else { " " },
|
|
150
|
+
Style::default().fg(BRAND_PURPLE).bold()
|
|
151
|
+
),
|
|
152
|
+
Span::styled("Username (optional):", Style::default().fg(TEXT_DIM)),
|
|
153
|
+
]),
|
|
154
|
+
Line::from(vec![
|
|
155
|
+
Span::styled(" ", Style::default()),
|
|
156
|
+
Span::styled(
|
|
157
|
+
format!("[{}]", if portal.username.is_empty() { " ".to_string() } else { portal.username.clone() }),
|
|
158
|
+
Style::default()
|
|
159
|
+
.fg(if username_focused && input_enabled { CYBER_CYAN } else { TEXT_MUTED })
|
|
160
|
+
),
|
|
161
|
+
]),
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
let paragraph = Paragraph::new(text)
|
|
165
|
+
.wrap(Wrap { trim: false })
|
|
166
|
+
.alignment(Alignment::Left);
|
|
167
|
+
|
|
168
|
+
f.render_widget(paragraph, inner);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn render_status(f: &mut Frame, area: Rect, state: &AppState) {
|
|
172
|
+
let block = Block::default()
|
|
173
|
+
.borders(Borders::LEFT | Borders::RIGHT)
|
|
174
|
+
.style(Style::default().bg(BG_PANEL));
|
|
175
|
+
|
|
176
|
+
let inner = block.inner(area);
|
|
177
|
+
f.render_widget(block, area);
|
|
178
|
+
|
|
179
|
+
let portal = &state.connection_portal;
|
|
180
|
+
|
|
181
|
+
let (status_text, status_color) = if let Some(error) = &portal.error {
|
|
182
|
+
(format!("Connection failed: {}", error), ERROR_RED)
|
|
183
|
+
} else if portal.connecting {
|
|
184
|
+
("Connecting...".to_string(), AMBER_WARN)
|
|
185
|
+
} else {
|
|
186
|
+
("Ready to connect".to_string(), TEXT_PRIMARY)
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
let text = Line::from(vec![
|
|
190
|
+
Span::styled(" Status: ", Style::default().fg(TEXT_DIM)),
|
|
191
|
+
Span::styled(status_text, Style::default().fg(status_color).bold()),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
let paragraph = Paragraph::new(text)
|
|
195
|
+
.alignment(Alignment::Left);
|
|
196
|
+
|
|
197
|
+
f.render_widget(paragraph, inner);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn render_actions(f: &mut Frame, area: Rect, state: &AppState) {
|
|
201
|
+
let block = Block::default()
|
|
202
|
+
.title(" ⌨️ Actions ")
|
|
203
|
+
.borders(Borders::ALL)
|
|
204
|
+
.border_style(Style::default().fg(TEXT_DIM))
|
|
205
|
+
.style(Style::default().bg(BG_PANEL));
|
|
206
|
+
|
|
207
|
+
let inner = block.inner(area);
|
|
208
|
+
f.render_widget(block, area);
|
|
209
|
+
|
|
210
|
+
let portal = &state.connection_portal;
|
|
211
|
+
|
|
212
|
+
let text = if portal.connecting {
|
|
213
|
+
Line::from(vec![
|
|
214
|
+
Span::styled("Connecting to Gateway server...", Style::default().fg(AMBER_WARN)),
|
|
215
|
+
])
|
|
216
|
+
} else {
|
|
217
|
+
Line::from(vec![
|
|
218
|
+
Span::styled("Tab", Style::default().fg(CYBER_CYAN).bold()),
|
|
219
|
+
Span::styled(" Switch Field │ ", Style::default().fg(TEXT_DIM)),
|
|
220
|
+
Span::styled("Enter", Style::default().fg(NEON_GREEN).bold()),
|
|
221
|
+
Span::styled(" Connect │ ", Style::default().fg(TEXT_DIM)),
|
|
222
|
+
Span::styled("ESC", Style::default().fg(BRAND_PURPLE).bold()),
|
|
223
|
+
Span::styled(" Cancel", Style::default().fg(TEXT_DIM)),
|
|
224
|
+
])
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
let paragraph = Paragraph::new(text)
|
|
228
|
+
.style(Style::default().fg(TEXT_PRIMARY))
|
|
229
|
+
.alignment(Alignment::Center);
|
|
230
|
+
|
|
231
|
+
f.render_widget(paragraph, inner);
|
|
232
|
+
}
|
package/mk3-tui/src/ui/layout.rs
CHANGED
|
@@ -118,13 +118,22 @@ fn render_header(f: &mut Frame, area: Rect, state: &AppState) {
|
|
|
118
118
|
// Format uptime
|
|
119
119
|
let uptime_str = format_uptime(state.uptime_secs);
|
|
120
120
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
// Operation mode indicator (Step 7)
|
|
122
|
+
let mode_text = state.operation_mode.as_str();
|
|
123
|
+
let mode_color = match state.operation_mode {
|
|
124
|
+
crate::app::OperationMode::Local => AMBER_WARN,
|
|
125
|
+
crate::app::OperationMode::Connected => NEON_GREEN,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Line 1: Brand + version + mode + uptime - Bug 3 fix: Use "4Runr." with dot (matches brand logo)
|
|
129
|
+
// Use npm package version (2.7.0) - matches package.json
|
|
130
|
+
const PACKAGE_VERSION: &str = "2.8.0";
|
|
124
131
|
let brand_line = Line::from(vec![
|
|
125
132
|
Span::styled("4Runr.", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
|
|
126
133
|
Span::styled(" AI AGENT OS", Style::default().fg(BRAND_VIOLET)),
|
|
127
134
|
Span::styled(format!(" v{}", PACKAGE_VERSION), Style::default().fg(TEXT_MUTED)),
|
|
135
|
+
Span::styled(" Mode: ", Style::default().fg(TEXT_MUTED)),
|
|
136
|
+
Span::styled(mode_text, Style::default().fg(mode_color).add_modifier(Modifier::BOLD)),
|
|
128
137
|
Span::styled(format!(" {} UPTIME: {}", spinner, uptime_str),
|
|
129
138
|
Style::default().fg(TEXT_MUTED)),
|
|
130
139
|
]);
|
package/mk3-tui/src/ui/mod.rs
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.
|
|
5
|
+
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.8.0: Step 5 & 7 Complete - Full agent management integration + Connection Portal. Create, view, select, and delete agents through TUI. Dynamic mode switching (Local ↔ Connected) with Gateway connection. Agent Builder, Agent List, Connection Portal fully functional. Built with Rust + Ratatui. ⚠️ Pre-MVP / Development Phase",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"4runr": "dist/index.js",
|