4runr-os 2.4.3 → 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
 
@@ -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
+ }
@@ -3,4 +3,4 @@
3
3
 
4
4
  pub mod cache;
5
5
 
6
- pub use cache::Cache;
6
+ pub use cache::{Cache, CacheData};
@@ -108,7 +108,7 @@ struct CostEstimate {
108
108
  is_free: bool,
109
109
  }
110
110
 
111
- fn calculate_cost(provider: &str, model: &str, _max_tokens: u32) -> CostEstimate {
111
+ fn calculate_cost(provider: &str, model: &str, max_tokens: u32) -> CostEstimate {
112
112
  let pricing = get_model_pricing(provider, model);
113
113
  let is_free = pricing.input_cost == 0.0 && pricing.output_cost == 0.0;
114
114
 
@@ -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);
@@ -137,6 +146,8 @@ fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
137
146
  }
138
147
 
139
148
  fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
149
+ use ratatui::layout::{Constraint, Direction, Layout};
150
+
140
151
  // Calculate popup size (70% width, 80% height, centered)
141
152
  let popup_width = (area.width * 70 / 100).max(50);
142
153
  let popup_height = (area.height * 80 / 100).max(15);
@@ -151,9 +162,16 @@ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
151
162
  };
152
163
 
153
164
  // Render dimmed overlay
154
- let overlay = Block::default()
155
- .style(Style::default().bg(Color::Black).fg(Color::Black));
156
- 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
+ }
157
175
 
158
176
  // Render detail popup
159
177
  let detail_block = Block::default()
@@ -163,7 +181,7 @@ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
163
181
  .border_style(Style::default().fg(CYBER_CYAN))
164
182
  .style(Style::default().bg(BG_PANEL));
165
183
 
166
- f.render_widget(detail_block.clone(), popup_area);
184
+ f.render_widget(detail_block, popup_area);
167
185
 
168
186
  let inner = detail_block.inner(popup_area);
169
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.3";
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,
@@ -7,3 +7,8 @@ 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;
14
+
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.4.3",
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",