4runr-os 2.4.2 → 2.5.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.
@@ -29,35 +29,6 @@ pub enum AppMode {
29
29
  Main,
30
30
  }
31
31
 
32
- #[derive(Debug, Clone)]
33
- pub struct AgentListState {
34
- pub agents: Vec<AgentInfo>,
35
- pub selected_index: usize,
36
- pub detail_view: Option<usize>, // None = list view, Some(index) = detail popup
37
- }
38
-
39
- impl Default for AgentListState {
40
- fn default() -> Self {
41
- Self {
42
- agents: Vec::new(),
43
- selected_index: 0,
44
- detail_view: None,
45
- }
46
- }
47
- }
48
-
49
- #[derive(Debug, Clone)]
50
- pub struct AgentInfo {
51
- pub name: String,
52
- pub description: Option<String>,
53
- pub model: String,
54
- pub provider: String,
55
- pub system_prompt: Option<String>,
56
- pub temperature: Option<f32>,
57
- pub max_tokens: Option<u32>,
58
- pub tools: Vec<String>,
59
- }
60
-
61
32
  #[derive(Debug, Clone)]
62
33
  pub struct AppState {
63
34
  // Navigation state (NEW - replaces simple mode)
@@ -190,6 +161,35 @@ impl Default for AppState {
190
161
  }
191
162
  }
192
163
 
164
+ #[derive(Debug, Clone)]
165
+ pub struct AgentListState {
166
+ pub agents: Vec<AgentInfo>,
167
+ pub selected_index: usize,
168
+ pub detail_view: Option<usize>, // None = list view, Some(index) = detail popup
169
+ }
170
+
171
+ impl Default for AgentListState {
172
+ fn default() -> Self {
173
+ Self {
174
+ agents: Vec::new(),
175
+ selected_index: 0,
176
+ detail_view: None,
177
+ }
178
+ }
179
+ }
180
+
181
+ #[derive(Debug, Clone)]
182
+ pub struct AgentInfo {
183
+ pub name: String,
184
+ pub description: Option<String>,
185
+ pub model: String,
186
+ pub provider: String,
187
+ pub system_prompt: Option<String>,
188
+ pub temperature: Option<f32>,
189
+ pub max_tokens: Option<u32>,
190
+ pub tools: Vec<String>,
191
+ }
192
+
193
193
  pub struct App {
194
194
  pub(crate) state: AppState,
195
195
  render_scheduler: RenderScheduler,
@@ -1112,69 +1112,6 @@ impl App {
1112
1112
  Ok(false)
1113
1113
  }
1114
1114
 
1115
- // ============================================================
1116
- // AGENT LIST INPUT HANDLING (Step 4)
1117
- // ============================================================
1118
-
1119
- /// Handle input when Agent List screen is active
1120
- fn handle_agent_list_input(&mut self, key: KeyEvent) -> anyhow::Result<bool> {
1121
- use crossterm::event::KeyModifiers;
1122
-
1123
- // Ctrl+C/Q to exit
1124
- if key.modifiers.contains(KeyModifiers::CONTROL) {
1125
- match key.code {
1126
- KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
1127
- _ => return Ok(false),
1128
- }
1129
- }
1130
-
1131
- match key.code {
1132
- // Navigation
1133
- KeyCode::Up => {
1134
- if self.state.agent_list.selected_index > 0 {
1135
- self.state.agent_list.selected_index -= 1;
1136
- }
1137
- self.request_render("agent_list_up");
1138
- }
1139
- KeyCode::Down => {
1140
- let max = self.state.agent_list.agents.len().saturating_sub(1);
1141
- if self.state.agent_list.selected_index < max {
1142
- self.state.agent_list.selected_index += 1;
1143
- }
1144
- self.request_render("agent_list_down");
1145
- }
1146
-
1147
- // View details / Close detail view
1148
- KeyCode::Enter => {
1149
- if self.state.agent_list.detail_view.is_some() {
1150
- // Close detail view
1151
- self.state.agent_list.detail_view = None;
1152
- } else {
1153
- // Open detail view for selected agent
1154
- self.state.agent_list.detail_view = Some(self.state.agent_list.selected_index);
1155
- }
1156
- self.request_render("agent_list_toggle_detail");
1157
- }
1158
-
1159
- // Close
1160
- KeyCode::Esc => {
1161
- if self.state.agent_list.detail_view.is_some() {
1162
- // Close detail popup first
1163
- self.state.agent_list.detail_view = None;
1164
- self.request_render("agent_list_close_detail");
1165
- } else {
1166
- // Close agent list
1167
- self.pop_overlay();
1168
- self.request_render("agent_list_close");
1169
- }
1170
- }
1171
-
1172
- _ => {}
1173
- }
1174
-
1175
- Ok(false)
1176
- }
1177
-
1178
1115
  // ============================================================
1179
1116
  // SETTINGS INPUT HANDLING (Step 4.8)
1180
1117
  // ============================================================
@@ -1246,5 +1183,64 @@ impl App {
1246
1183
 
1247
1184
  Ok(false)
1248
1185
  }
1186
+
1187
+ /// Handle input when Agent List screen is active
1188
+ fn handle_agent_list_input(&mut self, key: KeyEvent) -> anyhow::Result<bool> {
1189
+ use crossterm::event::KeyModifiers;
1190
+
1191
+ // Ctrl+C/Q to exit
1192
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
1193
+ match key.code {
1194
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
1195
+ _ => return Ok(false),
1196
+ }
1197
+ }
1198
+
1199
+ match key.code {
1200
+ // Navigation
1201
+ KeyCode::Up => {
1202
+ if self.state.agent_list.selected_index > 0 {
1203
+ self.state.agent_list.selected_index -= 1;
1204
+ }
1205
+ self.request_render("agent_list_up");
1206
+ }
1207
+ KeyCode::Down => {
1208
+ let max = self.state.agent_list.agents.len().saturating_sub(1);
1209
+ if self.state.agent_list.selected_index < max {
1210
+ self.state.agent_list.selected_index += 1;
1211
+ }
1212
+ self.request_render("agent_list_down");
1213
+ }
1214
+
1215
+ // View details / Close detail view
1216
+ KeyCode::Enter => {
1217
+ if self.state.agent_list.detail_view.is_some() {
1218
+ // Close detail view
1219
+ self.state.agent_list.detail_view = None;
1220
+ } else {
1221
+ // Open detail view for selected agent
1222
+ self.state.agent_list.detail_view = Some(self.state.agent_list.selected_index);
1223
+ }
1224
+ self.request_render("agent_list_toggle_detail");
1225
+ }
1226
+
1227
+ // Close
1228
+ KeyCode::Esc => {
1229
+ if self.state.agent_list.detail_view.is_some() {
1230
+ // Close detail popup first
1231
+ self.state.agent_list.detail_view = None;
1232
+ self.request_render("agent_list_close_detail");
1233
+ } else {
1234
+ // Close agent list
1235
+ self.pop_overlay();
1236
+ self.request_render("agent_list_close");
1237
+ }
1238
+ }
1239
+
1240
+ _ => {}
1241
+ }
1242
+
1243
+ Ok(false)
1244
+ }
1249
1245
  }
