4runr-os 2.4.3 → 2.5.1

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
 
@@ -15,7 +15,6 @@ mod websocket;
15
15
 
16
16
  use app::App;
17
17
  use io::IoHandler;
18
- use screens::Screen;
19
18
  use websocket::{WebSocketClient, WsClientMessage};
20
19
 
21
20
  fn main() -> Result<()> {
@@ -142,17 +141,7 @@ fn main() -> Result<()> {
142
141
  // Handle agent.list response
143
142
  else if let Some(agents_data) = obj.get("agents") {
144
143
  if let Some(agents_array) = agents_data.as_array() {
145
- // Update capabilities with real agent names
146
- app.state.capabilities = agents_array.iter()
147
- .filter_map(|agent| {
148
- agent.as_object()
149
- .and_then(|a| a.get("name"))
150
- .and_then(|n| n.as_str())
151
- .map(|s| s.to_string())
152
- })
153
- .collect();
154
-
155
- // Parse agents into AgentInfo structs for Agent List viewer
144
+ // Parse agents into AgentInfo structs
156
145
  use crate::app::AgentInfo;
157
146
  let agents: Vec<AgentInfo> = agents_array.iter()
158
147
  .filter_map(|agent| {
@@ -175,44 +164,37 @@ fn main() -> Result<()> {
175
164
  })
176
165
  .collect();
177
166
 
167
+ // Update capabilities with agent names
168
+ app.state.capabilities = agents.iter()
169
+ .map(|a| a.name.clone())
170
+ .collect();
171
+
178
172
  // Update cache with agent data
179
173
  if let Some(cache) = &mut app.state.cache {
180
174
  use crate::storage::cache::AgentData;
181
175
  use std::time::{SystemTime, UNIX_EPOCH};
182
176
 
183
- let cache_agents: Vec<AgentData> = agents_array.iter()
184
- .filter_map(|agent| {
185
- let obj = agent.as_object()?;
186
- Some(AgentData {
187
- name: obj.get("name")?.as_str()?.to_string(),
188
- description: obj.get("description").and_then(|d| d.as_str()).map(|s| s.to_string()),
189
- model: obj.get("model").and_then(|m| m.as_str()).unwrap_or("unknown").to_string(),
190
- provider: obj.get("provider").and_then(|p| p.as_str()).unwrap_or("unknown").to_string(),
191
- created_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
192
- })
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(),
193
184
  })
194
185
  .collect();
195
186
 
196
187
  let _ = cache.update_agents(cache_agents);
197
188
  }
198
189
 
199
- // Update agent list state
190
+ // Update agent list state and open AgentList overlay
200
191
  app.state.agent_list.agents = agents;
201
192
  app.state.agent_list.selected_index = 0;
202
193
  app.state.agent_list.detail_view = None;
203
194
 
204
- // Open AgentList overlay
195
+ use crate::screens::Screen;
205
196
  app.push_overlay(Screen::AgentList);
206
197
  app.request_render("agent_list_opened");
207
-
208
- // Only log if agents were actually loaded
209
- if agents_array.len() > 0 {
210
- app.add_log(format!(
211
- "✓ [{}] Loaded {} agents",
212
- short_id,
213
- agents_array.len()
214
- ));
215
- }
216
198
  }
217
199
  } else {
218
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,62 @@ 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]);
63
-
64
60
  let table_area = content_block.inner(chunks[1]);
61
+ f.render_widget(content_block, chunks[1]);
65
62
 
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
- };
63
+ if state.agent_list.agents.is_empty() {
64
+ // Show "No agents" message
65
+ f.render_widget(
66
+ Paragraph::new("No agents available.\n\nUse Agent Builder to create new agents.")
67
+ .style(Style::default().fg(TEXT_DIM))
68
+ .alignment(Alignment::Center),
69
+ table_area
70
+ );
71
+ } else {
72
+ // Create table rows
73
+ let rows: Vec<Row> = state.agent_list.agents.iter().enumerate().map(|(i, agent)| {
74
+ let is_selected = i == state.agent_list.selected_index;
75
+ let style = if is_selected {
76
+ Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)
77
+ } else {
78
+ Style::default().fg(TEXT_PRIMARY)
79
+ };
80
+
81
+ let name = if is_selected {
82
+ format!("▶ {}", agent.name)
83
+ } else {
84
+ format!(" {}", agent.name)
85
+ };
86
+
87
+ let desc = agent.description.as_deref().unwrap_or("No description").chars().take(30).collect::<String>();
88
+ let model = agent.model.chars().take(15).collect::<String>();
89
+ let provider = agent.provider.chars().take(10).collect::<String>();
90
+
91
+ Row::new(vec![
92
+ Cell::from(name).style(style),
93
+ Cell::from(desc).style(style),
94
+ Cell::from(model).style(style),
95
+ Cell::from(provider).style(style),
96
+ ])
97
+ }).collect();
80
98
 
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>();
99
+ let table = Table::new(rows, [
100
+ Constraint::Percentage(25),
101
+ Constraint::Percentage(35),
102
+ Constraint::Percentage(20),
103
+ Constraint::Percentage(20),
104
+ ])
105
+ .header(Row::new(vec![
106
+ Cell::from("Name").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
107
+ Cell::from("Description").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
108
+ Cell::from("Model").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
109
+ Cell::from("Provider").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
110
+ ]))
111
+ .column_spacing(1)
112
+ .style(Style::default().fg(TEXT_PRIMARY));
85
113
 
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);
114
+ f.render_widget(table, table_area);
115
+ }
113
116
 
