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.
- package/mk3-tui/src/app.rs +88 -92
- package/mk3-tui/src/main.rs +15 -33
- package/mk3-tui/src/screens/mod.rs +225 -225
- package/mk3-tui/src/ui/agent_list.rs +72 -57
- package/mk3-tui/src/ui/help.rs +59 -98
- package/mk3-tui/src/ui/layout.rs +2 -1
- package/mk3-tui/src/ui/mod.rs +4 -0
- package/package.json +2 -3
package/mk3-tui/src/app.rs
CHANGED
|
@@ -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
|
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -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
|
-
//
|
|
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> =
|
|
184
|
-
.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
155
|
-
.
|
|
156
|
-
|
|
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![
|
package/mk3-tui/src/ui/help.rs
CHANGED
|
@@ -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(
|
|
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(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
ListItem::new(
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
ListItem::new(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
ListItem::new(
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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(" -
|
|
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(" -
|
|
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(" -
|
|
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(" -
|
|
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(" -
|
|
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(" -
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
271
|
+
Span::raw(" = Next | "),
|
|
312
272
|
Span::styled("Backspace", Style::default().fg(NEON_GREEN)),
|
|
313
|
-
Span::
|
|
273
|
+
Span::raw(" = Prev | "),
|
|
314
274
|
Span::styled("ESC", Style::default().fg(NEON_GREEN)),
|
|
315
|
-
Span::
|
|
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::
|
|
284
|
+
Span::raw(" = Navigate | "),
|
|
325
285
|
Span::styled("F", Style::default().fg(NEON_GREEN)),
|
|
326
|
-
Span::
|
|
286
|
+
Span::raw(" = Filter | "),
|
|
327
287
|
Span::styled("S", Style::default().fg(NEON_GREEN)),
|
|
328
|
-
Span::
|
|
288
|
+
Span::raw(" = Sort | "),
|
|
329
289
|
Span::styled("R", Style::default().fg(NEON_GREEN)),
|
|
330
|
-
Span::
|
|
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::
|
|
299
|
+
Span::raw(" = Navigate | "),
|
|
340
300
|
Span::styled("Space", Style::default().fg(NEON_GREEN)),
|
|
341
|
-
Span::
|
|
301
|
+
Span::raw(" = Toggle | "),
|
|
342
302
|
Span::styled("Enter", Style::default().fg(NEON_GREEN)),
|
|
343
|
-
Span::
|
|
303
|
+
Span::raw(" = Save"),
|
|
344
304
|
]),
|
|
345
305
|
];
|
|
346
306
|
|
|
347
|
-
|
|
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
|
-
|
|
312
|
+
controls_inner
|
|
352
313
|
);
|
|
353
314
|
}
|
package/mk3-tui/src/ui/layout.rs
CHANGED
|
@@ -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.
|
|
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,
|
package/mk3-tui/src/ui/mod.rs
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
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",
|