4runr-os 2.3.8 → 2.4.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 +86 -8
- package/mk3-tui/src/main.rs +54 -4
- package/mk3-tui/src/storage/cache.rs +213 -0
- package/mk3-tui/src/storage/mod.rs +6 -0
- package/mk3-tui/src/ui/agent_builder.rs +421 -30
- package/mk3-tui/src/ui/boot.rs +1 -1
- package/mk3-tui/src/ui/layout.rs +1 -1
- package/mk3-tui/src/ui/run_manager.rs +346 -71
- package/mk3-tui/src/ui/settings.rs +57 -42
- package/mk3-tui/src/websocket.rs +47 -2
- package/package.json +1 -1
|
@@ -5,6 +5,16 @@ use ratatui::prelude::*;
|
|
|
5
5
|
use ratatui::widgets::{Block, Borders, Paragraph, Wrap, List, ListItem};
|
|
6
6
|
use crate::app::AppState;
|
|
7
7
|
|
|
8
|
+
// === 4RUNR BRAND COLORS (matching layout.rs) ===
|
|
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
|
+
|
|
8
18
|
/// Run Manager state
|
|
9
19
|
#[derive(Debug, Clone)]
|
|
10
20
|
pub struct RunManagerState {
|
|
@@ -25,6 +35,9 @@ pub struct RunManagerState {
|
|
|
25
35
|
|
|
26
36
|
/// Last refresh time
|
|
27
37
|
pub last_refresh: Option<std::time::Instant>,
|
|
38
|
+
|
|
39
|
+
/// Detailed view state (None = list view, Some(index) = detail view)
|
|
40
|
+
pub detail_view: Option<usize>,
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
#[derive(Debug, Clone)]
|
|
@@ -36,6 +49,14 @@ pub struct RunInfo {
|
|
|
36
49
|
pub started_at: String,
|
|
37
50
|
pub duration: Option<String>,
|
|
38
51
|
pub progress: Option<f32>, // 0.0-1.0
|
|
52
|
+
|
|
53
|
+
// Extended details
|
|
54
|
+
pub input_tokens: Option<u32>,
|
|
55
|
+
pub output_tokens: Option<u32>,
|
|
56
|
+
pub total_cost: Option<f64>,
|
|
57
|
+
pub model: Option<String>,
|
|
58
|
+
pub error_message: Option<String>,
|
|
59
|
+
pub logs: Vec<String>,
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
#[derive(Debug, Clone, PartialEq)]
|
|
@@ -60,11 +81,11 @@ impl RunStatus {
|
|
|
60
81
|
|
|
61
82
|
pub fn color(&self) -> Color {
|
|
62
83
|
match self {
|
|
63
|
-
RunStatus::Pending =>
|
|
64
|
-
RunStatus::Running =>
|
|
65
|
-
RunStatus::Completed =>
|
|
66
|
-
RunStatus::Failed => Color::
|
|
67
|
-
RunStatus::Cancelled =>
|
|
84
|
+
RunStatus::Pending => AMBER_WARN,
|
|
85
|
+
RunStatus::Running => CYBER_CYAN,
|
|
86
|
+
RunStatus::Completed => NEON_GREEN,
|
|
87
|
+
RunStatus::Failed => Color::Rgb(255, 69, 69), // Bright red
|
|
88
|
+
RunStatus::Cancelled => TEXT_DIM,
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
}
|
|
@@ -129,6 +150,18 @@ impl Default for RunManagerState {
|
|
|
129
150
|
started_at: "2m ago".to_string(),
|
|
130
151
|
duration: Some("2m 15s".to_string()),
|
|
131
152
|
progress: Some(0.65),
|
|
153
|
+
input_tokens: Some(1250),
|
|
154
|
+
output_tokens: Some(850),
|
|
155
|
+
total_cost: Some(0.0315),
|
|
156
|
+
model: Some("gpt-4-turbo".to_string()),
|
|
157
|
+
error_message: None,
|
|
158
|
+
logs: vec![
|
|
159
|
+
"[00:00] Run started".to_string(),
|
|
160
|
+
"[00:15] Loading data from source...".to_string(),
|
|
161
|
+
"[00:45] Processing 1,234 records...".to_string(),
|
|
162
|
+
"[01:30] Analyzing patterns...".to_string(),
|
|
163
|
+
"[02:15] Generating insights...".to_string(),
|
|
164
|
+
],
|
|
132
165
|
},
|
|
133
166
|
RunInfo {
|
|
134
167
|
id: "run-002".to_string(),
|
|
@@ -138,6 +171,18 @@ impl Default for RunManagerState {
|
|
|
138
171
|
started_at: "15m ago".to_string(),
|
|
139
172
|
duration: Some("3m 42s".to_string()),
|
|
140
173
|
progress: Some(1.0),
|
|
174
|
+
input_tokens: Some(3200),
|
|
175
|
+
output_tokens: Some(1800),
|
|
176
|
+
total_cost: Some(0.075),
|
|
177
|
+
model: Some("gpt-4".to_string()),
|
|
178
|
+
error_message: None,
|
|
179
|
+
logs: vec![
|
|
180
|
+
"[00:00] Run started".to_string(),
|
|
181
|
+
"[00:30] Analyzing code structure...".to_string(),
|
|
182
|
+
"[01:45] Checking for issues...".to_string(),
|
|
183
|
+
"[02:30] Generating recommendations...".to_string(),
|
|
184
|
+
"[03:42] Review complete".to_string(),
|
|
185
|
+
],
|
|
141
186
|
},
|
|
142
187
|
RunInfo {
|
|
143
188
|
id: "run-003".to_string(),
|
|
@@ -147,6 +192,16 @@ impl Default for RunManagerState {
|
|
|
147
192
|
started_at: "1h ago".to_string(),
|
|
148
193
|
duration: Some("1m 05s".to_string()),
|
|
149
194
|
progress: Some(0.25),
|
|
195
|
+
input_tokens: Some(450),
|
|
196
|
+
output_tokens: Some(120),
|
|
197
|
+
total_cost: Some(0.0086),
|
|
198
|
+
model: Some("gpt-3.5-turbo".to_string()),
|
|
199
|
+
error_message: Some("API rate limit exceeded".to_string()),
|
|
200
|
+
logs: vec![
|
|
201
|
+
"[00:00] Run started".to_string(),
|
|
202
|
+
"[00:30] Loading template...".to_string(),
|
|
203
|
+
"[01:05] ERROR: API rate limit exceeded".to_string(),
|
|
204
|
+
],
|
|
150
205
|
},
|
|
151
206
|
],
|
|
152
207
|
selected_index: 0,
|
|
@@ -154,6 +209,7 @@ impl Default for RunManagerState {
|
|
|
154
209
|
sort: RunSort::DateDesc,
|
|
155
210
|
loading: false,
|
|
156
211
|
last_refresh: None,
|
|
212
|
+
detail_view: None,
|
|
157
213
|
}
|
|
158
214
|
}
|
|
159
215
|
}
|
|
@@ -190,6 +246,27 @@ impl RunManagerState {
|
|
|
190
246
|
filtered.get(self.selected_index).copied()
|
|
191
247
|
}
|
|
192
248
|
|
|
249
|
+
/// Toggle detail view for selected run
|
|
250
|
+
pub fn toggle_detail_view(&mut self) {
|
|
251
|
+
if self.detail_view.is_some() {
|
|
252
|
+
// Close detail view
|
|
253
|
+
self.detail_view = None;
|
|
254
|
+
} else {
|
|
255
|
+
// Open detail view for selected run
|
|
256
|
+
self.detail_view = Some(self.selected_index);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// Close detail view
|
|
261
|
+
pub fn close_detail_view(&mut self) {
|
|
262
|
+
self.detail_view = None;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Check if in detail view
|
|
266
|
+
pub fn is_detail_view(&self) -> bool {
|
|
267
|
+
self.detail_view.is_some()
|
|
268
|
+
}
|
|
269
|
+
|
|
193
270
|
/// Move selection up
|
|
194
271
|
pub fn select_previous(&mut self) {
|
|
195
272
|
if self.selected_index > 0 {
|
|
@@ -234,23 +311,28 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
234
311
|
// Get run manager state from AppState
|
|
235
312
|
let run_state = &state.run_manager;
|
|
236
313
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
314
|
+
// Check if in detail view
|
|
315
|
+
if run_state.is_detail_view() {
|
|
316
|
+
render_detail_view(f, area, run_state);
|
|
317
|
+
} else {
|
|
318
|
+
// Split screen into sections
|
|
319
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
320
|
+
|
|
321
|
+
let chunks = Layout::default()
|
|
322
|
+
.direction(Direction::Vertical)
|
|
323
|
+
.constraints([
|
|
324
|
+
Constraint::Length(3), // Header
|
|
325
|
+
Constraint::Min(10), // Run list
|
|
326
|
+
Constraint::Length(8), // Details panel
|
|
327
|
+
Constraint::Length(3), // Actions
|
|
328
|
+
])
|
|
329
|
+
.split(area);
|
|
330
|
+
|
|
331
|
+
render_header(f, chunks[0], run_state);
|
|
332
|
+
render_run_list(f, chunks[1], run_state);
|
|
333
|
+
render_details_panel(f, chunks[2], run_state);
|
|
334
|
+
render_actions(f, chunks[3]);
|
|
335
|
+
}
|
|
254
336
|
}
|
|
255
337
|
|
|
256
338
|
fn render_header(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
@@ -259,22 +341,24 @@ fn render_header(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
259
341
|
|
|
260
342
|
let block = Block::default()
|
|
261
343
|
.title(format!(
|
|
262
|
-
" Run Manager - {} ({}/{} runs) ",
|
|
344
|
+
" ⚡ Run Manager - {} ({}/{} runs) ",
|
|
263
345
|
state.filter.as_str(),
|
|
264
346
|
filtered_count,
|
|
265
347
|
total_count
|
|
266
348
|
))
|
|
267
349
|
.borders(Borders::ALL)
|
|
268
|
-
.border_style(Style::default().fg(
|
|
350
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
351
|
+
.style(Style::default().bg(BG_PANEL));
|
|
269
352
|
|
|
270
353
|
f.render_widget(block, area);
|
|
271
354
|
}
|
|
272
355
|
|
|
273
356
|
fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
274
357
|
let block = Block::default()
|
|
275
|
-
.title(format!(" Runs (Sort: {}) ", state.sort.as_str()))
|
|
358
|
+
.title(format!(" 📋 Runs (Sort: {}) ", state.sort.as_str()))
|
|
276
359
|
.borders(Borders::ALL)
|
|
277
|
-
.border_style(Style::default().fg(
|
|
360
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
361
|
+
.style(Style::default().bg(BG_PANEL));
|
|
278
362
|
|
|
279
363
|
let inner = block.inner(area);
|
|
280
364
|
f.render_widget(block, area);
|
|
@@ -302,23 +386,39 @@ fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
302
386
|
|
|
303
387
|
let duration_str = run.duration.as_deref().unwrap_or("--");
|
|
304
388
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
389
|
+
// Build line with colored spans
|
|
390
|
+
let line = Line::from(vec![
|
|
391
|
+
Span::styled(
|
|
392
|
+
if is_selected { "▶ " } else { " " },
|
|
393
|
+
Style::default().fg(BRAND_PURPLE).bold()
|
|
394
|
+
),
|
|
395
|
+
Span::styled(
|
|
396
|
+
format!("{:<10}", run.status.as_str()),
|
|
397
|
+
Style::default().fg(run.status.color()).bold()
|
|
398
|
+
),
|
|
399
|
+
Span::styled(" │ ", Style::default().fg(TEXT_DIM)),
|
|
400
|
+
Span::styled(
|
|
401
|
+
format!("{:<20}", run.name),
|
|
402
|
+
Style::default().fg(if is_selected { CYBER_CYAN } else { TEXT_PRIMARY }).bold()
|
|
403
|
+
),
|
|
404
|
+
Span::styled(" │ ", Style::default().fg(TEXT_DIM)),
|
|
405
|
+
Span::styled(
|
|
406
|
+
format!("{:<15}", run.agent_name),
|
|
407
|
+
Style::default().fg(TEXT_DIM)
|
|
408
|
+
),
|
|
409
|
+
Span::styled(" │ ", Style::default().fg(TEXT_DIM)),
|
|
410
|
+
Span::styled(
|
|
411
|
+
format!("{:>8}", duration_str),
|
|
412
|
+
Style::default().fg(TEXT_MUTED)
|
|
413
|
+
),
|
|
414
|
+
Span::raw(" "),
|
|
415
|
+
Span::styled(
|
|
416
|
+
progress_bar,
|
|
417
|
+
Style::default().fg(if run.progress.unwrap_or(0.0) > 0.5 { NEON_GREEN } else { AMBER_WARN })
|
|
418
|
+
),
|
|
419
|
+
]);
|
|
320
420
|
|
|
321
|
-
ListItem::new(line)
|
|
421
|
+
ListItem::new(line)
|
|
322
422
|
}).collect();
|
|
323
423
|
|
|
324
424
|
let list = List::new(items);
|
|
@@ -327,9 +427,10 @@ fn render_run_list(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
327
427
|
|
|
328
428
|
fn render_details_panel(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
329
429
|
let block = Block::default()
|
|
330
|
-
.title(" Run Details ")
|
|
430
|
+
.title(" 🔍 Run Details ")
|
|
331
431
|
.borders(Borders::ALL)
|
|
332
|
-
.border_style(Style::default().fg(
|
|
432
|
+
.border_style(Style::default().fg(NEON_GREEN))
|
|
433
|
+
.style(Style::default().bg(BG_PANEL));
|
|
333
434
|
|
|
334
435
|
let inner = block.inner(area);
|
|
335
436
|
f.render_widget(block, area);
|
|
@@ -337,24 +438,24 @@ fn render_details_panel(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
337
438
|
if let Some(run) = state.selected_run() {
|
|
338
439
|
let text = vec![
|
|
339
440
|
Line::from(vec![
|
|
340
|
-
Span::
|
|
341
|
-
Span::styled(&run.id, Style::default().fg(
|
|
441
|
+
Span::styled("ID: ", Style::default().fg(TEXT_DIM)),
|
|
442
|
+
Span::styled(&run.id, Style::default().fg(AMBER_WARN)),
|
|
342
443
|
]),
|
|
343
444
|
Line::from(vec![
|
|
344
|
-
Span::
|
|
345
|
-
Span::styled(&run.name, Style::default().fg(
|
|
445
|
+
Span::styled("Name: ", Style::default().fg(TEXT_DIM)),
|
|
446
|
+
Span::styled(&run.name, Style::default().fg(TEXT_PRIMARY).bold()),
|
|
346
447
|
]),
|
|
347
448
|
Line::from(vec![
|
|
348
|
-
Span::
|
|
349
|
-
Span::styled(&run.agent_name, Style::default().fg(
|
|
449
|
+
Span::styled("Agent: ", Style::default().fg(TEXT_DIM)),
|
|
450
|
+
Span::styled(&run.agent_name, Style::default().fg(CYBER_CYAN)),
|
|
350
451
|
]),
|
|
351
452
|
Line::from(vec![
|
|
352
|
-
Span::
|
|
353
|
-
Span::styled(run.status.as_str(), Style::default().fg(run.status.color())),
|
|
453
|
+
Span::styled("Status: ", Style::default().fg(TEXT_DIM)),
|
|
454
|
+
Span::styled(run.status.as_str(), Style::default().fg(run.status.color()).bold()),
|
|
354
455
|
]),
|
|
355
456
|
Line::from(vec![
|
|
356
|
-
Span::
|
|
357
|
-
Span::styled(&run.started_at, Style::default().fg(
|
|
457
|
+
Span::styled("Started: ", Style::default().fg(TEXT_DIM)),
|
|
458
|
+
Span::styled(&run.started_at, Style::default().fg(TEXT_MUTED)),
|
|
358
459
|
]),
|
|
359
460
|
];
|
|
360
461
|
|
|
@@ -362,39 +463,213 @@ fn render_details_panel(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
|
362
463
|
f.render_widget(paragraph, inner);
|
|
363
464
|
} else {
|
|
364
465
|
let text = Paragraph::new("No run selected")
|
|
365
|
-
.style(Style::default().fg(
|
|
466
|
+
.style(Style::default().fg(TEXT_DIM))
|
|
366
467
|
.alignment(Alignment::Center);
|
|
367
468
|
f.render_widget(text, inner);
|
|
368
469
|
}
|
|
369
470
|
}
|
|
370
471
|
|
|
472
|
+
fn render_detail_view(f: &mut Frame, area: Rect, state: &RunManagerState) {
|
|
473
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
474
|
+
|
|
475
|
+
// Split into header, content, and footer
|
|
476
|
+
let chunks = Layout::default()
|
|
477
|
+
.direction(Direction::Vertical)
|
|
478
|
+
.constraints([
|
|
479
|
+
Constraint::Length(3), // Header
|
|
480
|
+
Constraint::Min(10), // Content
|
|
481
|
+
Constraint::Length(3), // Footer
|
|
482
|
+
])
|
|
483
|
+
.split(area);
|
|
484
|
+
|
|
485
|
+
// Get the selected run
|
|
486
|
+
let run = match state.selected_run() {
|
|
487
|
+
Some(r) => r,
|
|
488
|
+
None => {
|
|
489
|
+
let block = Block::default()
|
|
490
|
+
.title(" Run Details ")
|
|
491
|
+
.borders(Borders::ALL)
|
|
492
|
+
.border_style(Style::default().fg(TEXT_DIM));
|
|
493
|
+
f.render_widget(block, area);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// Header
|
|
499
|
+
let header_block = Block::default()
|
|
500
|
+
.title(format!(" 📊 Run Details - {} ", run.name))
|
|
501
|
+
.borders(Borders::ALL)
|
|
502
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
503
|
+
.style(Style::default().bg(BG_PANEL));
|
|
504
|
+
f.render_widget(header_block, chunks[0]);
|
|
505
|
+
|
|
506
|
+
// Content - split into left (info) and right (logs)
|
|
507
|
+
let content_chunks = Layout::default()
|
|
508
|
+
.direction(Direction::Horizontal)
|
|
509
|
+
.constraints([
|
|
510
|
+
Constraint::Percentage(40), // Info
|
|
511
|
+
Constraint::Percentage(60), // Logs
|
|
512
|
+
])
|
|
513
|
+
.split(chunks[1]);
|
|
514
|
+
|
|
515
|
+
// Left: Run Info
|
|
516
|
+
let info_block = Block::default()
|
|
517
|
+
.title(" ℹ️ Information ")
|
|
518
|
+
.borders(Borders::ALL)
|
|
519
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
520
|
+
.style(Style::default().bg(BG_PANEL));
|
|
521
|
+
|
|
522
|
+
let info_inner = info_block.inner(content_chunks[0]);
|
|
523
|
+
f.render_widget(info_block, content_chunks[0]);
|
|
524
|
+
|
|
525
|
+
let mut info_lines = vec![
|
|
526
|
+
Line::from(""),
|
|
527
|
+
Line::from(vec![
|
|
528
|
+
Span::styled("ID: ", Style::default().fg(TEXT_DIM)),
|
|
529
|
+
Span::styled(&run.id, Style::default().fg(TEXT_PRIMARY)),
|
|
530
|
+
]),
|
|
531
|
+
Line::from(vec![
|
|
532
|
+
Span::styled("Agent: ", Style::default().fg(TEXT_DIM)),
|
|
533
|
+
Span::styled(&run.agent_name, Style::default().fg(CYBER_CYAN).bold()),
|
|
534
|
+
]),
|
|
535
|
+
Line::from(vec![
|
|
536
|
+
Span::styled("Status: ", Style::default().fg(TEXT_DIM)),
|
|
537
|
+
Span::styled(run.status.as_str(), Style::default().fg(run.status.color()).bold()),
|
|
538
|
+
]),
|
|
539
|
+
Line::from(""),
|
|
540
|
+
Line::from(vec![
|
|
541
|
+
Span::styled("Started: ", Style::default().fg(TEXT_DIM)),
|
|
542
|
+
Span::styled(&run.started_at, Style::default().fg(TEXT_PRIMARY)),
|
|
543
|
+
]),
|
|
544
|
+
Line::from(vec![
|
|
545
|
+
Span::styled("Duration: ", Style::default().fg(TEXT_DIM)),
|
|
546
|
+
Span::styled(run.duration.as_deref().unwrap_or("--"), Style::default().fg(TEXT_PRIMARY)),
|
|
547
|
+
]),
|
|
548
|
+
Line::from(""),
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
// Add model info if available
|
|
552
|
+
if let Some(model) = &run.model {
|
|
553
|
+
info_lines.push(Line::from(vec![
|
|
554
|
+
Span::styled("Model: ", Style::default().fg(TEXT_DIM)),
|
|
555
|
+
Span::styled(model, Style::default().fg(TEXT_PRIMARY)),
|
|
556
|
+
]));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Add token usage if available
|
|
560
|
+
if let (Some(input), Some(output)) = (run.input_tokens, run.output_tokens) {
|
|
561
|
+
info_lines.push(Line::from(""));
|
|
562
|
+
info_lines.push(Line::from("Token Usage:").style(Style::default().fg(NEON_GREEN).bold()));
|
|
563
|
+
info_lines.push(Line::from(vec![
|
|
564
|
+
Span::styled(" Input: ", Style::default().fg(TEXT_DIM)),
|
|
565
|
+
Span::styled(format!("{}", input), Style::default().fg(TEXT_PRIMARY)),
|
|
566
|
+
]));
|
|
567
|
+
info_lines.push(Line::from(vec![
|
|
568
|
+
Span::styled(" Output: ", Style::default().fg(TEXT_DIM)),
|
|
569
|
+
Span::styled(format!("{}", output), Style::default().fg(TEXT_PRIMARY)),
|
|
570
|
+
]));
|
|
571
|
+
info_lines.push(Line::from(vec![
|
|
572
|
+
Span::styled(" Total: ", Style::default().fg(TEXT_DIM)),
|
|
573
|
+
Span::styled(format!("{}", input + output), Style::default().fg(NEON_GREEN).bold()),
|
|
574
|
+
]));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Add cost if available
|
|
578
|
+
if let Some(cost) = run.total_cost {
|
|
579
|
+
info_lines.push(Line::from(""));
|
|
580
|
+
info_lines.push(Line::from(vec![
|
|
581
|
+
Span::styled("Cost: ", Style::default().fg(TEXT_DIM)),
|
|
582
|
+
Span::styled(format!("${:.4}", cost), Style::default().fg(AMBER_WARN).bold()),
|
|
583
|
+
]));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Add error if present
|
|
587
|
+
if let Some(error) = &run.error_message {
|
|
588
|
+
info_lines.push(Line::from(""));
|
|
589
|
+
info_lines.push(Line::from("Error:").style(Style::default().fg(Color::Rgb(255, 69, 69)).bold()));
|
|
590
|
+
info_lines.push(Line::from(format!(" {}", error)).style(Style::default().fg(Color::Rgb(255, 69, 69))));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let info_paragraph = Paragraph::new(info_lines)
|
|
594
|
+
.wrap(Wrap { trim: false });
|
|
595
|
+
f.render_widget(info_paragraph, info_inner);
|
|
596
|
+
|
|
597
|
+
// Right: Logs
|
|
598
|
+
let logs_block = Block::default()
|
|
599
|
+
.title(" 📜 Execution Logs ")
|
|
600
|
+
.borders(Borders::ALL)
|
|
601
|
+
.border_style(Style::default().fg(NEON_GREEN))
|
|
602
|
+
.style(Style::default().bg(BG_PANEL));
|
|
603
|
+
|
|
604
|
+
let logs_inner = logs_block.inner(content_chunks[1]);
|
|
605
|
+
f.render_widget(logs_block, content_chunks[1]);
|
|
606
|
+
|
|
607
|
+
let log_lines: Vec<Line> = if run.logs.is_empty() {
|
|
608
|
+
vec![
|
|
609
|
+
Line::from(""),
|
|
610
|
+
Line::from("No logs available").style(Style::default().fg(TEXT_DIM)),
|
|
611
|
+
]
|
|
612
|
+
} else {
|
|
613
|
+
run.logs.iter().map(|log| {
|
|
614
|
+
Line::from(log.as_str()).style(Style::default().fg(TEXT_PRIMARY))
|
|
615
|
+
}).collect()
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
let logs_paragraph = Paragraph::new(log_lines)
|
|
619
|
+
.wrap(Wrap { trim: false });
|
|
620
|
+
f.render_widget(logs_paragraph, logs_inner);
|
|
621
|
+
|
|
622
|
+
// Footer
|
|
623
|
+
let footer_block = Block::default()
|
|
624
|
+
.title(" ⌨️ Actions ")
|
|
625
|
+
.borders(Borders::ALL)
|
|
626
|
+
.border_style(Style::default().fg(TEXT_DIM))
|
|
627
|
+
.style(Style::default().bg(BG_PANEL));
|
|
628
|
+
|
|
629
|
+
let footer_inner = footer_block.inner(chunks[2]);
|
|
630
|
+
f.render_widget(footer_block, chunks[2]);
|
|
631
|
+
|
|
632
|
+
let footer_text = Line::from(vec![
|
|
633
|
+
Span::styled("ESC/Enter", Style::default().fg(BRAND_PURPLE).bold()),
|
|
634
|
+
Span::styled(" Back to List │ ", Style::default().fg(TEXT_DIM)),
|
|
635
|
+
Span::styled("R", Style::default().fg(NEON_GREEN).bold()),
|
|
636
|
+
Span::styled(" Refresh", Style::default().fg(TEXT_DIM)),
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
let footer_paragraph = Paragraph::new(footer_text)
|
|
640
|
+
.alignment(Alignment::Center);
|
|
641
|
+
f.render_widget(footer_paragraph, footer_inner);
|
|
642
|
+
}
|
|
643
|
+
|
|
371
644
|
fn render_actions(f: &mut Frame, area: Rect) {
|
|
372
645
|
let block = Block::default()
|
|
373
|
-
.title(" Actions ")
|
|
646
|
+
.title(" ⌨️ Actions ")
|
|
374
647
|
.borders(Borders::ALL)
|
|
375
|
-
.border_style(Style::default().fg(
|
|
648
|
+
.border_style(Style::default().fg(TEXT_DIM))
|
|
649
|
+
.style(Style::default().bg(BG_PANEL));
|
|
376
650
|
|
|
377
651
|
let inner = block.inner(area);
|
|
378
652
|
f.render_widget(block, area);
|
|
379
653
|
|
|
380
654
|
let text = Line::from(vec![
|
|
381
|
-
Span::
|
|
382
|
-
Span::styled("
|
|
383
|
-
Span::
|
|
384
|
-
Span::styled("
|
|
385
|
-
Span::
|
|
386
|
-
Span::styled("
|
|
387
|
-
Span::
|
|
388
|
-
Span::styled("
|
|
389
|
-
Span::
|
|
390
|
-
Span::styled("
|
|
391
|
-
Span::
|
|
392
|
-
Span::styled("
|
|
393
|
-
Span::
|
|
655
|
+
Span::styled("↑/↓", Style::default().fg(CYBER_CYAN).bold()),
|
|
656
|
+
Span::styled(" Select │ ", Style::default().fg(TEXT_DIM)),
|
|
657
|
+
Span::styled("F", Style::default().fg(AMBER_WARN).bold()),
|
|
658
|
+
Span::styled(" Filter │ ", Style::default().fg(TEXT_DIM)),
|
|
659
|
+
Span::styled("S", Style::default().fg(AMBER_WARN).bold()),
|
|
660
|
+
Span::styled(" Sort │ ", Style::default().fg(TEXT_DIM)),
|
|
661
|
+
Span::styled("R", Style::default().fg(NEON_GREEN).bold()),
|
|
662
|
+
Span::styled(" Refresh │ ", Style::default().fg(TEXT_DIM)),
|
|
663
|
+
Span::styled("C", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
|
|
664
|
+
Span::styled(" Cancel │ ", Style::default().fg(TEXT_DIM)),
|
|
665
|
+
Span::styled("D", Style::default().fg(Color::Rgb(255, 69, 69)).bold()),
|
|
666
|
+
Span::styled(" Delete │ ", Style::default().fg(TEXT_DIM)),
|
|
667
|
+
Span::styled("ESC", Style::default().fg(BRAND_PURPLE).bold()),
|
|
668
|
+
Span::styled(" Close", Style::default().fg(TEXT_DIM)),
|
|
394
669
|
]);
|
|
395
670
|
|
|
396
671
|
let paragraph = Paragraph::new(text)
|
|
397
|
-
.style(Style::default().fg(
|
|
672
|
+
.style(Style::default().fg(TEXT_PRIMARY))
|
|
398
673
|
.alignment(Alignment::Center);
|
|
399
674
|
|
|
400
675
|
f.render_widget(paragraph, inner);
|