114
117
  // === FOOTER ===
115
118
  let footer_text = Line::from(vec![
@@ -124,12 +127,17 @@ fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
124
127
  f.render_widget(
125
128
  Paragraph::new(footer_text)
126
129
  .alignment(Alignment::Center)
127
- .style(Style::default().bg(BG_PANEL)),
130
+ .style(Style::default().bg(BG_PANEL))
131
+ .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(TEXT_MUTED))),
128
132
  chunks[2]
129
133
  );
130
134
  }
131
135
 
132
136
  fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
137
+ // First render list view (dimmed)
138
+ render_list_view(f, area, state);
139
+
140
+ // Then render detail popup on top
133
141
  let detail_index = state.agent_list.detail_view.unwrap();
134
142
  if let Some(agent) = state.agent_list.agents.get(detail_index) {
135
143
  render_agent_detail(f, area, agent);
@@ -137,6 +145,7 @@ fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
137
145
  }
138
146
 
139
147
  fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
148
+
140
149
  // Calculate popup size (70% width, 80% height, centered)
141
150
  let popup_width = (area.width * 70 / 100).max(50);
142
151
  let popup_height = (area.height * 80 / 100).max(15);
@@ -151,9 +160,16 @@ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
151
160
  };
152
161
 
153
162
  // Render dimmed overlay
154
- let overlay = Block::default()
155
- .style(Style::default().bg(Color::Black).fg(Color::Black));
156
- f.render_widget(overlay, area);
163
+ for y in area.y..area.y + area.height {
164
+ for x in area.x..area.x + area.width {
165
+ if x < popup_x || x >= popup_x + popup_width || y < popup_y || y >= popup_y + popup_height {
166
+ f.render_widget(
167
+ Block::default().style(Style::default().bg(Color::Black)),
168
+ Rect { x, y, width: 1, height: 1 }
169
+ );
170
+ }
171
+ }
172
+ }
157
173
 
158
174
  // Render detail popup
159
175
  let detail_block = Block::default()
@@ -163,9 +179,8 @@ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
163
179
  .border_style(Style::default().fg(CYBER_CYAN))
164
180
  .style(Style::default().bg(BG_PANEL));
165
181
 
166
- f.render_widget(detail_block.clone(), popup_area);
167
-
168
182
  let inner = detail_block.inner(popup_area);
183
+ f.render_widget(detail_block, popup_area);
169
184
 
170
185
  // Create detail text
