4runr-os 2.4.2 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mk3-tui/src/app.rs +88 -92
- package/mk3-tui/src/main.rs +15 -32
- package/mk3-tui/src/screens/mod.rs +225 -225
- package/mk3-tui/src/ui/agent_list.rs +71 -55
- package/mk3-tui/src/ui/help.rs +52 -96
- package/mk3-tui/src/ui/layout.rs +2 -1
- 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
|
@@ -141,17 +141,7 @@ fn main() -> Result<()> {
|
|
|
141
141
|
// Handle agent.list response
|
|
142
142
|
else if let Some(agents_data) = obj.get("agents") {
|
|
143
143
|
if let Some(agents_array) = agents_data.as_array() {
|
|
144
|
-
//
|
|
145
|
-
app.state.capabilities = agents_array.iter()
|
|
146
|
-
.filter_map(|agent| {
|
|
147
|
-
agent.as_object()
|
|
148
|
-
.and_then(|a| a.get("name"))
|
|
149
|
-
.and_then(|n| n.as_str())
|
|
150
|
-
.map(|s| s.to_string())
|
|
151
|
-
})
|
|
152
|
-
.collect();
|
|
153
|
-
|
|
154
|
-
// Parse agents into AgentInfo structs for Agent List viewer
|
|
144
|
+
// Parse agents into AgentInfo structs
|
|
155
145
|
use crate::app::AgentInfo;
|
|
156
146
|
let agents: Vec<AgentInfo> = agents_array.iter()
|
|
157
147
|
.filter_map(|agent| {
|
|
@@ -174,44 +164,37 @@ fn main() -> Result<()> {
|
|
|
174
164
|
})
|
|
175
165
|
.collect();
|
|
176
166
|
|
|
167
|
+
// Update capabilities with agent names
|
|
168
|
+
app.state.capabilities = agents.iter()
|
|
169
|
+
.map(|a| a.name.clone())
|
|
170
|
+
.collect();
|
|
171
|
+
|
|
177
172
|
// Update cache with agent data
|
|
178
173
|
if let Some(cache) = &mut app.state.cache {
|
|
179
174
|
use crate::storage::cache::AgentData;
|
|
180
175
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
181
176
|
|
|
182
|
-
let cache_agents: Vec<AgentData> =
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
provider: obj.get("provider").and_then(|p| p.as_str()).unwrap_or("unknown").to_string(),
|
|
190
|
-
created_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
|
191
|
-
})
|
|
177
|
+
let cache_agents: Vec<AgentData> = agents.iter()
|
|
178
|
+
.map(|agent| AgentData {
|
|
179
|
+
name: agent.name.clone(),
|
|
180
|
+
description: agent.description.clone(),
|
|
181
|
+
model: agent.model.clone(),
|
|
182
|
+
provider: agent.provider.clone(),
|
|
183
|
+
created_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
|
192
184
|
})
|
|
193
185
|
.collect();
|
|
194
186
|
|
|
195
187
|
let _ = cache.update_agents(cache_agents);
|
|
196
188
|
}
|
|
197
189
|
|
|
198
|
-
// Update agent list state
|
|
190
|
+
// Update agent list state and open AgentList overlay
|
|
199
191
|
app.state.agent_list.agents = agents;
|
|
200
192
|
app.state.agent_list.selected_index = 0;
|
|
201
193
|
app.state.agent_list.detail_view = None;
|
|
202
194
|
|
|
203
|
-
|
|
195
|
+
use crate::screens::Screen;
|
|
204
196
|
app.push_overlay(Screen::AgentList);
|
|
205
197
|
app.request_render("agent_list_opened");
|
|
206
|
-
|
|
207
|
-
// Only log if agents were actually loaded
|
|
208
|
-
if agents_array.len() > 0 {
|
|
209
|
-
app.add_log(format!(
|
|
210
|
-
"✓ [{}] Loaded {} agents",
|
|
211
|
-
short_id,
|
|
212
|
-
agents_array.len()
|
|
213
|
-
));
|
|
214
|
-
}
|
|
215
198
|
}
|
|
216
199
|
} else {
|
|
217
200
|
app.add_log(format!("✓ [{}] Success", short_id));
|
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
/// Screen types and navigation system
|
|
2
|
-
///
|
|
3
|
-
/// This module defines the screen/mode system for the TUI.
|
|
4
|
-
/// Screens can be:
|
|
5
|
-
/// - Base screens (Boot, Main) - always visible
|
|
6
|
-
/// - Overlay screens (Agent Builder, Run Manager, Settings) - full-screen modals
|
|
7
|
-
/// - Popup screens (Confirmations, Alerts) - small overlays
|
|
8
|
-
|
|
9
|
-
use serde::{Deserialize, Serialize};
|
|
10
|
-
|
|
11
|
-
/// Screen enum - represents all possible screens in the TUI
|
|
12
|
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
13
|
-
pub enum Screen {
|
|
14
|
-
// Base screens
|
|
15
|
-
Boot,
|
|
16
|
-
Main,
|
|
17
|
-
|
|
18
|
-
// Overlay screens (full-screen modals)
|
|
19
|
-
AgentBuilder,
|
|
20
|
-
RunManager,
|
|
21
|
-
Settings,
|
|
22
|
-
AgentList,
|
|
23
|
-
|
|
24
|
-
// Popup screens (small overlays)
|
|
25
|
-
Confirmation { message: String, action: String },
|
|
26
|
-
Alert { message: String },
|
|
27
|
-
Help,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
impl Screen {
|
|
31
|
-
/// Returns true if this screen is an overlay (covers base screen)
|
|
32
|
-
pub fn is_overlay(&self) -> bool {
|
|
33
|
-
matches!(
|
|
34
|
-
self,
|
|
35
|
-
Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/// Returns true if this screen is a popup (small overlay)
|
|
40
|
-
pub fn is_popup(&self) -> bool {
|
|
41
|
-
matches!(
|
|
42
|
-
self,
|
|
43
|
-
Screen::Confirmation { .. } | Screen::Alert { .. } | Screen::Help
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/// Returns true if this screen is a base screen
|
|
48
|
-
pub fn is_base(&self) -> bool {
|
|
49
|
-
matches!(self, Screen::Boot | Screen::Main)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/// Returns the screen name for display
|
|
53
|
-
pub fn name(&self) -> &str {
|
|
54
|
-
match self {
|
|
55
|
-
Screen::Boot => "Boot",
|
|
56
|
-
Screen::Main => "Main",
|
|
57
|
-
Screen::AgentBuilder => "Agent Builder",
|
|
58
|
-
Screen::RunManager => "Run Manager",
|
|
59
|
-
Screen::Settings => "Settings",
|
|
60
|
-
Screen::AgentList => "Agent List",
|
|
61
|
-
Screen::Confirmation { .. } => "Confirmation",
|
|
62
|
-
Screen::Alert { .. } => "Alert",
|
|
63
|
-
Screen::Help => "Help",
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/// Navigation state - manages screen stack and transitions
|
|
69
|
-
#[derive(Debug, Clone)]
|
|
70
|
-
pub struct NavigationState {
|
|
71
|
-
/// Current base screen (Boot or Main)
|
|
72
|
-
pub base_screen: Screen,
|
|
73
|
-
|
|
74
|
-
/// Overlay stack (full-screen modals)
|
|
75
|
-
/// Last item is the currently visible overlay
|
|
76
|
-
pub overlay_stack: Vec<Screen>,
|
|
77
|
-
|
|
78
|
-
/// Popup stack (small overlays on top of everything)
|
|
79
|
-
/// Last item is the currently visible popup
|
|
80
|
-
pub popup_stack: Vec<Screen>,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
impl Default for NavigationState {
|
|
84
|
-
fn default() -> Self {
|
|
85
|
-
Self {
|
|
86
|
-
base_screen: Screen::Boot,
|
|
87
|
-
overlay_stack: Vec::new(),
|
|
88
|
-
popup_stack: Vec::new(),
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
impl NavigationState {
|
|
94
|
-
/// Create a new navigation state starting at Boot
|
|
95
|
-
pub fn new() -> Self {
|
|
96
|
-
Self::default()
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/// Get the currently visible screen (topmost in hierarchy)
|
|
100
|
-
pub fn current_screen(&self) -> &Screen {
|
|
101
|
-
// Priority: Popup > Overlay > Base
|
|
102
|
-
if let Some(popup) = self.popup_stack.last() {
|
|
103
|
-
popup
|
|
104
|
-
} else if let Some(overlay) = self.overlay_stack.last() {
|
|
105
|
-
overlay
|
|
106
|
-
} else {
|
|
107
|
-
&self.base_screen
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/// Navigate to a base screen (Boot or Main)
|
|
112
|
-
pub fn navigate_to_base(&mut self, screen: Screen) {
|
|
113
|
-
if screen.is_base() {
|
|
114
|
-
self.base_screen = screen;
|
|
115
|
-
// Clear overlays and popups when changing base screen
|
|
116
|
-
self.overlay_stack.clear();
|
|
117
|
-
self.popup_stack.clear();
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/// Push an overlay screen (Agent Builder, Run Manager, Settings)
|
|
122
|
-
pub fn push_overlay(&mut self, screen: Screen) {
|
|
123
|
-
if screen.is_overlay() {
|
|
124
|
-
self.overlay_stack.push(screen);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/// Pop the current overlay and return to previous screen
|
|
129
|
-
pub fn pop_overlay(&mut self) -> Option<Screen> {
|
|
130
|
-
self.overlay_stack.pop()
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/// Push a popup screen (Confirmation, Alert, Help)
|
|
134
|
-
pub fn push_popup(&mut self, screen: Screen) {
|
|
135
|
-
if screen.is_popup() {
|
|
136
|
-
self.popup_stack.push(screen);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/// Pop the current popup
|
|
141
|
-
pub fn pop_popup(&mut self) -> Option<Screen> {
|
|
142
|
-
self.popup_stack.pop()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/// Close all overlays and popups (return to base screen)
|
|
146
|
-
pub fn close_all(&mut self) {
|
|
147
|
-
self.overlay_stack.clear();
|
|
148
|
-
self.popup_stack.clear();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/// Check if we're currently on the base screen (no overlays/popups)
|
|
152
|
-
pub fn is_on_base(&self) -> bool {
|
|
153
|
-
self.overlay_stack.is_empty() && self.popup_stack.is_empty()
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/// Check if we have any overlays open
|
|
157
|
-
pub fn has_overlay(&self) -> bool {
|
|
158
|
-
!self.overlay_stack.is_empty()
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/// Check if we have any popups open
|
|
162
|
-
pub fn has_popup(&self) -> bool {
|
|
163
|
-
!self.popup_stack.is_empty()
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
#[cfg(test)]
|
|
168
|
-
mod tests {
|
|
169
|
-
use super::*;
|
|
170
|
-
|
|
171
|
-
#[test]
|
|
172
|
-
fn test_screen_classification() {
|
|
173
|
-
assert!(Screen::Boot.is_base());
|
|
174
|
-
assert!(Screen::Main.is_base());
|
|
175
|
-
assert!(Screen::AgentBuilder.is_overlay());
|
|
176
|
-
assert!(Screen::RunManager.is_overlay());
|
|
177
|
-
assert!(Screen::Settings.is_overlay());
|
|
178
|
-
assert!(Screen::Help.is_popup());
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
#[test]
|
|
182
|
-
fn test_navigation_state() {
|
|
183
|
-
let mut nav = NavigationState::new();
|
|
184
|
-
|
|
185
|
-
// Start at Boot
|
|
186
|
-
assert_eq!(nav.current_screen(), &Screen::Boot);
|
|
187
|
-
assert!(nav.is_on_base());
|
|
188
|
-
|
|
189
|
-
// Navigate to Main
|
|
190
|
-
nav.navigate_to_base(Screen::Main);
|
|
191
|
-
assert_eq!(nav.current_screen(), &Screen::Main);
|
|
192
|
-
|
|
193
|
-
// Push overlay
|
|
194
|
-
nav.push_overlay(Screen::AgentBuilder);
|
|
195
|
-
assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
|
|
196
|
-
assert!(nav.has_overlay());
|
|
197
|
-
assert!(!nav.is_on_base());
|
|
198
|
-
|
|
199
|
-
// Push popup
|
|
200
|
-
nav.push_popup(Screen::Help);
|
|
201
|
-
assert_eq!(nav.current_screen(), &Screen::Help);
|
|
202
|
-
assert!(nav.has_popup());
|
|
203
|
-
|
|
204
|
-
// Pop popup
|
|
205
|
-
nav.pop_popup();
|
|
206
|
-
assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
|
|
207
|
-
|
|
208
|
-
// Pop overlay
|
|
209
|
-
nav.pop_overlay();
|
|
210
|
-
assert_eq!(nav.current_screen(), &Screen::Main);
|
|
211
|
-
assert!(nav.is_on_base());
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
#[test]
|
|
215
|
-
fn test_close_all() {
|
|
216
|
-
let mut nav = NavigationState::new();
|
|
217
|
-
nav.navigate_to_base(Screen::Main);
|
|
218
|
-
nav.push_overlay(Screen::AgentBuilder);
|
|
219
|
-
nav.push_popup(Screen::Help);
|
|
220
|
-
|
|
221
|
-
nav.close_all();
|
|
222
|
-
assert!(nav.is_on_base());
|
|
223
|
-
assert_eq!(nav.current_screen(), &Screen::Main);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
1
|
+
/// Screen types and navigation system
|
|
2
|
+
///
|
|
3
|
+
/// This module defines the screen/mode system for the TUI.
|
|
4
|
+
/// Screens can be:
|
|
5
|
+
/// - Base screens (Boot, Main) - always visible
|
|
6
|
+
/// - Overlay screens (Agent Builder, Run Manager, Settings) - full-screen modals
|
|
7
|
+
/// - Popup screens (Confirmations, Alerts) - small overlays
|
|
8
|
+
|
|
9
|
+
use serde::{Deserialize, Serialize};
|
|
10
|
+
|
|
11
|
+
/// Screen enum - represents all possible screens in the TUI
|
|
12
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
13
|
+
pub enum Screen {
|
|
14
|
+
// Base screens
|
|
15
|
+
Boot,
|
|
16
|
+
Main,
|
|
17
|
+
|
|
18
|
+
// Overlay screens (full-screen modals)
|
|
19
|
+
AgentBuilder,
|
|
20
|
+
RunManager,
|
|
21
|
+
Settings,
|
|
22
|
+
AgentList,
|
|
23
|
+
|
|
24
|
+
// Popup screens (small overlays)
|
|
25
|
+
Confirmation { message: String, action: String },
|
|
26
|
+
Alert { message: String },
|
|
27
|
+
Help,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Screen {
|
|
31
|
+
/// Returns true if this screen is an overlay (covers base screen)
|
|
32
|
+
pub fn is_overlay(&self) -> bool {
|
|
33
|
+
matches!(
|
|
34
|
+
self,
|
|
35
|
+
Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Returns true if this screen is a popup (small overlay)
|
|
40
|
+
pub fn is_popup(&self) -> bool {
|
|
41
|
+
matches!(
|
|
42
|
+
self,
|
|
43
|
+
Screen::Confirmation { .. } | Screen::Alert { .. } | Screen::Help
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Returns true if this screen is a base screen
|
|
48
|
+
pub fn is_base(&self) -> bool {
|
|
49
|
+
matches!(self, Screen::Boot | Screen::Main)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Returns the screen name for display
|
|
53
|
+
pub fn name(&self) -> &str {
|
|
54
|
+
match self {
|
|
55
|
+
Screen::Boot => "Boot",
|
|
56
|
+
Screen::Main => "Main",
|
|
57
|
+
Screen::AgentBuilder => "Agent Builder",
|
|
58
|
+
Screen::RunManager => "Run Manager",
|
|
59
|
+
Screen::Settings => "Settings",
|
|
60
|
+
Screen::AgentList => "Agent List",
|
|
61
|
+
Screen::Confirmation { .. } => "Confirmation",
|
|
62
|
+
Screen::Alert { .. } => "Alert",
|
|
63
|
+
Screen::Help => "Help",
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Navigation state - manages screen stack and transitions
|
|
69
|
+
#[derive(Debug, Clone)]
|
|
70
|
+
pub struct NavigationState {
|
|
71
|
+
/// Current base screen (Boot or Main)
|
|
72
|
+
pub base_screen: Screen,
|
|
73
|
+
|
|
74
|
+
/// Overlay stack (full-screen modals)
|
|
75
|
+
/// Last item is the currently visible overlay
|
|
76
|
+
pub overlay_stack: Vec<Screen>,
|
|
77
|
+
|
|
78
|
+
/// Popup stack (small overlays on top of everything)
|
|
79
|
+
/// Last item is the currently visible popup
|
|
80
|
+
pub popup_stack: Vec<Screen>,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl Default for NavigationState {
|
|
84
|
+
fn default() -> Self {
|
|
85
|
+
Self {
|
|
86
|
+
base_screen: Screen::Boot,
|
|
87
|
+
overlay_stack: Vec::new(),
|
|
88
|
+
popup_stack: Vec::new(),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl NavigationState {
|
|
94
|
+
/// Create a new navigation state starting at Boot
|
|
95
|
+
pub fn new() -> Self {
|
|
96
|
+
Self::default()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Get the currently visible screen (topmost in hierarchy)
|
|
100
|
+
pub fn current_screen(&self) -> &Screen {
|
|
101
|
+
// Priority: Popup > Overlay > Base
|
|
102
|
+
if let Some(popup) = self.popup_stack.last() {
|
|
103
|
+
popup
|
|
104
|
+
} else if let Some(overlay) = self.overlay_stack.last() {
|
|
105
|
+
overlay
|
|
106
|
+
} else {
|
|
107
|
+
&self.base_screen
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Navigate to a base screen (Boot or Main)
|
|
112
|
+
pub fn navigate_to_base(&mut self, screen: Screen) {
|
|
113
|
+
if screen.is_base() {
|
|
114
|
+
self.base_screen = screen;
|
|
115
|
+
// Clear overlays and popups when changing base screen
|
|
116
|
+
self.overlay_stack.clear();
|
|
117
|
+
self.popup_stack.clear();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Push an overlay screen (Agent Builder, Run Manager, Settings)
|
|
122
|
+
pub fn push_overlay(&mut self, screen: Screen) {
|
|
123
|
+
if screen.is_overlay() {
|
|
124
|
+
self.overlay_stack.push(screen);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Pop the current overlay and return to previous screen
|
|
129
|
+
pub fn pop_overlay(&mut self) -> Option<Screen> {
|
|
130
|
+
self.overlay_stack.pop()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Push a popup screen (Confirmation, Alert, Help)
|
|
134
|
+
pub fn push_popup(&mut self, screen: Screen) {
|
|
135
|
+
if screen.is_popup() {
|
|
136
|
+
self.popup_stack.push(screen);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Pop the current popup
|
|
141
|
+
pub fn pop_popup(&mut self) -> Option<Screen> {
|
|
142
|
+
self.popup_stack.pop()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Close all overlays and popups (return to base screen)
|
|
146
|
+
pub fn close_all(&mut self) {
|
|
147
|
+
self.overlay_stack.clear();
|
|
148
|
+
self.popup_stack.clear();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Check if we're currently on the base screen (no overlays/popups)
|
|
152
|
+
pub fn is_on_base(&self) -> bool {
|
|
153
|
+
self.overlay_stack.is_empty() && self.popup_stack.is_empty()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Check if we have any overlays open
|
|
157
|
+
pub fn has_overlay(&self) -> bool {
|
|
158
|
+
!self.overlay_stack.is_empty()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Check if we have any popups open
|
|
162
|
+
pub fn has_popup(&self) -> bool {
|
|
163
|
+
!self.popup_stack.is_empty()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[cfg(test)]
|
|
168
|
+
mod tests {
|
|
169
|
+
use super::*;
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn test_screen_classification() {
|
|
173
|
+
assert!(Screen::Boot.is_base());
|
|
174
|
+
assert!(Screen::Main.is_base());
|
|
175
|
+
assert!(Screen::AgentBuilder.is_overlay());
|
|
176
|
+
assert!(Screen::RunManager.is_overlay());
|
|
177
|
+
assert!(Screen::Settings.is_overlay());
|
|
178
|
+
assert!(Screen::Help.is_popup());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
fn test_navigation_state() {
|
|
183
|
+
let mut nav = NavigationState::new();
|
|
184
|
+
|
|
185
|
+
// Start at Boot
|
|
186
|
+
assert_eq!(nav.current_screen(), &Screen::Boot);
|
|
187
|
+
assert!(nav.is_on_base());
|
|
188
|
+
|
|
189
|
+
// Navigate to Main
|
|
190
|
+
nav.navigate_to_base(Screen::Main);
|
|
191
|
+
assert_eq!(nav.current_screen(), &Screen::Main);
|
|
192
|
+
|
|
193
|
+
// Push overlay
|
|
194
|
+
nav.push_overlay(Screen::AgentBuilder);
|
|
195
|
+
assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
|
|
196
|
+
assert!(nav.has_overlay());
|
|
197
|
+
assert!(!nav.is_on_base());
|
|
198
|
+
|
|
199
|
+
// Push popup
|
|
200
|
+
nav.push_popup(Screen::Help);
|
|
201
|
+
assert_eq!(nav.current_screen(), &Screen::Help);
|
|
202
|
+
assert!(nav.has_popup());
|
|
203
|
+
|
|
204
|
+
// Pop popup
|
|
205
|
+
nav.pop_popup();
|
|
206
|
+
assert_eq!(nav.current_screen(), &Screen::AgentBuilder);
|
|
207
|
+
|
|
208
|
+
// Pop overlay
|
|
209
|
+
nav.pop_overlay();
|
|
210
|
+
assert_eq!(nav.current_screen(), &Screen::Main);
|
|
211
|
+
assert!(nav.is_on_base());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn test_close_all() {
|
|
216
|
+
let mut nav = NavigationState::new();
|
|
217
|
+
nav.navigate_to_base(Screen::Main);
|
|
218
|
+
nav.push_overlay(Screen::AgentBuilder);
|
|
219
|
+
nav.push_popup(Screen::Help);
|
|
220
|
+
|
|
221
|
+
nav.close_all();
|
|
222
|
+
assert!(nav.is_on_base());
|
|
223
|
+
assert_eq!(nav.current_screen(), &Screen::Main);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -19,12 +19,10 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
19
19
|
let area = f.size();
|
|
20
20
|
|
|
21
21
|
if state.agent_list.detail_view.is_some() {
|
|
22
|
-
// Render
|
|
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,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
|
|
60
|
+
f.render_widget(content_block, chunks[1]);
|
|
63
61
|
|
|
64
62
|
let table_area = content_block.inner(chunks[1]);
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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);
|
|
115
|
+
f.render_widget(table, table_area);
|
|
116
|
+
}
|
|
113
117
|
|
|
114
118
|
// === FOOTER ===
|
|
115
119
|
let footer_text = Line::from(vec![
|
|
@@ -124,12 +128,17 @@ fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
|
|
|
124
128
|
f.render_widget(
|
|
125
129
|
Paragraph::new(footer_text)
|
|
126
130
|
.alignment(Alignment::Center)
|
|
127
|
-
.style(Style::default().bg(BG_PANEL))
|
|
131
|
+
.style(Style::default().bg(BG_PANEL))
|
|
132
|
+
.block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(TEXT_MUTED))),
|
|
128
133
|
chunks[2]
|
|
129
134
|
);
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
|
|
138
|
+
// First render list view (dimmed)
|
|
139
|
+
render_list_view(f, area, state);
|
|
140
|
+
|
|
141
|
+
// Then render detail popup on top
|
|
133
142
|
let detail_index = state.agent_list.detail_view.unwrap();
|
|
134
143
|
if let Some(agent) = state.agent_list.agents.get(detail_index) {
|
|
135
144
|
render_agent_detail(f, area, agent);
|
|
@@ -153,9 +162,16 @@ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
|
|
|
153
162
|
};
|
|
154
163
|
|
|
155
164
|
// Render dimmed overlay
|
|
156
|
-
|
|
157
|
-
.
|
|
158
|
-
|
|
165
|
+
for y in area.y..area.y + area.height {
|
|
166
|
+
for x in area.x..area.x + area.width {
|
|
167
|
+
if x < popup_x || x >= popup_x + popup_width || y < popup_y || y >= popup_y + popup_height {
|
|
168
|
+
f.render_widget(
|
|
169
|
+
Block::default().style(Style::default().bg(Color::Black)),
|
|
170
|
+
Rect { x, y, width: 1, height: 1 }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
159
175
|
|
|
160
176
|
// Render detail popup
|
|
161
177
|
let detail_block = Block::default()
|
|
@@ -165,7 +181,7 @@ fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
|
|
|
165
181
|
.border_style(Style::default().fg(CYBER_CYAN))
|
|
166
182
|
.style(Style::default().bg(BG_PANEL));
|
|
167
183
|
|
|
168
|
-
f.render_widget(detail_block
|
|
184
|
+
f.render_widget(detail_block, popup_area);
|
|
169
185
|
|
|
170
186
|
let inner = detail_block.inner(popup_area);
|
|
171
187
|
|
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,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(
|
|
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
|
-
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(
|
|
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
|
-
])),
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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(
|
|
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
|
-
])),
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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(" -
|
|
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(" -
|
|
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(" -
|
|
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(" -
|
|
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(" -
|
|
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(" -
|
|
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
|
|
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::
|
|
267
|
+
Span::raw(" = Next | "),
|
|
312
268
|
Span::styled("Backspace", Style::default().fg(NEON_GREEN)),
|
|
313
|
-
Span::
|
|
269
|
+
Span::raw(" = Prev | "),
|
|
314
270
|
Span::styled("ESC", Style::default().fg(NEON_GREEN)),
|
|
315
|
-
Span::
|
|
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::
|
|
280
|
+
Span::raw(" = Navigate | "),
|
|
325
281
|
Span::styled("F", Style::default().fg(NEON_GREEN)),
|
|
326
|
-
Span::
|
|
282
|
+
Span::raw(" = Filter | "),
|
|
327
283
|
Span::styled("S", Style::default().fg(NEON_GREEN)),
|
|
328
|
-
Span::
|
|
284
|
+
Span::raw(" = Sort | "),
|
|
329
285
|
Span::styled("R", Style::default().fg(NEON_GREEN)),
|
|
330
|
-
Span::
|
|
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::
|
|
295
|
+
Span::raw(" = Navigate | "),
|
|
340
296
|
Span::styled("Space", Style::default().fg(NEON_GREEN)),
|
|
341
|
-
Span::
|
|
297
|
+
Span::raw(" = Toggle | "),
|
|
342
298
|
Span::styled("Enter", Style::default().fg(NEON_GREEN)),
|
|
343
|
-
Span::
|
|
299
|
+
Span::raw(" = Save"),
|
|
344
300
|
]),
|
|
345
301
|
];
|
|
346
302
|
|
|
347
|
-
f.render_widget(controls_block
|
|
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 }),
|
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.0";
|
|
124
124
|
let brand_line = Line::from(vec![
|
|
125
125
|
Span::styled("4Runr.", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
|
|
126
126
|
Span::styled(" AI AGENT OS", Style::default().fg(BRAND_VIOLET)),
|
|
@@ -422,6 +422,7 @@ fn mem_color(value: f64) -> Color {
|
|
|
422
422
|
else if value > 0.6 { Color::Rgb(255, 191, 0) }
|
|
423
423
|
else { CYBER_CYAN }
|
|
424
424
|
}
|
|
425
|
+
|
|
425
426
|
fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
|
|
426
427
|
let panel_area = Rect {
|
|
427
428
|
x: area.x + 1,
|
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
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.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",
|