4runr-os 2.4.1 → 2.4.3
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 +111 -44
- package/mk3-tui/src/main.rs +35 -2
- package/mk3-tui/src/screens/mod.rs +3 -1
- package/mk3-tui/src/storage/mod.rs +1 -1
- package/mk3-tui/src/ui/agent_builder.rs +1 -1
- package/mk3-tui/src/ui/agent_list.rs +245 -0
- package/mk3-tui/src/ui/help.rs +353 -0
- package/mk3-tui/src/ui/layout.rs +2 -64
- package/mk3-tui/src/ui/mod.rs +2 -5
- package/package.json +2 -1
package/mk3-tui/src/app.rs
CHANGED
|
@@ -29,6 +29,35 @@ 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
|
+
|
|
32
61
|
#[derive(Debug, Clone)]
|
|
33
62
|
pub struct AppState {
|
|
34
63
|
// Navigation state (NEW - replaces simple mode)
|
|
@@ -96,6 +125,7 @@ pub struct AppState {
|
|
|
96
125
|
pub agent_builder: AgentBuilderState,
|
|
97
126
|
pub run_manager: RunManagerState,
|
|
98
127
|
pub settings: SettingsState,
|
|
128
|
+
pub agent_list: AgentListState,
|
|
99
129
|
|
|
100
130
|
// Local cache
|
|
101
131
|
pub cache: Option<Cache>,
|
|
@@ -153,6 +183,7 @@ impl Default for AppState {
|
|
|
153
183
|
agent_builder: AgentBuilderState::default(),
|
|
154
184
|
run_manager: RunManagerState::default(),
|
|
155
185
|
settings: SettingsState::default(),
|
|
186
|
+
agent_list: AgentListState::default(),
|
|
156
187
|
cache: Cache::new().ok(),
|
|
157
188
|
cache_loaded: false,
|
|
158
189
|
}
|
|
@@ -339,6 +370,11 @@ impl App {
|
|
|
339
370
|
return self.handle_settings_input(key);
|
|
340
371
|
}
|
|
341
372
|
|
|
373
|
+
// === AGENT LIST INPUT HANDLING ===
|
|
374
|
+
if self.state.navigation.current_screen() == &Screen::AgentList {
|
|
375
|
+
return self.handle_agent_list_input(key);
|
|
376
|
+
}
|
|
377
|
+
|
|
342
378
|
// === MAIN INPUT HANDLING ===
|
|
343
379
|
match key.code {
|
|
344
380
|
// Typing - ALL characters go to command input (with debounce)
|
|
@@ -362,48 +398,9 @@ impl App {
|
|
|
362
398
|
"quit" | "exit" => return Ok(true),
|
|
363
399
|
"clear" => self.state.logs.clear(),
|
|
364
400
|
"help" => {
|
|
365
|
-
|
|
366
|
-
self.
|
|
367
|
-
self.
|
|
368
|
-
self.state.logs.push_back("".into());
|
|
369
|
-
|
|
370
|
-
// Local Commands
|
|
371
|
-
self.state.logs.push_back("[HELP] Local Commands:".into());
|
|
372
|
-
self.state.logs.push_back(" quit, exit - Exit application".into());
|
|
373
|
-
self.state.logs.push_back(" clear - Clear logs".into());
|
|
374
|
-
self.state.logs.push_back(" help - Show this help".into());
|
|
375
|
-
self.state.logs.push_back(" :perf - Show performance stats".into());
|
|
376
|
-
self.state.logs.push_back("".into());
|
|
377
|
-
|
|
378
|
-
// Navigation Commands
|
|
379
|
-
self.state.logs.push_back("[HELP] Navigation Commands:".into());
|
|
380
|
-
self.state.logs.push_back(" build - Open Agent Builder (6-step wizard)".into());
|
|
381
|
-
self.state.logs.push_back(" runs - Open Run Manager (list, filter, sort)".into());
|
|
382
|
-
self.state.logs.push_back(" config, settings - Open Settings (mode, AI provider)".into());
|
|
383
|
-
self.state.logs.push_back(" ESC - Close overlay/popup or clear input".into());
|
|
384
|
-
self.state.logs.push_back("".into());
|
|
385
|
-
|
|
386
|
-
// Screen Controls
|
|
387
|
-
self.state.logs.push_back("[HELP] Screen Controls:".into());
|
|
388
|
-
self.state.logs.push_back(" Agent Builder: Enter=Next, Backspace=Prev, ESC=Cancel".into());
|
|
389
|
-
self.state.logs.push_back(" Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh".into());
|
|
390
|
-
self.state.logs.push_back(" Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save".into());
|
|
391
|
-
self.state.logs.push_back("".into());
|
|
392
|
-
|
|
393
|
-
// WebSocket Commands - Label BEFORE separator
|
|
394
|
-
self.state.logs.push_back("[HELP] WebSocket Commands (requires connection):".into());
|
|
395
|
-
self.state.logs.push_back("[HELP] ─────────────────────────────────────────".into());
|
|
396
|
-
self.state.logs.push_back(" agent.list - List all agents".into());
|
|
397
|
-
self.state.logs.push_back(" agent.get - Get agent details (data: {name})".into());
|
|
398
|
-
self.state.logs.push_back(" agent.create - Create agent (use Agent Builder)".into());
|
|
399
|
-
self.state.logs.push_back(" agent.delete - Delete agent (data: {name})".into());
|
|
400
|
-
self.state.logs.push_back(" system.status - Get system status".into());
|
|
401
|
-
self.state.logs.push_back(" run.list - List runs (requires gateway)".into());
|
|
402
|
-
self.state.logs.push_back(" tool.list - List available tools".into());
|
|
403
|
-
self.state.logs.push_back("".into());
|
|
404
|
-
|
|
405
|
-
self.state.logs.push_back("[HELP] Press F12 to toggle performance overlay".into());
|
|
406
|
-
self.state.logs.push_back("[HELP] ═══════════════════════════════════════".into());
|
|
401
|
+
// Open help popup instead of logging to operations log
|
|
402
|
+
self.push_popup(Screen::Help);
|
|
403
|
+
self.request_render("help_command");
|
|
407
404
|
}
|
|
408
405
|
":perf" => {
|
|
409
406
|
// Perf self-check command
|
|
@@ -591,6 +588,10 @@ impl App {
|
|
|
591
588
|
use crate::ui::settings;
|
|
592
589
|
settings::render(f, &self.state);
|
|
593
590
|
}
|
|
591
|
+
Screen::AgentList => {
|
|
592
|
+
use crate::ui::agent_list;
|
|
593
|
+
agent_list::render(f, &self.state);
|
|
594
|
+
}
|
|
594
595
|
Screen::Confirmation { message, action } => {
|
|
595
596
|
// TODO: Implement confirmation popup
|
|
596
597
|
// For now, just render the base screen
|
|
@@ -606,10 +607,13 @@ impl App {
|
|
|
606
607
|
let _ = message; // Suppress warning
|
|
607
608
|
}
|
|
608
609
|
Screen::Help => {
|
|
609
|
-
//
|
|
610
|
-
// For now, just render the base screen
|
|
610
|
+
// Render base screen first (dimmed)
|
|
611
611
|
use crate::ui::layout;
|
|
612
612
|
layout::render(f, &self.state);
|
|
613
|
+
|
|
614
|
+
// Render help popup overlay
|
|
615
|
+
use crate::ui::help;
|
|
616
|
+
help::render_help(f, f.size(), &self.state);
|
|
613
617
|
}
|
|
614
618
|
}
|
|
615
619
|
}
|
|
@@ -1108,6 +1112,69 @@ impl App {
|
|
|
1108
1112
|
Ok(false)
|
|
1109
1113
|
}
|
|
1110
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
|
+
|
|
1111
1178
|
// ============================================================
|
|
1112
1179
|
// SETTINGS INPUT HANDLING (Step 4.8)
|
|
1113
1180
|
// ============================================================
|
package/mk3-tui/src/main.rs
CHANGED
|
@@ -15,6 +15,7 @@ mod websocket;
|
|
|
15
15
|
|
|
16
16
|
use app::App;
|
|
17
17
|
use io::IoHandler;
|
|
18
|
+
use screens::Screen;
|
|
18
19
|
use websocket::{WebSocketClient, WsClientMessage};
|
|
19
20
|
|
|
20
21
|
fn main() -> Result<()> {
|
|
@@ -151,12 +152,35 @@ fn main() -> Result<()> {
|
|
|
151
152
|
})
|
|
152
153
|
.collect();
|
|
153
154
|
|
|
155
|
+
// Parse agents into AgentInfo structs for Agent List viewer
|
|
156
|
+
use crate::app::AgentInfo;
|
|
157
|
+
let agents: Vec<AgentInfo> = agents_array.iter()
|
|
158
|
+
.filter_map(|agent| {
|
|
159
|
+
let obj = agent.as_object()?;
|
|
160
|
+
Some(AgentInfo {
|
|
161
|
+
name: obj.get("name")?.as_str()?.to_string(),
|
|
162
|
+
description: obj.get("description").and_then(|d| d.as_str()).map(|s| s.to_string()),
|
|
163
|
+
model: obj.get("model").and_then(|m| m.as_str()).unwrap_or("unknown").to_string(),
|
|
164
|
+
provider: obj.get("provider").and_then(|p| p.as_str()).unwrap_or("unknown").to_string(),
|
|
165
|
+
system_prompt: obj.get("systemPrompt").and_then(|sp| sp.as_str()).map(|s| s.to_string()),
|
|
166
|
+
temperature: obj.get("temperature").and_then(|t| t.as_f64()).map(|f| f as f32),
|
|
167
|
+
max_tokens: obj.get("maxTokens").and_then(|mt| mt.as_u64()).map(|u| u as u32),
|
|
168
|
+
tools: obj.get("tools")
|
|
169
|
+
.and_then(|t| t.as_array())
|
|
170
|
+
.map(|arr| arr.iter()
|
|
171
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
172
|
+
.collect())
|
|
173
|
+
.unwrap_or_default(),
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
.collect();
|
|
177
|
+
|
|
154
178
|
// Update cache with agent data
|
|
155
179
|
if let Some(cache) = &mut app.state.cache {
|
|
156
180
|
use crate::storage::cache::AgentData;
|
|
157
181
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
158
182
|
|
|
159
|
-
let
|
|
183
|
+
let cache_agents: Vec<AgentData> = agents_array.iter()
|
|
160
184
|
.filter_map(|agent| {
|
|
161
185
|
let obj = agent.as_object()?;
|
|
162
186
|
Some(AgentData {
|
|
@@ -169,9 +193,18 @@ fn main() -> Result<()> {
|
|
|
169
193
|
})
|
|
170
194
|
.collect();
|
|
171
195
|
|
|
172
|
-
let _ = cache.update_agents(
|
|
196
|
+
let _ = cache.update_agents(cache_agents);
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
// Update agent list state
|
|
200
|
+
app.state.agent_list.agents = agents;
|
|
201
|
+
app.state.agent_list.selected_index = 0;
|
|
202
|
+
app.state.agent_list.detail_view = None;
|
|
203
|
+
|
|
204
|
+
// Open AgentList overlay
|
|
205
|
+
app.push_overlay(Screen::AgentList);
|
|
206
|
+
app.request_render("agent_list_opened");
|
|
207
|
+
|
|
175
208
|
// Only log if agents were actually loaded
|
|
176
209
|
if agents_array.len() > 0 {
|
|
177
210
|
app.add_log(format!(
|
|
@@ -19,6 +19,7 @@ pub enum Screen {
|
|
|
19
19
|
AgentBuilder,
|
|
20
20
|
RunManager,
|
|
21
21
|
Settings,
|
|
22
|
+
AgentList,
|
|
22
23
|
|
|
23
24
|
// Popup screens (small overlays)
|
|
24
25
|
Confirmation { message: String, action: String },
|
|
@@ -31,7 +32,7 @@ impl Screen {
|
|
|
31
32
|
pub fn is_overlay(&self) -> bool {
|
|
32
33
|
matches!(
|
|
33
34
|
self,
|
|
34
|
-
Screen::AgentBuilder | Screen::RunManager | Screen::Settings
|
|
35
|
+
Screen::AgentBuilder | Screen::RunManager | Screen::Settings | Screen::AgentList
|
|
35
36
|
)
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -56,6 +57,7 @@ impl Screen {
|
|
|
56
57
|
Screen::AgentBuilder => "Agent Builder",
|
|
57
58
|
Screen::RunManager => "Run Manager",
|
|
58
59
|
Screen::Settings => "Settings",
|
|
60
|
+
Screen::AgentList => "Agent List",
|
|
59
61
|
Screen::Confirmation { .. } => "Confirmation",
|
|
60
62
|
Screen::Alert { .. } => "Alert",
|
|
61
63
|
Screen::Help => "Help",
|
|
@@ -108,7 +108,7 @@ struct CostEstimate {
|
|
|
108
108
|
is_free: bool,
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
fn calculate_cost(provider: &str, model: &str,
|
|
111
|
+
fn calculate_cost(provider: &str, model: &str, _max_tokens: u32) -> CostEstimate {
|
|
112
112
|
let pricing = get_model_pricing(provider, model);
|
|
113
113
|
let is_free = pricing.input_cost == 0.0 && pricing.output_cost == 0.0;
|
|
114
114
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/// Agent List Screen
|
|
2
|
+
/// View and manage AI agents
|
|
3
|
+
|
|
4
|
+
use ratatui::prelude::*;
|
|
5
|
+
use ratatui::widgets::{Block, Borders, Paragraph, Wrap, Table, Row, Cell};
|
|
6
|
+
use crate::app::{AppState, AgentInfo};
|
|
7
|
+
|
|
8
|
+
// === 4RUNR BRAND COLORS ===
|
|
9
|
+
const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
|
|
10
|
+
const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
|
|
11
|
+
const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
|
|
12
|
+
const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
|
|
13
|
+
const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
|
|
14
|
+
const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
|
|
15
|
+
const BG_PANEL: Color = Color::Rgb(18, 18, 25);
|
|
16
|
+
|
|
17
|
+
/// Render agent list screen
|
|
18
|
+
pub fn render(f: &mut Frame, state: &AppState) {
|
|
19
|
+
let area = f.size();
|
|
20
|
+
|
|
21
|
+
if state.agent_list.detail_view.is_some() {
|
|
22
|
+
// Render list view first (as base)
|
|
23
|
+
render_list_view(f, area, state);
|
|
24
|
+
// Then render detail popup on top
|
|
25
|
+
render_detail_popup(f, area, state);
|
|
26
|
+
} else {
|
|
27
|
+
// Render list view only
|
|
28
|
+
render_list_view(f, area, state);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn render_list_view(f: &mut Frame, area: Rect, state: &AppState) {
|
|
33
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
34
|
+
|
|
35
|
+
// Split into header, content, footer
|
|
36
|
+
let chunks = Layout::default()
|
|
37
|
+
.direction(Direction::Vertical)
|
|
38
|
+
.constraints([
|
|
39
|
+
Constraint::Length(3), // Header
|
|
40
|
+
Constraint::Min(0), // Content
|
|
41
|
+
Constraint::Length(3), // Footer
|
|
42
|
+
])
|
|
43
|
+
.split(area);
|
|
44
|
+
|
|
45
|
+
// === HEADER ===
|
|
46
|
+
let agent_count = state.agent_list.agents.len();
|
|
47
|
+
let header_block = Block::default()
|
|
48
|
+
.title(format!(" 🤖 Agent List ({} agents) ", agent_count))
|
|
49
|
+
.title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
|
|
50
|
+
.borders(Borders::ALL)
|
|
51
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
52
|
+
.style(Style::default().bg(BG_PANEL));
|
|
53
|
+
|
|
54
|
+
f.render_widget(header_block, chunks[0]);
|
|
55
|
+
|
|
56
|
+
// === CONTENT: Agent Table ===
|
|
57
|
+
let content_block = Block::default()
|
|
58
|
+
.borders(Borders::ALL)
|
|
59
|
+
.border_style(Style::default().fg(TEXT_MUTED))
|
|
60
|
+
.style(Style::default().bg(BG_PANEL));
|
|
61
|
+
|
|
62
|
+
f.render_widget(content_block.clone(), chunks[1]);
|
|
63
|
+
|
|
64
|
+
let table_area = content_block.inner(chunks[1]);
|
|
65
|
+
|
|
66
|
+
// Create table rows
|
|
67
|
+
let rows: Vec<Row> = state.agent_list.agents.iter().enumerate().map(|(i, agent)| {
|
|
68
|
+
let is_selected = i == state.agent_list.selected_index;
|
|
69
|
+
let style = if is_selected {
|
|
70
|
+
Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)
|
|
71
|
+
} else {
|
|
72
|
+
Style::default().fg(TEXT_PRIMARY)
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let name = if is_selected {
|
|
76
|
+
format!("▶ {}", agent.name)
|
|
77
|
+
} else {
|
|
78
|
+
format!(" {}", agent.name)
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let desc = agent.description.as_deref().unwrap_or("No description")
|
|
82
|
+
.chars().take(30).collect::<String>();
|
|
83
|
+
let model = agent.model.chars().take(15).collect::<String>();
|
|
84
|
+
let provider = agent.provider.chars().take(10).collect::<String>();
|
|
85
|
+
|
|
86
|
+
Row::new(vec![
|
|
87
|
+
Cell::from(name).style(style),
|
|
88
|
+
Cell::from(desc).style(style),
|
|
89
|
+
Cell::from(model).style(style),
|
|
90
|
+
Cell::from(provider).style(style),
|
|
91
|
+
])
|
|
92
|
+
}).collect();
|
|
93
|
+
|
|
94
|
+
let table = Table::new(
|
|
95
|
+
rows,
|
|
96
|
+
[
|
|
97
|
+
Constraint::Percentage(25),
|
|
98
|
+
Constraint::Percentage(35),
|
|
99
|
+
Constraint::Percentage(20),
|
|
100
|
+
Constraint::Percentage(20),
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
.header(Row::new(vec![
|
|
104
|
+
Cell::from("Name").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
105
|
+
Cell::from("Description").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
106
|
+
Cell::from("Model").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
107
|
+
Cell::from("Provider").style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
108
|
+
]))
|
|
109
|
+
.column_spacing(1)
|
|
110
|
+
.style(Style::default().fg(TEXT_PRIMARY));
|
|
111
|
+
|
|
112
|
+
f.render_widget(table, table_area);
|
|
113
|
+
|
|
114
|
+
// === FOOTER ===
|
|
115
|
+
let footer_text = Line::from(vec![
|
|
116
|
+
Span::styled("↑/↓", Style::default().fg(CYBER_CYAN)),
|
|
117
|
+
Span::styled(" Navigate | ", Style::default().fg(TEXT_DIM)),
|
|
118
|
+
Span::styled("Enter", Style::default().fg(NEON_GREEN)),
|
|
119
|
+
Span::styled(" View Details | ", Style::default().fg(TEXT_DIM)),
|
|
120
|
+
Span::styled("ESC", Style::default().fg(BRAND_PURPLE)),
|
|
121
|
+
Span::styled(" Close", Style::default().fg(TEXT_DIM)),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
f.render_widget(
|
|
125
|
+
Paragraph::new(footer_text)
|
|
126
|
+
.alignment(Alignment::Center)
|
|
127
|
+
.style(Style::default().bg(BG_PANEL)),
|
|
128
|
+
chunks[2]
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn render_detail_popup(f: &mut Frame, area: Rect, state: &AppState) {
|
|
133
|
+
let detail_index = state.agent_list.detail_view.unwrap();
|
|
134
|
+
if let Some(agent) = state.agent_list.agents.get(detail_index) {
|
|
135
|
+
render_agent_detail(f, area, agent);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn render_agent_detail(f: &mut Frame, area: Rect, agent: &AgentInfo) {
|
|
140
|
+
// Calculate popup size (70% width, 80% height, centered)
|
|
141
|
+
let popup_width = (area.width * 70 / 100).max(50);
|
|
142
|
+
let popup_height = (area.height * 80 / 100).max(15);
|
|
143
|
+
let popup_x = (area.width.saturating_sub(popup_width)) / 2;
|
|
144
|
+
let popup_y = (area.height.saturating_sub(popup_height)) / 2;
|
|
145
|
+
|
|
146
|
+
let popup_area = Rect {
|
|
147
|
+
x: popup_x,
|
|
148
|
+
y: popup_y,
|
|
149
|
+
width: popup_width,
|
|
150
|
+
height: popup_height,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Render dimmed overlay
|
|
154
|
+
let overlay = Block::default()
|
|
155
|
+
.style(Style::default().bg(Color::Black).fg(Color::Black));
|
|
156
|
+
f.render_widget(overlay, area);
|
|
157
|
+
|
|
158
|
+
// Render detail popup
|
|
159
|
+
let detail_block = Block::default()
|
|
160
|
+
.title(format!(" 📋 Agent Details: {} ", agent.name))
|
|
161
|
+
.title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
|
|
162
|
+
.borders(Borders::ALL)
|
|
163
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
164
|
+
.style(Style::default().bg(BG_PANEL));
|
|
165
|
+
|
|
166
|
+
f.render_widget(detail_block.clone(), popup_area);
|
|
167
|
+
|
|
168
|
+
let inner = detail_block.inner(popup_area);
|
|
169
|
+
|
|
170
|
+
// Create detail text
|
|
171
|
+
let mut detail_lines = vec![
|
|
172
|
+
Line::from(vec![
|
|
173
|
+
Span::styled("Name: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
174
|
+
Span::styled(&agent.name, Style::default().fg(NEON_GREEN)),
|
|
175
|
+
]),
|
|
176
|
+
Line::from(""),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
if let Some(desc) = &agent.description {
|
|
180
|
+
detail_lines.push(Line::from(vec![
|
|
181
|
+
Span::styled("Description: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
182
|
+
Span::styled(desc, Style::default().fg(TEXT_PRIMARY)),
|
|
183
|
+
]));
|
|
184
|
+
detail_lines.push(Line::from(""));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
detail_lines.push(Line::from(vec![
|
|
188
|
+
Span::styled("Model: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
189
|
+
Span::styled(&agent.model, Style::default().fg(TEXT_PRIMARY)),
|
|
190
|
+
]));
|
|
191
|
+
detail_lines.push(Line::from(vec![
|
|
192
|
+
Span::styled("Provider: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
193
|
+
Span::styled(&agent.provider, Style::default().fg(TEXT_PRIMARY)),
|
|
194
|
+
]));
|
|
195
|
+
|
|
196
|
+
if let Some(temp) = agent.temperature {
|
|
197
|
+
detail_lines.push(Line::from(vec![
|
|
198
|
+
Span::styled("Temperature: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
199
|
+
Span::styled(format!("{:.2}", temp), Style::default().fg(TEXT_PRIMARY)),
|
|
200
|
+
]));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if let Some(max_tokens) = agent.max_tokens {
|
|
204
|
+
detail_lines.push(Line::from(vec![
|
|
205
|
+
Span::styled("Max Tokens: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
206
|
+
Span::styled(format!("{}", max_tokens), Style::default().fg(TEXT_PRIMARY)),
|
|
207
|
+
]));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if !agent.tools.is_empty() {
|
|
211
|
+
detail_lines.push(Line::from(""));
|
|
212
|
+
detail_lines.push(Line::from(vec![
|
|
213
|
+
Span::styled("Tools: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
214
|
+
]));
|
|
215
|
+
for tool in &agent.tools {
|
|
216
|
+
detail_lines.push(Line::from(vec![
|
|
217
|
+
Span::raw(" • "),
|
|
218
|
+
Span::styled(tool, Style::default().fg(NEON_GREEN)),
|
|
219
|
+
]));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if let Some(prompt) = &agent.system_prompt {
|
|
224
|
+
detail_lines.push(Line::from(""));
|
|
225
|
+
detail_lines.push(Line::from(vec![
|
|
226
|
+
Span::styled("System Prompt: ", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
227
|
+
]));
|
|
228
|
+
// Show first 200 chars of prompt
|
|
229
|
+
let prompt_preview = prompt.chars().take(200).collect::<String>();
|
|
230
|
+
detail_lines.push(Line::from(format!(" {}", prompt_preview)).style(Style::default().fg(TEXT_DIM)));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
detail_lines.push(Line::from(""));
|
|
234
|
+
detail_lines.push(Line::from(vec![
|
|
235
|
+
Span::styled("Press ", Style::default().fg(TEXT_DIM)),
|
|
236
|
+
Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
237
|
+
Span::styled(" to close", Style::default().fg(TEXT_DIM)),
|
|
238
|
+
]));
|
|
239
|
+
|
|
240
|
+
f.render_widget(
|
|
241
|
+
Paragraph::new(detail_lines)
|
|
242
|
+
.wrap(Wrap { trim: false }),
|
|
243
|
+
inner
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/// Help popup screen
|
|
2
|
+
/// Professional, organized command reference
|
|
3
|
+
|
|
4
|
+
use ratatui::prelude::*;
|
|
5
|
+
use ratatui::widgets::{Block, Borders, Paragraph, Wrap, List, ListItem};
|
|
6
|
+
use crate::app::AppState;
|
|
7
|
+
|
|
8
|
+
// === 4RUNR BRAND COLORS ===
|
|
9
|
+
const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
|
|
10
|
+
const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
|
|
11
|
+
const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
|
|
12
|
+
const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
|
|
13
|
+
const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
|
|
14
|
+
const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
|
|
15
|
+
const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
|
|
16
|
+
const BG_PANEL: Color = Color::Rgb(18, 18, 25);
|
|
17
|
+
|
|
18
|
+
/// Render help popup overlay
|
|
19
|
+
pub fn render_help(f: &mut Frame, area: Rect, _state: &AppState) {
|
|
20
|
+
// Calculate popup size (80% width, 85% height, centered)
|
|
21
|
+
let popup_width = (area.width * 80 / 100).max(60);
|
|
22
|
+
let popup_height = (area.height * 85 / 100).max(20);
|
|
23
|
+
let popup_x = (area.width.saturating_sub(popup_width)) / 2;
|
|
24
|
+
let popup_y = (area.height.saturating_sub(popup_height)) / 2;
|
|
25
|
+
|
|
26
|
+
let popup_area = Rect {
|
|
27
|
+
x: popup_x,
|
|
28
|
+
y: popup_y,
|
|
29
|
+
width: popup_width,
|
|
30
|
+
height: popup_height,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Render background overlay (dimmed)
|
|
34
|
+
let overlay = Block::default()
|
|
35
|
+
.style(Style::default().bg(Color::Black).fg(Color::Black));
|
|
36
|
+
f.render_widget(overlay, area);
|
|
37
|
+
|
|
38
|
+
// Render help popup
|
|
39
|
+
render_help_content(f, popup_area);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn render_help_content(f: &mut Frame, area: Rect) {
|
|
43
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
44
|
+
|
|
45
|
+
// Split into sections
|
|
46
|
+
let chunks = Layout::default()
|
|
47
|
+
.direction(Direction::Vertical)
|
|
48
|
+
.constraints([
|
|
49
|
+
Constraint::Length(3), // Header
|
|
50
|
+
Constraint::Min(0), // Content
|
|
51
|
+
Constraint::Length(1), // Footer
|
|
52
|
+
])
|
|
53
|
+
.split(area);
|
|
54
|
+
|
|
55
|
+
// === HEADER ===
|
|
56
|
+
let header_block = Block::default()
|
|
57
|
+
.title(" 📖 4Runr AI Agent OS - Command Reference ")
|
|
58
|
+
.title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
|
|
59
|
+
.borders(Borders::ALL)
|
|
60
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
61
|
+
.style(Style::default().bg(BG_PANEL));
|
|
62
|
+
|
|
63
|
+
let header_text = Line::from(vec![
|
|
64
|
+
Span::styled("Press ", Style::default().fg(TEXT_DIM)),
|
|
65
|
+
Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
66
|
+
Span::styled(" to close", Style::default().fg(TEXT_DIM)),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
f.render_widget(header_block, chunks[0]);
|
|
70
|
+
f.render_widget(
|
|
71
|
+
Paragraph::new(header_text).alignment(Alignment::Right),
|
|
72
|
+
Rect {
|
|
73
|
+
x: chunks[0].x + 1,
|
|
74
|
+
y: chunks[0].y + 1,
|
|
75
|
+
width: chunks[0].width.saturating_sub(2),
|
|
76
|
+
height: 1,
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// === CONTENT ===
|
|
81
|
+
let content_chunks = Layout::default()
|
|
82
|
+
.direction(Direction::Horizontal)
|
|
83
|
+
.constraints([
|
|
84
|
+
Constraint::Percentage(40), // Left: Command list
|
|
85
|
+
Constraint::Percentage(60), // Right: Details
|
|
86
|
+
])
|
|
87
|
+
.split(chunks[1]);
|
|
88
|
+
|
|
89
|
+
// Left panel: Command categories
|
|
90
|
+
render_command_list(f, content_chunks[0]);
|
|
91
|
+
|
|
92
|
+
// Right panel: Command details
|
|
93
|
+
render_command_details(f, content_chunks[1]);
|
|
94
|
+
|
|
95
|
+
// === FOOTER ===
|
|
96
|
+
let footer_text = Line::from(vec![
|
|
97
|
+
Span::styled("Navigate: ", Style::default().fg(TEXT_DIM)),
|
|
98
|
+
Span::styled("↑/↓", Style::default().fg(CYBER_CYAN)),
|
|
99
|
+
Span::styled(" Select | ", Style::default().fg(TEXT_MUTED)),
|
|
100
|
+
Span::styled("ESC", Style::default().fg(NEON_GREEN)),
|
|
101
|
+
Span::styled(" Close", Style::default().fg(TEXT_DIM)),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
f.render_widget(
|
|
105
|
+
Paragraph::new(footer_text)
|
|
106
|
+
.alignment(Alignment::Center)
|
|
107
|
+
.style(Style::default().bg(BG_PANEL)),
|
|
108
|
+
chunks[2]
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn render_command_list(f: &mut Frame, area: Rect) {
|
|
113
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
114
|
+
|
|
115
|
+
let chunks = Layout::default()
|
|
116
|
+
.direction(Direction::Vertical)
|
|
117
|
+
.constraints([
|
|
118
|
+
Constraint::Length(8), // Navigation Commands
|
|
119
|
+
Constraint::Length(7), // Local Commands
|
|
120
|
+
Constraint::Min(0), // WebSocket Commands
|
|
121
|
+
])
|
|
122
|
+
.split(area);
|
|
123
|
+
|
|
124
|
+
// Navigation Commands section
|
|
125
|
+
let nav_block = Block::default()
|
|
126
|
+
.title(" 🧭 Navigation ")
|
|
127
|
+
.title_style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD))
|
|
128
|
+
.borders(Borders::ALL)
|
|
129
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
130
|
+
.style(Style::default().bg(BG_PANEL));
|
|
131
|
+
|
|
132
|
+
let nav_items = vec![
|
|
133
|
+
ListItem::new(Line::from(vec![
|
|
134
|
+
Span::styled("build", Style::default().fg(NEON_GREEN)),
|
|
135
|
+
])),
|
|
136
|
+
ListItem::new(Line::from(vec![
|
|
137
|
+
Span::styled("runs", Style::default().fg(NEON_GREEN)),
|
|
138
|
+
])),
|
|
139
|
+
ListItem::new(Line::from(vec![
|
|
140
|
+
Span::styled("config", Style::default().fg(NEON_GREEN)),
|
|
141
|
+
Span::styled(", ", Style::default().fg(TEXT_DIM)),
|
|
142
|
+
Span::styled("settings", Style::default().fg(NEON_GREEN)),
|
|
143
|
+
])),
|
|
144
|
+
ListItem::new(Line::from(vec![
|
|
145
|
+
Span::styled("ESC", Style::default().fg(NEON_GREEN)),
|
|
146
|
+
])),
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
f.render_widget(nav_block.clone(), chunks[0]);
|
|
150
|
+
f.render_widget(
|
|
151
|
+
List::new(nav_items)
|
|
152
|
+
.style(Style::default().fg(TEXT_PRIMARY)),
|
|
153
|
+
nav_block.inner(chunks[0])
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Local Commands section
|
|
157
|
+
let local_block = Block::default()
|
|
158
|
+
.title(" 💻 Local ")
|
|
159
|
+
.title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
|
|
160
|
+
.borders(Borders::ALL)
|
|
161
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
162
|
+
.style(Style::default().bg(BG_PANEL));
|
|
163
|
+
|
|
164
|
+
let local_items = vec![
|
|
165
|
+
ListItem::new(Line::from(vec![
|
|
166
|
+
Span::styled("quit", Style::default().fg(NEON_GREEN)),
|
|
167
|
+
Span::styled(", ", Style::default().fg(TEXT_DIM)),
|
|
168
|
+
Span::styled("exit", Style::default().fg(NEON_GREEN)),
|
|
169
|
+
])),
|
|
170
|
+
ListItem::new(Line::from(vec![
|
|
171
|
+
Span::styled("clear", Style::default().fg(NEON_GREEN)),
|
|
172
|
+
])),
|
|
173
|
+
ListItem::new(Line::from(vec![
|
|
174
|
+
Span::styled("help", Style::default().fg(NEON_GREEN)),
|
|
175
|
+
])),
|
|
176
|
+
ListItem::new(Line::from(vec![
|
|
177
|
+
Span::styled(":perf", Style::default().fg(NEON_GREEN)),
|
|
178
|
+
])),
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
f.render_widget(local_block.clone(), chunks[1]);
|
|
182
|
+
f.render_widget(
|
|
183
|
+
List::new(local_items)
|
|
184
|
+
.style(Style::default().fg(TEXT_PRIMARY)),
|
|
185
|
+
local_block.inner(chunks[1])
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// WebSocket Commands section
|
|
189
|
+
let ws_block = Block::default()
|
|
190
|
+
.title(" 🌐 WebSocket (requires connection) ")
|
|
191
|
+
.title_style(Style::default().fg(AMBER_WARN).add_modifier(Modifier::BOLD))
|
|
192
|
+
.borders(Borders::ALL)
|
|
193
|
+
.border_style(Style::default().fg(AMBER_WARN))
|
|
194
|
+
.style(Style::default().bg(BG_PANEL));
|
|
195
|
+
|
|
196
|
+
let ws_items = vec![
|
|
197
|
+
ListItem::new(Line::from(vec![
|
|
198
|
+
Span::styled("agent.list", Style::default().fg(NEON_GREEN)),
|
|
199
|
+
])),
|
|
200
|
+
ListItem::new(Line::from(vec![
|
|
201
|
+
Span::styled("agent.get", Style::default().fg(NEON_GREEN)),
|
|
202
|
+
])),
|
|
203
|
+
ListItem::new(Line::from(vec![
|
|
204
|
+
Span::styled("agent.create", Style::default().fg(NEON_GREEN)),
|
|
205
|
+
])),
|
|
206
|
+
ListItem::new(Line::from(vec![
|
|
207
|
+
Span::styled("agent.delete", Style::default().fg(NEON_GREEN)),
|
|
208
|
+
])),
|
|
209
|
+
ListItem::new(Line::from(vec![
|
|
210
|
+
Span::styled("system.status", Style::default().fg(NEON_GREEN)),
|
|
211
|
+
])),
|
|
212
|
+
ListItem::new(Line::from(vec![
|
|
213
|
+
Span::styled("run.list", Style::default().fg(NEON_GREEN)),
|
|
214
|
+
])),
|
|
215
|
+
ListItem::new(Line::from(vec![
|
|
216
|
+
Span::styled("tool.list", Style::default().fg(NEON_GREEN)),
|
|
217
|
+
])),
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
f.render_widget(ws_block.clone(), chunks[2]);
|
|
221
|
+
f.render_widget(
|
|
222
|
+
List::new(ws_items)
|
|
223
|
+
.style(Style::default().fg(TEXT_PRIMARY)),
|
|
224
|
+
ws_block.inner(chunks[2])
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn render_command_details(f: &mut Frame, area: Rect) {
|
|
229
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
230
|
+
|
|
231
|
+
let chunks = Layout::default()
|
|
232
|
+
.direction(Direction::Vertical)
|
|
233
|
+
.constraints([
|
|
234
|
+
Constraint::Percentage(50), // Command descriptions
|
|
235
|
+
Constraint::Percentage(50), // Screen controls
|
|
236
|
+
])
|
|
237
|
+
.split(area);
|
|
238
|
+
|
|
239
|
+
// Command descriptions
|
|
240
|
+
let desc_block = Block::default()
|
|
241
|
+
.title(" 📋 Command Details ")
|
|
242
|
+
.title_style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD))
|
|
243
|
+
.borders(Borders::ALL)
|
|
244
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
245
|
+
.style(Style::default().bg(BG_PANEL));
|
|
246
|
+
|
|
247
|
+
let desc_text = vec![
|
|
248
|
+
Line::from(vec![
|
|
249
|
+
Span::styled("build", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
250
|
+
Span::styled(" - Open Agent Builder (6-step wizard)", Style::default().fg(TEXT_PRIMARY)),
|
|
251
|
+
]),
|
|
252
|
+
Line::from(""),
|
|
253
|
+
Line::from(vec![
|
|
254
|
+
Span::styled("runs", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
255
|
+
Span::styled(" - Open Run Manager (list, filter, sort)", Style::default().fg(TEXT_PRIMARY)),
|
|
256
|
+
]),
|
|
257
|
+
Line::from(""),
|
|
258
|
+
Line::from(vec![
|
|
259
|
+
Span::styled("config, settings", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
260
|
+
Span::styled(" - Open Settings (mode, AI provider)", Style::default().fg(TEXT_PRIMARY)),
|
|
261
|
+
]),
|
|
262
|
+
Line::from(""),
|
|
263
|
+
Line::from(vec![
|
|
264
|
+
Span::styled("ESC", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
265
|
+
Span::styled(" - Close overlay/popup or clear input", Style::default().fg(TEXT_PRIMARY)),
|
|
266
|
+
]),
|
|
267
|
+
Line::from(""),
|
|
268
|
+
Line::from(vec![
|
|
269
|
+
Span::styled("quit, exit", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
270
|
+
Span::styled(" - Exit application", Style::default().fg(TEXT_PRIMARY)),
|
|
271
|
+
]),
|
|
272
|
+
Line::from(""),
|
|
273
|
+
Line::from(vec![
|
|
274
|
+
Span::styled("clear", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
275
|
+
Span::styled(" - Clear operations log", Style::default().fg(TEXT_PRIMARY)),
|
|
276
|
+
]),
|
|
277
|
+
Line::from(""),
|
|
278
|
+
Line::from(vec![
|
|
279
|
+
Span::styled("help", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
280
|
+
Span::styled(" - Show this help", Style::default().fg(TEXT_PRIMARY)),
|
|
281
|
+
]),
|
|
282
|
+
Line::from(""),
|
|
283
|
+
Line::from(vec![
|
|
284
|
+
Span::styled(":perf", Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD)),
|
|
285
|
+
Span::styled(" - Show performance stats", Style::default().fg(TEXT_PRIMARY)),
|
|
286
|
+
]),
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
f.render_widget(desc_block.clone(), chunks[0]);
|
|
290
|
+
f.render_widget(
|
|
291
|
+
Paragraph::new(desc_text)
|
|
292
|
+
.wrap(Wrap { trim: false }),
|
|
293
|
+
desc_block.inner(chunks[0])
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Screen controls
|
|
297
|
+
let controls_block = Block::default()
|
|
298
|
+
.title(" ⌨️ Screen Controls ")
|
|
299
|
+
.title_style(Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD))
|
|
300
|
+
.borders(Borders::ALL)
|
|
301
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
302
|
+
.style(Style::default().bg(BG_PANEL));
|
|
303
|
+
|
|
304
|
+
let controls_text = vec![
|
|
305
|
+
Line::from(vec![
|
|
306
|
+
Span::styled("Agent Builder:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
307
|
+
]),
|
|
308
|
+
Line::from(vec![
|
|
309
|
+
Span::raw(" "),
|
|
310
|
+
Span::styled("Enter", Style::default().fg(NEON_GREEN)),
|
|
311
|
+
Span::styled(" = Next | ", Style::default().fg(TEXT_DIM)),
|
|
312
|
+
Span::styled("Backspace", Style::default().fg(NEON_GREEN)),
|
|
313
|
+
Span::styled(" = Prev | ", Style::default().fg(TEXT_DIM)),
|
|
314
|
+
Span::styled("ESC", Style::default().fg(NEON_GREEN)),
|
|
315
|
+
Span::styled(" = Cancel", Style::default().fg(TEXT_DIM)),
|
|
316
|
+
]),
|
|
317
|
+
Line::from(""),
|
|
318
|
+
Line::from(vec![
|
|
319
|
+
Span::styled("Run Manager:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
320
|
+
]),
|
|
321
|
+
Line::from(vec![
|
|
322
|
+
Span::raw(" "),
|
|
323
|
+
Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
|
|
324
|
+
Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
|
|
325
|
+
Span::styled("F", Style::default().fg(NEON_GREEN)),
|
|
326
|
+
Span::styled(" = Filter | ", Style::default().fg(TEXT_DIM)),
|
|
327
|
+
Span::styled("S", Style::default().fg(NEON_GREEN)),
|
|
328
|
+
Span::styled(" = Sort | ", Style::default().fg(TEXT_DIM)),
|
|
329
|
+
Span::styled("R", Style::default().fg(NEON_GREEN)),
|
|
330
|
+
Span::styled(" = Refresh", Style::default().fg(TEXT_DIM)),
|
|
331
|
+
]),
|
|
332
|
+
Line::from(""),
|
|
333
|
+
Line::from(vec![
|
|
334
|
+
Span::styled("Settings:", Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD)),
|
|
335
|
+
]),
|
|
336
|
+
Line::from(vec![
|
|
337
|
+
Span::raw(" "),
|
|
338
|
+
Span::styled("↑/↓", Style::default().fg(NEON_GREEN)),
|
|
339
|
+
Span::styled(" = Navigate | ", Style::default().fg(TEXT_DIM)),
|
|
340
|
+
Span::styled("Space", Style::default().fg(NEON_GREEN)),
|
|
341
|
+
Span::styled(" = Toggle | ", Style::default().fg(TEXT_DIM)),
|
|
342
|
+
Span::styled("Enter", Style::default().fg(NEON_GREEN)),
|
|
343
|
+
Span::styled(" = Save", Style::default().fg(TEXT_DIM)),
|
|
344
|
+
]),
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
f.render_widget(controls_block.clone(), chunks[1]);
|
|
348
|
+
f.render_widget(
|
|
349
|
+
Paragraph::new(controls_text)
|
|
350
|
+
.wrap(Wrap { trim: false }),
|
|
351
|
+
controls_block.inner(chunks[1])
|
|
352
|
+
);
|
|
353
|
+
}
|
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.4.
|
|
123
|
+
const PACKAGE_VERSION: &str = "2.4.3";
|
|
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,64 +422,6 @@ 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
|
-
|
|
426
|
-
/// Parse help log format and return styled spans
|
|
427
|
-
fn parse_help_log(log: &str) -> Vec<Span> {
|
|
428
|
-
// Remove [HELP] prefix if present
|
|
429
|
-
let content = if log.starts_with("[HELP] ") {
|
|
430
|
-
&log[7..] // Skip "[HELP] "
|
|
431
|
-
} else if log.starts_with("[HELP]") {
|
|
432
|
-
&log[6..] // Skip "[HELP]"
|
|
433
|
-
} else {
|
|
434
|
-
log
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
// Check for separator line (all dashes or equals)
|
|
438
|
-
if content.trim().chars().all(|c| c == '─' || c == '-' || c == '═') {
|
|
439
|
-
return vec![
|
|
440
|
-
Span::styled("> ", Style::default().fg(CYBER_CYAN)),
|
|
441
|
-
Span::styled("[HELP] ", Style::default().fg(CYBER_CYAN)),
|
|
442
|
-
Span::styled(content, Style::default().fg(TEXT_MUTED)),
|
|
443
|
-
];
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Check for section header (ends with colon)
|
|
447
|
-
if content.ends_with(':') && !content.starts_with(" ") {
|
|
448
|
-
return vec![
|
|
449
|
-
Span::styled("> ", Style::default().fg(CYBER_CYAN)),
|
|
450
|
-
Span::styled("[HELP] ", Style::default().fg(CYBER_CYAN)),
|
|
451
|
-
Span::styled(
|
|
452
|
-
content,
|
|
453
|
-
Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)
|
|
454
|
-
),
|
|
455
|
-
];
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Check for command line (starts with " " and contains " - ")
|
|
459
|
-
if content.starts_with(" ") && content.contains(" - ") {
|
|
460
|
-
if let Some(dash_pos) = content.find(" - ") {
|
|
461
|
-
let command_part = content[2..dash_pos].trim_end();
|
|
462
|
-
let desc_part = &content[dash_pos + 3..];
|
|
463
|
-
|
|
464
|
-
return vec![
|
|
465
|
-
Span::styled("> ", Style::default().fg(CYBER_CYAN)),
|
|
466
|
-
Span::styled("[HELP] ", Style::default().fg(CYBER_CYAN)),
|
|
467
|
-
Span::styled(" ", Style::default().fg(TEXT_PRIMARY)),
|
|
468
|
-
Span::styled(command_part, Style::default().fg(NEON_GREEN)),
|
|
469
|
-
Span::styled(" - ", Style::default().fg(TEXT_MUTED)),
|
|
470
|
-
Span::styled(desc_part, Style::default().fg(TEXT_PRIMARY)),
|
|
471
|
-
];
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Default: regular help message
|
|
476
|
-
vec![
|
|
477
|
-
Span::styled("> ", Style::default().fg(CYBER_CYAN)),
|
|
478
|
-
Span::styled("[HELP] ", Style::default().fg(CYBER_CYAN)),
|
|
479
|
-
Span::styled(content, Style::default().fg(TEXT_PRIMARY)),
|
|
480
|
-
]
|
|
481
|
-
}
|
|
482
|
-
|
|
483
425
|
fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
|
|
484
426
|
let panel_area = Rect {
|
|
485
427
|
x: area.x + 1,
|
|
@@ -525,11 +467,7 @@ fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
|
|
|
525
467
|
|
|
526
468
|
// Show real logs with proper formatting (reversed order - newest first)
|
|
527
469
|
for log in state.logs.iter().rev().skip(start_idx).take(visible_height) {
|
|
528
|
-
|
|
529
|
-
if log.starts_with("[HELP]") {
|
|
530
|
-
let spans = parse_help_log(log);
|
|
531
|
-
lines.push(Line::from(spans));
|
|
532
|
-
} else if log.starts_with("[") {
|
|
470
|
+
if log.starts_with("[") {
|
|
533
471
|
// Parse log format: [COMPONENT] message
|
|
534
472
|
if let Some(bracket_end) = log.find(']') {
|
|
535
473
|
let component = &log[1..bracket_end];
|
package/mk3-tui/src/ui/mod.rs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
pub mod agent_builder;
|
|
2
|
+
pub mod agent_list;
|
|
2
3
|
pub mod boot;
|
|
4
|
+
pub mod help;
|
|
3
5
|
pub mod layout;
|
|
4
6
|
pub mod run_manager;
|
|
5
7
|
pub mod safe_viewport;
|
|
6
8
|
pub mod settings;
|
|
7
9
|
|
|
8
|
-
// Re-export screen states
|
|
9
|
-
pub use agent_builder::AgentBuilderState;
|
|
10
|
-
pub use run_manager::RunManagerState;
|
|
11
|
-
pub use settings::SettingsState;
|
|
12
|
-
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"private": false,
|
|
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",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"bin": {
|