1250
1246
 
@@ -141,17 +141,7 @@ fn main() -> Result<()> {
141
141
  // Handle agent.list response
142
142
  else if let Some(agents_data) = obj.get("agents") {
143
143
  if let Some(agents_array) = agents_data.as_array() {
144
- // Update capabilities with real agent names
145
- app.state.capabilities = agents_array.iter()
146
- .filter_map(|agent| {
147
- agent.as_object()
148
- .and_then(|a| a.get("name"))
149
- .and_then(|n| n.as_str())
150
- .map(|s| s.to_string())
151
- })
152
- .collect();
153
-
154
- // Parse agents into AgentInfo structs for Agent List viewer
144
+ // Parse agents into AgentInfo structs
155
145
  use crate::app::AgentInfo;
156
146
  let agents: Vec<AgentInfo> = agents_array.iter()
157
147
  .filter_map(|agent| {
@@ -174,44 +164,37 @@ fn main() -> Result<()> {
174
164
  })
175
165
  .collect();
176
166
 
167
+ // Update capabilities with agent names
168
+ app.state.capabilities = agents.iter()
169
+ .map(|a| a.name.clone())
170
+ .collect();
171
+
177
172
  // Update cache with agent data
178
173
  if let Some(cache) = &mut app.state.cache {
179
174
  use crate::storage::cache::AgentData;
180
175
  use std::time::{SystemTime, UNIX_EPOCH};
181
176
 
182
- let cache_agents: Vec<AgentData> = agents_array.iter()
183
- .filter_map(|agent| {
184
- let obj = agent.as_object()?;
185
- Some(AgentData {
186
- name: obj.get("name")?.as_str()?.to_string(),
187
- description: obj.get("description").and_then(|d| d.as_str()).map(|s| s.to_string()),
188
- model: obj.get("model").and_then(|m| m.as_str()).unwrap_or("unknown").to_string(),
189
- provider: obj.get("provider").and_then(|p| p.as_str()).unwrap_or("unknown").to_string(),
190
- created_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
191
- })
177
+ let cache_agents: Vec<AgentData> = agents.iter()
178
+ .map(|agent| AgentData {
179
+ name: agent.name.clone(),
180
+ description: agent.description.clone(),
181
+ model: agent.model.clone(),
182
+ provider: agent.provider.clone(),
183
+ created_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
192
184
  })
193
185
  .collect();
194
186
 
195
187
  let _ = cache.update_agents(cache_agents);
196
188
  }
197
189
 
198
- // Update agent list state
190
+ // Update agent list state and open AgentList overlay
199
191
  app.state.agent_list.agents = agents;
200
192
  app.state.agent_list.selected_index = 0;
201
193
  app.state.agent_list.detail_view = None;
202
194
 
203
- // Open AgentList overlay
195
+ use crate::screens::Screen;
204
196
  app.push_overlay(Screen::AgentList);
205
197
  app.request_render("agent_list_opened");
206
-
207
- // Only log if agents were actually loaded
208
- if agents_array.len() > 0 {
209
- app.add_log(format!(
210
- "✓ [{}] Loaded {} agents",
211
- short_id,
212
- agents_array.len()
213
- ));
214
- }
215
198
  }
216
199
  } else {
217
200
  app.add_log(format!("✓ [{}] Success", short_id));
@@ -1,225 +1,225 @@
1
- /// Screen types and navigation system
2
- ///
3
- /// This module defines the screen/mode system for the TUI.
4
- /// Screens can be:
5
- /// - Base screens (Boot, Main) - always visible
6
- /// - Overlay screens (Agent Builder, Run Manager, Settings) - full-screen modals
7
- /// - Popup screens (Confirmations, Alerts) - small overlays
8
-
9
- use serde::{Deserialize, Serialize};
10
-
11
- /// Screen enum - represents all possible screens in the TUI
12
- #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13
- pub enum Screen {
14
- // Base screens
15
- Boot,
16
- Main,
17
-
18
- // Overlay screens (full-screen modals)
19
- AgentBuilder,
20
- RunManager,
21
- Settings,
22
- AgentList,
23
-
24
- // Popup screens (small overlays)
25
- Confirmation { message: String, action: String },
26
- Alert { message: String },
27
- Help,
28
- }
29
-
30
- impl Screen {
31
- /// Returns true if this screen is an overlay (covers base screen)
32
- pub fn is_overlay(&self) -> bool {
33
- matches!(
34
- self,
35
- Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
36
- )
37
- }
38
-
39
- /// Returns true if this screen is a popup (small overlay)
40
- pub fn is_popup(&self) -> bool {
41
- matches!(
42
- self,
43
- Screen::Confirmation { .. } | Screen::Alert { .. } | Screen::Help
44
- )
45
- }
46
-
47
- /// Returns true if this screen is a base screen
48
- pub fn is_base(&self) -> bool {
49
- matches!(self, Screen::Boot | Screen::Main)
50
- }
51
-
52
- /// Returns the screen name for display
53
- pub fn name(&self) -> &str {
54
- match self {
55
- Screen::Boot => "Boot",
56
- Screen::Main => "Main",
57
- Screen::AgentBuilder => "Agent Builder",
58
- Screen::RunManager => "Run Manager",
59
- Screen::Settings => "Settings",
60
- Screen::AgentList => "Agent List",
61
- Screen::Confirmation { .. } => "Confirmation",
62
- Screen::Alert { .. } => "Alert",
63
- Screen::Help => "Help",
64
- }
65
- }
66
- }
67
-
68
- /// Navigation state - manages screen stack and transitions
69
- #[derive(Debug, Clone)]
70
- pub struct NavigationState {
71
- /// Current base screen (Boot or Main)
72
- pub base_screen: Screen,
73
-
74
- /// Overlay stack (full-screen modals)
75
- /// Last item is the currently visible overlay
76
- pub overlay_stack: Vec<Screen>,
77
-
78
- /// Popup stack (small overlays on top of everything)
79
- /// Last item is the currently visible popup
80
- pub popup_stack: Vec<Screen>,
81
- }
82
-
83
- impl Default for NavigationState {
84
- fn default() -> Self {
85
- Self {
86
- base_screen: Screen::Boot,
87
- overlay_stack: Vec::new(),
88
- popup_stack: Vec::new(),
89
- }
90
- }
91
- }
92
-
93
- impl NavigationState {
94
- /// Create a new navigation state starting at Boot
95
- pub fn new() -> Self {
96
- Self::default()
97
- }
98
-
99
- /// Get the currently visible screen (topmost in hierarchy)
100
- pub fn current_screen(&self) -> &Screen {
101
- // Priority: Popup > Overlay > Base
102
- if let Some(popup) = self.popup_stack.last() {
103
- popup
104
- } else if let Some(overlay) = self.overlay_stack.last() {
105
- overlay
106
- } else {
107
- &self.base_screen
108
- }
109
- }
110
-
111
- /// Navigate to a base screen (Boot or Main)
112
- pub fn navigate_to_base(&mut self, screen: Screen) {
113
- if screen.is_base() {
114
- self.base_screen = screen;
115
- // Clear overlays and popups when changing base screen
116
- self.overlay_stack.clear();
117
- self.popup_stack.clear();
118
- }
119
- }
120
-
121
- /// Push an overlay screen (Agent Builder, Run Manager, Settings)
122
- pub fn push_overlay(&mut self, screen: Screen) {
123
- if screen.is_overlay() {
124
- self.overlay_stack.push(screen);
125
- }
126
- }
127
-
128
- /// Pop the current overlay and return to previous screen
129
- pub fn pop_overlay(&mut self) -> Option<Screen> {
130
- self.overlay_stack.pop()
131
- }
132
-
133
- /// Push a popup screen (Confirmation, Alert, Help)
134
- pub fn push_popup(&mut self, screen: Screen) {
135
- if screen.is_popup() {
136
- self.popup_stack.push(screen);
137
- }
138
- }
139
-
140
- /// Pop the current popup
141
- pub fn pop_popup(&mut self) -> Option<Screen> {
142
- self.popup_stack.pop()
143
- }
144
-
145
- /// Close all overlays and popups (return to base screen)
146
- pub fn close_all(&mut self) {
147
- self.overlay_stack.clear();
148
- self.popup_stack.clear();
149
- }
150
-
151
- /// Check if we're currently on the base screen (no overlays/popups)
152
- pub fn is_on_base(&self) -> bool {
153
- self.overlay_stack.is_empty() && self.popup_stack.is_empty()
154
- }
155
-
156
- /// Check if we have any overlays open
157
- pub fn has_overlay(&self) -> bool {
158
- !self.overlay_stack.is_empty()
159
- }
160
-
161
- /// Check if we have any popups open
162
- pub fn has_popup(&self) -> bool {
163
- !self.popup_stack.is_empty()
164
- }
165
- }
166
-
167
- #[cfg(test)]
168
- mod tests {
169
- use super::*;
170
-
171
- #[test]
172
- fn test_screen_classification() {
173
- assert!(Screen::Boot.is_base());
174
- assert!(Screen::Main.is_base());
175
- assert!(Screen::AgentBuilder.is_overlay());
176
- assert!(Screen::RunManager.is_overlay());
177
- assert!(Screen::Settings.is_overlay());
178
- assert!(Screen::Help.is_popup());
179
- }
180
-
181
- #[test]
182
- fn test_navigation_state() {
183
- let mut nav = NavigationState::new();
184
-
185
- // Start at Boot
186
- assert_eq!(nav.current_screen(), &Screen::Boot);
187
- assert!(nav.is_on_base());
188
-
189
- // Navigate to Main
190
- nav.navigate_to_base(Screen::Main);
191
- assert_eq!(nav.current_screen(), &Screen::Main);
192
-
193
- // Push overlay
194
- nav.push_overlay(Screen::AgentBuilder);
195
- assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
196
- assert!(nav.has_overlay());
197
- assert!(!nav.is_on_base());
198
-
199
- // Push popup
200
- nav.push_popup(Screen::Help);
201
- assert_eq!(nav.current_screen(), &Screen::Help);
202
- assert!(nav.has_popup());
203
-
204
- // Pop popup
205
- nav.pop_popup();
206
- assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
207
-
208
- // Pop overlay
209
- nav.pop_overlay();
210
- assert_eq!(nav.current_screen(), &Screen::Main);
211
- assert!(nav.is_on_base());
212
- }
213
-
214
- #[test]
215
- fn test_close_all() {
216
- let mut nav = NavigationState::new();
217
- nav.navigate_to_base(Screen::Main);
218
- nav.push_overlay(Screen::AgentBuilder);
219
- nav.push_popup(Screen::Help);
220
-
221
- nav.close_all();
222
- assert!(nav.is_on_base());
223
- assert_eq!(nav.current_screen(), &Screen::Main);
224
- }
225
- }
1
+ /// Screen types and navigation system
2
+ ///
3
+ /// This module defines the screen/mode system for the TUI.
4
+ /// Screens can be:
5
+ /// - Base screens (Boot, Main) - always visible
6
+ /// - Overlay screens (Agent Builder, Run Manager, Settings) - full-screen modals
7
+ /// - Popup screens (Confirmations, Alerts) - small overlays
8
+
9
+ use serde::{Deserialize, Serialize};
10
+
11
+ /// Screen enum - represents all possible screens in the TUI
12
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13
+ pub enum Screen {
14
+ // Base screens
15
+ Boot,
16
+ Main,
17
+
18
+ // Overlay screens (full-screen modals)
19
+ AgentBuilder,
20
+ RunManager,
21
+ Settings,
22
+ AgentList,
23
+
24
+ // Popup screens (small overlays)
25
+ Confirmation { message: String, action: String },
26
+ Alert { message: String },
27
+ Help,
28
+ }
29
+
30
+ impl Screen {
31
+ /// Returns true if this screen is an overlay (covers base screen)
32
+ pub fn is_overlay(&self) -> bool {
33
+ matches!(
34
+ self,
35
+ Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
36
+ )
37
+ }
38
+
39
+ /// Returns true if this screen is a popup (small overlay)
40
+ pub fn is_popup(&self) -> bool {
41
+ matches!(
42
+ self,
43
+ Screen::Confirmation { .. } | Screen::Alert { .. } | Screen::Help
44
+ )
45
+ }
46
+
47
+ /// Returns true if this screen is a base screen
48
+ pub fn is_base(&self) -> bool {
49
+ matches!(self, Screen::Boot | Screen::Main)
50
+ }
51
+
52
+ /// Returns the screen name for display
53
+ pub fn name(&self) -> &str {
54
+ match self {
55
+ Screen::Boot => "Boot",
56
+ Screen::Main => "Main",
57
+ Screen::AgentBuilder => "Agent Builder",
58
+ Screen::RunManager => "Run Manager",
59
+ Screen::Settings => "Settings",
60
+ Screen::AgentList => "Agent List",
61
+ Screen::Confirmation { .. } => "Confirmation",
62
+ Screen::Alert { .. } => "Alert",
63
+ Screen::Help => "Help",
64
+ }
65
+ }
66
+ }
67
+
68
+ /// Navigation state - manages screen stack and transitions
69
+ #[derive(Debug, Clone)]
70
+ pub struct NavigationState {
71
+ /// Current base screen (Boot or Main)
72
+ pub base_screen: Screen,
73
+
74
+ /// Overlay stack (full-screen modals)
75
+ /// Last item is the currently visible overlay
76
+ pub overlay_stack: Vec<Screen>,
77
+
78
+ /// Popup stack (small overlays on top of everything)
79
+ /// Last item is the currently visible popup
80
+ pub popup_stack: Vec<Screen>,
81
+ }
82
+
83
+ impl Default for NavigationState {
84
+ fn default() -> Self {
85
+ Self {
86
+ base_screen: Screen::Boot,
87
+ overlay_stack: Vec::new(),
88
+ popup_stack: Vec::new(),
89
+ }
90
+ }
91
+ }
92
+
93
+ impl NavigationState {
94
+ /// Create a new navigation state starting at Boot
95
+ pub fn new() -> Self {
96
+ Self::default()
97
+ }
98
+
99
+ /// Get the currently visible screen (topmost in hierarchy)
100
+ pub fn current_screen(&self) -> &Screen {
101
+ // Priority: Popup > Overlay > Base
102
+ if let Some(popup) = self.popup_stack.last() {
103
+ popup
104
+ } else if let Some(overlay) = self.overlay_stack.last() {
105
+ overlay
106
+ } else {
107
+ &self.base_screen
108
+ }
109
+ }
110
+
111
+ /// Navigate to a base screen (Boot or Main)
112
+ pub fn navigate_to_base(&mut self, screen: Screen) {
113
+ if screen.is_base() {
114
+ self.base_screen = screen;
115
+ // Clear overlays and popups when changing base screen
116
+ self.overlay_stack.clear();
117
+ self.popup_stack.clear();
118
+ }
119
+ }
120
+
121
+ /// Push an overlay screen (Agent Builder, Run Manager, Settings)
122
+ pub fn push_overlay(&mut self, screen: Screen) {
123
+ if screen.is_overlay() {
124
+ self.overlay_stack.push(screen);
125
+ }
126
+ }
127
+
128
+ /// Pop the current overlay and return to previous screen
129
+ pub fn pop_overlay(&mut self) -> Option<Screen> {
130
+ self.overlay_stack.pop()
131
+ }
132
+
133
+ /// Push a popup screen (Confirmation, Alert, Help)
134
+ pub fn push_popup(&mut self, screen: Screen) {
135
+ if screen.is_popup() {
136
+ self.popup_stack.push(screen);
137
+ }
138
+ }
139
+
140
+ /// Pop the current popup
141
+ pub fn pop_popup(&mut self) -> Option<Screen> {
142
+ self.popup_stack.pop()
143
+ }
144
+
145
+ /// Close all overlays and popups (return to base screen)
146
+ pub fn close_all(&mut self) {
147
+ self.overlay_stack.clear();
148
+ self.popup_stack.clear();
149
+ }
150
+
151
+ /// Check if we're currently on the base screen (no overlays/popups)
152
+ pub fn is_on_base(&self) -> bool {
153
+ self.overlay_stack.is_empty() && self.popup_stack.is_empty()
154
+ }
155
+
156
+ /// Check if we have any overlays open
157
+ pub fn has_overlay(&self) -> bool {
158
+ !self.overlay_stack.is_empty()
159
+ }
160
+
161
+ /// Check if we have any popups open
162
+ pub fn has_popup(&self) -> bool {
163
+ !self.popup_stack.is_empty()
164
+ }
165
+ }
166
+
167
+ #[cfg(test)]
168
+ mod tests {
169
+ use super::*;
170
+
171
+ #[test]
172
+ fn test_screen_classification() {
173
+ assert!(Screen::Boot.is_base());
174
+ assert!(Screen::Main.is_base());
175
+ assert!(Screen::AgentBuilder.is_overlay());
176
+ assert!(Screen::RunManager.is_overlay());
177
+ assert!(Screen::Settings.is_overlay());
178
+ assert!(Screen::Help.is_popup());
179
+ }
180
+
181
+ #[test]
182
+ fn test_navigation_state() {
183
+ let mut nav = NavigationState::new();
184
+
185
+ // Start at Boot
186
+ assert_eq!(nav.current_screen(), &Screen::Boot);
187
+ assert!(nav.is_on_base());
188
+
189
+ // Navigate to Main
190
+ nav.navigate_to_base(Screen::Main);
191
+ assert_eq!(nav.current_screen(), &Screen::Main);
192
+
193
+ // Push overlay
194
+ nav.push_overlay(Screen::AgentBuilder);
195
+ assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
196
+ assert!(nav.has_overlay());
197
+ assert!(!nav.is_on_base());
198
+
199
+ // Push popup
200
+ nav.push_popup(Screen::Help);
201
+ assert_eq!(nav.current_screen(), &Screen::Help);
202
+ assert!(nav.has_popup());
203
+
204
+ // Pop popup
205
+ nav.pop_popup();
206
+ assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
207
+
208
+ // Pop overlay
209
+ nav.pop_overlay();
210
+ assert_eq!(nav.current_screen(), &Screen::Main);
211
+ assert!(nav.is_on_base());
212
+ }
213
+
214
+ #[test]
215
+ fn test_close_all() {
216
+ let mut nav = NavigationState::new();
217
+ nav.navigate_to_base(Screen::Main);
218
+ nav.push_overlay(Screen::AgentBuilder);
219
+ nav.push_popup(Screen::Help);
220
+
221
+ nav.close_all();
222
+ assert!(nav.is_on_base());
223
+ assert_eq!(nav.current_screen(), &Screen::Main);
224
+ }
225
+ }
@@ -19,12 +19,10 @@ pub fn render(f: &mut Frame, state: &AppState) {
19
19
  let area = f.size();
20
20
 
21
21
  if state.agent_list.detail_view.is_some() {
22
- // Render list view first (as base)
23
- render_list_view(f, area, state);
24
- // Then render detail popup on top
22
+ // Render detail popup
25
23
  render_detail_popup(f, area, state);
26
24
  } else {
27
- // Render list view only
25
+ // Render list view
28
26
  render_list_view(f, area, state);
29
27
  }
30
28
  }
@@ -59,57 +57,63 @@ fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
59
57
  .border_style(Style::default().fg(TEXT_MUTED))
60
58
  .style(Style::default().bg(BG_PANEL));
61
59
 
62
- f.render_widget(content_block.clone(), chunks[1]);
60
+ f.render_widget(content_block, chunks[1]);
63
61
 
64
62
  let table_area = content_block.inner(chunks[1]);
65
63
 
66
- // Create table rows
67
- let rows: Vec<Row> = state.agent_list.agents.iter().enumerate().map(|(i, agent)| {
68
- let is_selected = i == state.agent_list.selected_index;
69
- let style = if is_selected {
70
- Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)
71
- } else {
72
- Style::default().fg(TEXT_PRIMARY)
73
- };
74
-
75
- let name = if is_selected {
76
- format!("▶ {}", agent.name)
77
- } else {
78
- format!(" {}", agent.name)
79
- };
64
+ if state.agent_list.agents.is_empty() {
65
+ // Show "No agents" message
66
+ f.render_widget(
67
+ Paragraph::new("No agents available.\n\nUse Agent Builder to create new agents.")
68
+ .style(Style::default().fg(TEXT_DIM))
69
+ .alignment(Alignment::Center),
70
+ table_area
71
+ );
72
+ } else {
73
+ // Create table rows
74
+ let rows: Vec<Row> = state.agent_list.agents.iter().enumerate().map(|(i, agent)| {
75
+ let is_selected = i == state.agent_list.selected_index;
76
+ let style = if is_selected {
77
+ Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)
78
+ } else {
79
+ Style::default().fg(TEXT_PRIMARY)
80
+ };
81
+
82
+ let name = if is_selected {
83
+ format!("▶ {}", agent.name)
84
+ } else {
85
+ format!(" {}", agent.name)
86
+ };
87
+
88
+ let desc = agent.description.as_deref().unwrap_or("No description").chars().take(30).collect::<String>();
89
+ let model = agent.model.chars().take(15).collect::<String>();
90
+ let provider = agent.provider.chars().take(10).collect::<String>();
91
+
92
+ Row::new(vec![
93
+ Cell::from(name).style(style),
94
+ Cell::from(desc).style(style),
95
+ Cell::from(model).style(style),
96
+ Cell::from(provider).style(style),
97
+ ])
98
+ }).collect();
80
99
 
81
- let desc = agent.description.as_deref().unwrap_or("No description")
82
- .chars().take(30).collect::<String>();
83
- let model = agent.model.chars().take(15).collect::<String>();
84
- let provider = agent.provider.chars().take(10).collect::<String>();
100
+ let table = Table::new(rows, [
101
+ Constraint::Percentage(25),
102
+ Constraint::Percentage(35),
103
+ Constraint::Percentage(20),
104
+ Constraint::Percentage(20),
105
+ ])
106
+ .header(Row::new(vec![
107
+ Cell::from("Name").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
108
+ Cell::from("Description").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
109
+ Cell::from("Model").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
110
+ Cell::from("Provider").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
111
+ ]))
112
+ .column_spacing(1)
113
+ .style(Style::default().fg(TEXT_PRIMARY));
85
114
 
86
- Row::new(vec![
87
- Cell::from(name).style(style),
88
- Cell::from(desc).style(style),
89
- Cell::from(model).style(style),
90
- Cell::from(provider).style(style),
91
- ])
92
- }).collect();
93
-
94
- let table = Table::new(
95
- rows,
96
- [
97
- Constraint::Percentage(25),
98
- Constraint::Percentage(35),
99
- Constraint::Percentage(20),
100
- Constraint::Percentage(20),
101
- ]
102
- )
103
- .header(Row::new(vec![
104
- Cell::from("Name").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
105
- Cell::from("Description").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
106
- Cell::from("Model").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
107
- Cell::from("Provider").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
108
- ]))
109
- .column_spacing(1)
110
- .style(Style::default().fg(TEXT_PRIMARY));
111
-
112
- f.render_widget(table, table_area);
115
+ f.render_widget(table, table_area);
116
+ }
113
117
 
114
118
  // === FOOTER ===
115
119
  let footer_text = Line::from(vec![
@@ -124,12 +128,17 @@ fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
124
128
  f.render_widget(
125
129
  Paragraph::new(footer_text)
126
130
  .alignment(Alignment::Center)
127
- .style(Style::default().bg(BG_PANEL)),
131
+ .style(Style::default().bg(BG_PANEL))
132
+ .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(TEXT_MUTED))),
128
133
  chunks[2]
129
134
  );
130
135
  }
131
136
 
132
137
  fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
138
+ // First render list view (dimmed)
139
+ render_list_view(f, area, state);
140
+
141
+ // Then render detail popup on top
133
142
  let detail_index = state.agent_list.detail_view.unwrap();
134
143
  if let Some(agent) = state.agent_list.agents.get(detail_index) {
135
144
  render_agent_detail(f, area, agent);
@@ -153,9 +162,16 @@ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
153
162
  };
154
163
 
155
164
  // Render dimmed overlay
156
- let overlay = Block::default()
157
- .style(Style::default().bg(Color::Black).fg(Color::Black));
158
- f.render_widget(overlay, area);
165
+ for y in area.y..area.y + area.height {
166
+ for x in area.x..area.x + area.width {
167
+ if x < popup_x || x >= popup_x + popup_width || y < popup_y || y >= popup_y + popup_height {
168
+ f.render_widget(
169
+ Block::default().style(Style::default().bg(Color::Black)),
170
+ Rect { x, y, width: 1, height: 1 }
171
+ );
172
+ }
173
+ }
174
+ }
159
175
 
160
176
  // Render detail popup
161
177
  let detail_block = Block::default()
@@ -165,7 +181,7 @@ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
165
181
  .border_style(Style::default().fg(CYBER_CYAN))
166
182
  .style(Style::default().bg(BG_PANEL));
167
183
 
168
- f.render_widget(detail_block.clone(), popup_area);
184
+ f.render_widget(detail_block, popup_area);
169
185
 
170
186
  let inner = detail_block.inner(popup_area);
171
187
 
@@ -115,7 +115,7 @@ fn render_command_list(f: &mut Frame, area: Rect) {
115
115
  let chunks = Layout::default()
116
116
  .direction(Direction::Vertical)
117
117
  .constraints([
118
- Constraint::Length(8), // Navigation Commands
118
+ Constraint::Length(7), // Navigation Commands
119
119
  Constraint::Length(7), // Local Commands
120
120
  Constraint::Min(0), // WebSocket Commands
121
121
  ])
@@ -130,28 +130,16 @@ fn render_command_list(f: &mut Frame, area: Rect) {
130
130
  .style(Style::default().bg(BG_PANEL));
131
131
 
132
132
  let nav_items = vec![
133
- ListItem::new(Line::from(vec![
134
- Span::styled("build", Style::default().fg(NEON_GREEN)),
135
- ])),
136
- ListItem::new(Line::from(vec![
137
- Span::styled("runs", Style::default().fg(NEON_GREEN)),
138
- ])),
139
- ListItem::new(Line::from(vec![
140
- Span::styled("config", Style::default().fg(NEON_GREEN)),
141
- Span::styled(", ", Style::default().fg(TEXT_DIM)),
142
- Span::styled("settings", Style::default().fg(NEON_GREEN)),
143
- ])),
144
- ListItem::new(Line::from(vec![
145
- Span::styled("ESC", Style::default().fg(NEON_GREEN)),
146
- ])),
133
+ ListItem::new("build").style(Style::default().fg(NEON_GREEN)),
134
+ ListItem::new("runs").style(Style::default().fg(NEON_GREEN)),
135
+ ListItem::new("config, settings").style(Style::default().fg(NEON_GREEN)),
136
+ ListItem::new("ESC").style(Style::default().fg(NEON_GREEN)),
147
137
  ];
148
138
 
149
- f.render_widget(nav_block.clone(), chunks[0]);
150
- f.render_widget(
151
- List::new(nav_items)
152
- .style(Style::default().fg(TEXT_PRIMARY)),
153
- nav_block.inner(chunks[0])
154
- );
139
+ let nav_list = List::new(nav_items);
140
+
141
+ f.render_widget(nav_block, chunks[0]);
142
+ f.render_widget(nav_list, nav_block.inner(chunks[0]));
155
143
 
156
144
  // Local Commands section
157
145
  let local_block = Block::default()
@@ -162,28 +150,16 @@ fn render_command_list(f: &mut Frame, area: Rect) {
162
150
  .style(Style::default().bg(BG_PANEL));
163
151
 
164
152
  let local_items = vec![
165
- ListItem::new(Line::from(vec![
166
- Span::styled("quit", Style::default().fg(NEON_GREEN)),
167
- Span::styled(", ", Style::default().fg(TEXT_DIM)),
168
- Span::styled("exit", Style::default().fg(NEON_GREEN)),
169
- ])),
170
- ListItem::new(Line::from(vec![
171
- Span::styled("clear", Style::default().fg(NEON_GREEN)),
172
- ])),
173
- ListItem::new(Line::from(vec![
174
- Span::styled("help", Style::default().fg(NEON_GREEN)),
175
- ])),
176
- ListItem::new(Line::from(vec![
177
- Span::styled(":perf", Style::default().fg(NEON_GREEN)),
178
- ])),
153
+ ListItem::new("quit, exit").style(Style::default().fg(NEON_GREEN)),
154
+ ListItem::new("clear").style(Style::default().fg(NEON_GREEN)),
155
+ ListItem::new("help").style(Style::default().fg(NEON_GREEN)),
156
+ ListItem::new(":perf").style(Style::default().fg(NEON_GREEN)),
179
157
  ];
180
158
 
181
- f.render_widget(local_block.clone(), chunks[1]);
182
- f.render_widget(
183
- List::new(local_items)
184
- .style(Style::default().fg(TEXT_PRIMARY)),
185
- local_block.inner(chunks[1])
186
- );
159
+ let local_list = List::new(local_items);
160
+
161
+ f.render_widget(local_block, chunks[1]);
162
+ f.render_widget(local_list, local_block.inner(chunks[1]));
187
163
 
188
164
  // WebSocket Commands section
189
165
  let ws_block = Block::default()
@@ -194,35 +170,19 @@ fn render_command_list(f: &mut Frame, area: Rect) {
194
170
  .style(Style::default().bg(BG_PANEL));
195
171
 
196
172
  let ws_items = vec![
197
- ListItem::new(Line::from(vec![
198
- Span::styled("agent.list", Style::default().fg(NEON_GREEN)),
199
- ])),
200
- ListItem::new(Line::from(vec![
201
- Span::styled("agent.get", Style::default().fg(NEON_GREEN)),
202
- ])),
203
- ListItem::new(Line::from(vec![
204
- Span::styled("agent.create", Style::default().fg(NEON_GREEN)),
205
- ])),
206
- ListItem::new(Line::from(vec![
207
- Span::styled("agent.delete", Style::default().fg(NEON_GREEN)),
208
- ])),
209
- ListItem::new(Line::from(vec![
210
- Span::styled("system.status", Style::default().fg(NEON_GREEN)),
211
- ])),
212
- ListItem::new(Line::from(vec![
213
- Span::styled("run.list", Style::default().fg(NEON_GREEN)),
214
- ])),
215
- ListItem::new(Line::from(vec![
216
- Span::styled("tool.list", Style::default().fg(NEON_GREEN)),
217
- ])),
173
+ ListItem::new("agent.list").style(Style::default().fg(NEON_GREEN)),
174
+ ListItem::new("agent.get").style(Style::default().fg(NEON_GREEN)),
175
+ ListItem::new("agent.create").style(Style::default().fg(NEON_GREEN)),
176
+ ListItem::new("agent.delete").style(Style::default().fg(NEON_GREEN)),
177
+ ListItem::new("system.status").style(Style::default().fg(NEON_GREEN)),
178
+ ListItem::new("run.list").style(Style::default().fg(NEON_GREEN)),
179
+ ListItem::new("tool.list").style(Style::default().fg(NEON_GREEN)),
218
180
  ];
219
181
 
220
- f.render_widget(ws_block.clone(), chunks[2]);
221
- f.render_widget(
222
- List::new(ws_items)
223
- .style(Style::default().fg(TEXT_PRIMARY)),
224
- ws_block.inner(chunks[2])
225
- );
182
+ let ws_list = List::new(ws_items);
183
+
184
+ f.render_widget(ws_block, chunks[2]);
185
+ f.render_widget(ws_list, ws_block.inner(chunks[2]));
226
186
  }
227
187
 
228
188
  fn render_command_details(f: &mut Frame, area: Rect) {
@@ -247,46 +207,42 @@ fn render_command_details(f: &mut Frame, area: Rect) {
247
207
  let desc_text = vec![
248
208
  Line::from(vec![
249
209
  Span::styled("build", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
250
- Span::styled(" - Open Agent Builder (6-step wizard)", Style::default().fg(TEXT_PRIMARY)),
210
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
211
+ Span::styled("Open Agent Builder (6-step wizard)", Style::default().fg(TEXT_PRIMARY)),
251
212
  ]),
252
213
  Line::from(""),
253
214
  Line::from(vec![
254
215
  Span::styled("runs", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
255
- Span::styled(" - Open Run Manager (list, filter, sort)", Style::default().fg(TEXT_PRIMARY)),
216
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
217
+ Span::styled("Open Run Manager (list, filter, sort)", Style::default().fg(TEXT_PRIMARY)),
256
218
  ]),
257
219
  Line::from(""),
258
220
  Line::from(vec![
259
221
  Span::styled("config, settings", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
260
- Span::styled(" - Open Settings (mode, AI provider)", Style::default().fg(TEXT_PRIMARY)),
222
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
223
+ Span::styled("Open Settings (mode, AI provider)", Style::default().fg(TEXT_PRIMARY)),
261
224
  ]),
262
225
  Line::from(""),
263
226
  Line::from(vec![
264
227
  Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
265
- Span::styled(" - Close overlay/popup or clear input", Style::default().fg(TEXT_PRIMARY)),
228
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
229
+ Span::styled("Close overlay/popup or clear input", Style::default().fg(TEXT_PRIMARY)),
266
230
  ]),
267
231
  Line::from(""),
268
232
  Line::from(vec![
269
233
  Span::styled("quit, exit", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
270
- Span::styled(" - Exit application", Style::default().fg(TEXT_PRIMARY)),
234
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
235
+ Span::styled("Exit application", Style::default().fg(TEXT_PRIMARY)),
271
236
  ]),
272
237
  Line::from(""),
273
238
  Line::from(vec![
274
239
  Span::styled("clear", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
275
- Span::styled(" - Clear operations log", Style::default().fg(TEXT_PRIMARY)),
276
- ]),
277
- Line::from(""),
278
- Line::from(vec![
279
- Span::styled("help", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
280
- Span::styled(" - Show this help", Style::default().fg(TEXT_PRIMARY)),
281
- ]),
282
- Line::from(""),
283
- Line::from(vec![
284
- Span::styled(":perf", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
285
- Span::styled(" - Show performance stats", Style::default().fg(TEXT_PRIMARY)),
240
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
241
+ Span::styled("Clear operations log", Style::default().fg(TEXT_PRIMARY)),
286
242
  ]),
287
243
  ];
288
244
 
289
- f.render_widget(desc_block.clone(), chunks[0]);
245
+ f.render_widget(desc_block, chunks[0]);
290
246
  f.render_widget(
291
247
  Paragraph::new(desc_text)
292
248
  .wrap(Wrap { trim: false }),
@@ -308,11 +264,11 @@ fn render_command_details(f: &mut Frame, area: Rect) {
308
264
  Line::from(vec![
309
265
  Span::raw(" "),
310
266
  Span::styled("Enter", Style::default().fg(NEON_GREEN)),
311
- Span::styled(" = Next | ", Style::default().fg(TEXT_DIM)),
267
+ Span::raw(" = Next | "),
312
268
  Span::styled("Backspace", Style::default().fg(NEON_GREEN)),
313
- Span::styled(" = Prev | ", Style::default().fg(TEXT_DIM)),
269
+ Span::raw(" = Prev | "),
314
270
  Span::styled("ESC", Style::default().fg(NEON_GREEN)),
315
- Span::styled(" = Cancel", Style::default().fg(TEXT_DIM)),
271
+ Span::raw(" = Cancel"),
316
272
  ]),
317
273
  Line::from(""),
318
274
  Line::from(vec![
@@ -321,13 +277,13 @@ fn render_command_details(f: &mut Frame, area: Rect) {
321
277
  Line::from(vec![
322
278
  Span::raw(" "),
323
279
  Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
324
- Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
280
+ Span::raw(" = Navigate | "),
325
281
  Span::styled("F", Style::default().fg(NEON_GREEN)),
326
- Span::styled(" = Filter | ", Style::default().fg(TEXT_DIM)),
282
+ Span::raw(" = Filter | "),
327
283
  Span::styled("S", Style::default().fg(NEON_GREEN)),
328
- Span::styled(" = Sort | ", Style::default().fg(TEXT_DIM)),
284
+ Span::raw(" = Sort | "),
329
285
  Span::styled("R", Style::default().fg(NEON_GREEN)),
330
- Span::styled(" = Refresh", Style::default().fg(TEXT_DIM)),
286
+ Span::raw(" = Refresh"),
331
287
  ]),
332
288
  Line::from(""),
333
289
  Line::from(vec![
@@ -336,15 +292,15 @@ fn render_command_details(f: &mut Frame, area: Rect) {
336
292
  Line::from(vec![
337
293
  Span::raw(" "),
338
294
  Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
339
- Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
295
+ Span::raw(" = Navigate | "),
340
296
  Span::styled("Space", Style::default().fg(NEON_GREEN)),
341
- Span::styled(" = Toggle | ", Style::default().fg(TEXT_DIM)),
297
+ Span::raw(" = Toggle | "),
342
298
  Span::styled("Enter", Style::default().fg(NEON_GREEN)),
343
- Span::styled(" = Save", Style::default().fg(TEXT_DIM)),
299
+ Span::raw(" = Save"),
344
300
  ]),
345
301
  ];
346
302
 
347
- f.render_widget(controls_block.clone(), chunks[1]);
303
+ f.render_widget(controls_block, chunks[1]);
348
304
  f.render_widget(
349
305
  Paragraph::new(controls_text)
350
306
  .wrap(Wrap { trim: false }),
@@ -120,7 +120,7 @@ fn render_header(f: &mut Frame, area: Rect, state: &AppState) {
120
120
 
121
121
  // Line 1: Brand + version + uptime - Bug 3 fix: Use "4Runr." with dot (matches brand logo)
122
122
  // Use npm package version (2.3.5) - matches package.json
123
- const PACKAGE_VERSION: &str = "2.4.2";
123
+ const PACKAGE_VERSION: &str = "2.5.0";
124
124
  let brand_line = Line::from(vec![
125
125
  Span::styled("4Runr.", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
126
126
  Span::styled(" AI AGENT OS", Style::default().fg(BRAND_VIOLET)),
@@ -422,6 +422,7 @@ fn mem_color(value: f64) -> Color {
422
422
  else if value > 0.6 { Color::Rgb(255, 191, 0) }
423
423
  else { CYBER_CYAN }
424
424
  }
425
+
425
426
  fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
426
427
  let panel_area = Rect {
427
428
  x: area.x + 1,
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
- "private": false,
6
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.3.5: Fixed boot screen logo rendering (restored original working version), fully functional Agent Builder with input handling and TUI styling. Built with Rust + Ratatui. ⚠️ Pre-MVP / Development Phase",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.5.0: NEW professional help popup interface, NEW Agent List viewer with detail popup for agent.list command. Enhanced UX with clean navigation and organized displays. Built with Rust + Ratatui. ⚠️ Pre-MVP / Development Phase",
7
6
  "main": "dist/index.js",
8
7
  "bin": {
9
8
  "4runr": "dist/index.js",