171
186
  let mut detail_lines = vec![
@@ -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,17 @@ 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
+ let nav_inner = nav_block.inner(chunks[0]);
142
+ f.render_widget(nav_block, chunks[0]);
143
+ f.render_widget(nav_list, nav_inner);
155
144
 
156
145
  // Local Commands section
157
146
  let local_block = Block::default()
@@ -162,28 +151,17 @@ fn render_command_list(f: &mut Frame, area: Rect) {
162
151
  .style(Style::default().bg(BG_PANEL));
163
152
 
164
153
  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
- ])),
154
+ ListItem::new("quit, exit").style(Style::default().fg(NEON_GREEN)),
155
+ ListItem::new("clear").style(Style::default().fg(NEON_GREEN)),
156
+ ListItem::new("help").style(Style::default().fg(NEON_GREEN)),
157
+ ListItem::new(":perf").style(Style::default().fg(NEON_GREEN)),
179
158
  ];
180
159
 
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
- );
160
+ let local_list = List::new(local_items);
161
+
162
+ let local_inner = local_block.inner(chunks[1]);
163
+ f.render_widget(local_block, chunks[1]);
164
+ f.render_widget(local_list, local_inner);
187
165
 
188
166
  // WebSocket Commands section
189
167
  let ws_block = Block::default()
@@ -194,35 +172,20 @@ fn render_command_list(f: &mut Frame, area: Rect) {
194
172
  .style(Style::default().bg(BG_PANEL));
195
173
 
196
174
  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
- ])),
175
+ ListItem::new("agent.list").style(Style::default().fg(NEON_GREEN)),
176
+ ListItem::new("agent.get").style(Style::default().fg(NEON_GREEN)),
177
+ ListItem::new("agent.create").style(Style::default().fg(NEON_GREEN)),
178
+ ListItem::new("agent.delete").style(Style::default().fg(NEON_GREEN)),
179
+ ListItem::new("system.status").style(Style::default().fg(NEON_GREEN)),
180
+ ListItem::new("run.list").style(Style::default().fg(NEON_GREEN)),
181
+ ListItem::new("tool.list").style(Style::default().fg(NEON_GREEN)),
218
182
  ];
219
183
 
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
- );
184
+ let ws_list = List::new(ws_items);
185
+
186
+ let ws_inner = ws_block.inner(chunks[2]);
187
+ f.render_widget(ws_block, chunks[2]);
188
+ f.render_widget(ws_list, ws_inner);
226
189
  }
227
190
 
228
191
  fn render_command_details(f: &mut Frame, area: Rect) {
@@ -247,50 +210,47 @@ fn render_command_details(f: &mut Frame, area: Rect) {
247
210
  let desc_text = vec![
248
211
  Line::from(vec![
249
212
  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)),
213
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
214
+ Span::styled("Open Agent Builder (6-step wizard)", Style::default().fg(TEXT_PRIMARY)),
251
215
  ]),
252
216
  Line::from(""),
253
217
  Line::from(vec![
254
218
  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)),
219
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
220
+ Span::styled("Open Run Manager (list, filter, sort)", Style::default().fg(TEXT_PRIMARY)),
256
221
  ]),
257
222
  Line::from(""),
258
223
  Line::from(vec![
259
224
  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)),
225
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
226
+ Span::styled("Open Settings (mode, AI provider)", Style::default().fg(TEXT_PRIMARY)),
261
227
  ]),
262
228
  Line::from(""),
263
229
  Line::from(vec![
264
230
  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)),
231
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
232
+ Span::styled("Close overlay/popup or clear input", Style::default().fg(TEXT_PRIMARY)),
266
233
  ]),
267
234
  Line::from(""),
268
235
  Line::from(vec![
269
236
  Span::styled("quit, exit", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
270
- Span::styled(" - Exit application", Style::default().fg(TEXT_PRIMARY)),
237
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
238
+ Span::styled("Exit application", Style::default().fg(TEXT_PRIMARY)),
271
239
  ]),
272
240
  Line::from(""),
273
241
  Line::from(vec![
274
242
  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)),
243
+ Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
244
+ Span::styled("Clear operations log", Style::default().fg(TEXT_PRIMARY)),
286
245
  ]),
287
246
  ];
288
247
 
289
- f.render_widget(desc_block.clone(), chunks[0]);
248
+ let desc_inner = desc_block.inner(chunks[0]);
249
+ f.render_widget(desc_block, chunks[0]);
290
250
  f.render_widget(
291
251
  Paragraph::new(desc_text)
292
252
  .wrap(Wrap { trim: false }),
293
- desc_block.inner(chunks[0])
253
+ desc_inner
294
254
  );
295
255
 
296
256
  // Screen controls
@@ -308,11 +268,11 @@ fn render_command_details(f: &mut Frame, area: Rect) {
308
268
  Line::from(vec![
309
269
  Span::raw(" "),
310
270
  Span::styled("Enter", Style::default().fg(NEON_GREEN)),
311
- Span::styled(" = Next | ", Style::default().fg(TEXT_DIM)),
271
+ Span::raw(" = Next | "),
312
272
  Span::styled("Backspace", Style::default().fg(NEON_GREEN)),
313
- Span::styled(" = Prev | ", Style::default().fg(TEXT_DIM)),
273
+ Span::raw(" = Prev | "),
314
274
  Span::styled("ESC", Style::default().fg(NEON_GREEN)),
315
- Span::styled(" = Cancel", Style::default().fg(TEXT_DIM)),
275
+ Span::raw(" = Cancel"),
316
276
  ]),
317
277
  Line::from(""),
318
278
  Line::from(vec![
@@ -321,13 +281,13 @@ fn render_command_details(f: &mut Frame, area: Rect) {
321
281
  Line::from(vec![
322
282
  Span::raw(" "),
323
283
  Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
324
- Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
284
+ Span::raw(" = Navigate | "),
325
285
  Span::styled("F", Style::default().fg(NEON_GREEN)),
326
- Span::styled(" = Filter | ", Style::default().fg(TEXT_DIM)),
286
+ Span::raw(" = Filter | "),
327
287
  Span::styled("S", Style::default().fg(NEON_GREEN)),
328
- Span::styled(" = Sort | ", Style::default().fg(TEXT_DIM)),
288
+ Span::raw(" = Sort | "),
329
289
  Span::styled("R", Style::default().fg(NEON_GREEN)),
330
- Span::styled(" = Refresh", Style::default().fg(TEXT_DIM)),
290
+ Span::raw(" = Refresh"),
331
291
  ]),
332
292
  Line::from(""),
333
293
  Line::from(vec![
@@ -336,18 +296,19 @@ fn render_command_details(f: &mut Frame, area: Rect) {
336
296
  Line::from(vec![
337
297
  Span::raw(" "),
338
298
  Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
339
- Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
299
+ Span::raw(" = Navigate | "),
340
300
  Span::styled("Space", Style::default().fg(NEON_GREEN)),
341
- Span::styled(" = Toggle | ", Style::default().fg(TEXT_DIM)),
301
+ Span::raw(" = Toggle | "),
342
302
  Span::styled("Enter", Style::default().fg(NEON_GREEN)),
343
- Span::styled(" = Save", Style::default().fg(TEXT_DIM)),
303
+ Span::raw(" = Save"),
344
304
  ]),
345
305
  ];
346
306
 
347
- f.render_widget(controls_block.clone(), chunks[1]);
307
+ let controls_inner = controls_block.inner(chunks[1]);
308
+ f.render_widget(controls_block, chunks[1]);
348
309
  f.render_widget(
349
310
  Paragraph::new(controls_text)
350
311
  .wrap(Wrap { trim: false }),
351
- controls_block.inner(chunks[1])
312
+ controls_inner
352
313
  );
353
314
  }
@@ -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.3";
123
+ const PACKAGE_VERSION: &str = "2.5.1";
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,
@@ -7,3 +7,7 @@ pub mod run_manager;
7
7
  pub mod safe_viewport;
8
8
  pub mod settings;
9
9
 
10
+ // Re-export screen states
11
+ pub use agent_builder::AgentBuilderState;
12
+ pub use run_manager::RunManagerState;
13
+ pub use settings::SettingsState;
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.4.3",
3
+ "version": "2.5.1",
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.1: Fixed compilation errors (borrowed value issues in help.rs and agent_list.rs). Professional help popup interface, Agent List viewer with detail popup. Enhanced UX with clean navigation. Built with Rust + Ratatui. ⚠️ Pre-MVP / Development Phase",
7
6
  "main": "dist/index.js",
8
7
  "bin": {
9
8
  "4runr": "dist/index.js",