4runr-os 2.10.39 → 2.10.41
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/apps/gateway/dist/apps/gateway/src/index.js +14 -4
- package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +18 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js +117 -0
- package/apps/gateway/dist/apps/gateway/src/metrics/monitoring-detail.js.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts +2 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js +54 -0
- package/apps/gateway/dist/apps/gateway/src/middleware/log-capture.js.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts +15 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.d.ts.map +1 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js +164 -0
- package/apps/gateway/dist/apps/gateway/src/routes/monitoring.js.map +1 -0
- package/apps/gateway/package-lock.json +204 -353
- package/apps/gateway/src/index.ts +27 -8
- package/apps/gateway/src/metrics/monitoring-detail.ts +162 -0
- package/apps/gateway/src/middleware/log-capture.ts +70 -0
- package/apps/gateway/src/routes/monitoring.ts +298 -0
- package/dist/gateway-client.d.ts +2 -0
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +22 -0
- package/dist/gateway-client.js.map +1 -1
- package/dist/tui-handlers.js +498 -0
- package/dist/tui-handlers.js.map +1 -1
- package/mk3-tui/src/app/render_scheduler.rs +111 -112
- package/mk3-tui/src/app.rs +1078 -295
- package/mk3-tui/src/debug_log.rs +131 -124
- package/mk3-tui/src/io/mod.rs +63 -66
- package/mk3-tui/src/io/protocol.rs +14 -15
- package/mk3-tui/src/io/stdio.rs +31 -32
- package/mk3-tui/src/io/ws.rs +25 -32
- package/mk3-tui/src/main.rs +774 -212
- package/mk3-tui/src/monitoring/mod.rs +428 -0
- package/mk3-tui/src/screens/mod.rs +53 -39
- package/mk3-tui/src/storage/cache.rs +221 -224
- package/mk3-tui/src/storage/mod.rs +5 -6
- package/mk3-tui/src/ui/agent_builder.rs +1148 -922
- package/mk3-tui/src/ui/agent_list.rs +344 -295
- package/mk3-tui/src/ui/boot.rs +145 -148
- package/mk3-tui/src/ui/connection_portal.rs +121 -98
- package/mk3-tui/src/ui/help.rs +340 -284
- package/mk3-tui/src/ui/layout.rs +966 -803
- package/mk3-tui/src/ui/mod.rs +1 -1
- package/mk3-tui/src/ui/portal_monitoring.rs +1027 -147
- package/mk3-tui/src/ui/run_manager.rs +784 -764
- package/mk3-tui/src/ui/safe_viewport.rs +236 -235
- package/mk3-tui/src/ui/settings.rs +414 -362
- package/mk3-tui/src/ui/setup_portal.rs +158 -101
- package/mk3-tui/src/websocket.rs +315 -308
- package/package.json +2 -2
package/mk3-tui/src/ui/layout.rs
CHANGED
|
@@ -1,803 +1,966 @@
|
|
|
1
|
-
use crate::app::AppState;
|
|
2
|
-
use crate::ui::safe_viewport::SafeViewport;
|
|
3
|
-
use ratatui::prelude::*;
|
|
4
|
-
use ratatui::
|
|
5
|
-
use ratatui::
|
|
6
|
-
use ratatui::
|
|
7
|
-
|
|
8
|
-
// === 4RUNR BRAND COLORS ===
|
|
9
|
-
const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
|
|
10
|
-
const BRAND_VIOLET: Color = Color::Rgb(148, 103, 189);
|
|
11
|
-
const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
|
|
12
|
-
const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
|
|
13
|
-
const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
|
|
14
|
-
const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
|
|
15
|
-
const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
|
|
16
|
-
const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
|
|
17
|
-
#[allow(dead_code)]
|
|
18
|
-
const BG_PANEL: Color = Color::Rgb(18, 18, 25);
|
|
19
|
-
|
|
20
|
-
// === ANIMATION CONSTANTS ===
|
|
21
|
-
const SPINNERS: [&str; 8] = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
22
|
-
// NOTE: PULSE removed - we use static dots (*, +, -) for status indicators
|
|
23
|
-
// to prevent flashing when typing. Instructions: "DON'T use animated pulse for status dots!"
|
|
24
|
-
|
|
25
|
-
const MIN_COLS: u16 = 80;
|
|
26
|
-
const MIN_ROWS: u16 = 24;
|
|
27
|
-
|
|
28
|
-
/// Render a horizontal separator line that stops before right edge
|
|
29
|
-
fn render_separator(f: &mut Frame, area: Rect) {
|
|
30
|
-
// Stop separator at 85% of terminal width (safe zone)
|
|
31
|
-
let safe_width = (area.width * 85 / 100).min(area.width.saturating_sub(10));
|
|
32
|
-
|
|
33
|
-
if safe_width > 0 {
|
|
34
|
-
let separator_line = "─".repeat(safe_width as usize);
|
|
35
|
-
f.render_widget(
|
|
36
|
-
Paragraph::new(separator_line).style(Style::default().fg(TEXT_DIM)),
|
|
37
|
-
Rect {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
Span::styled(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
let
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
let
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
Span::styled(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
Span::styled(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
let
|
|
458
|
-
let
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
//
|
|
721
|
-
let
|
|
722
|
-
let
|
|
723
|
-
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
.
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
1
|
+
use crate::app::AppState;
|
|
2
|
+
use crate::ui::safe_viewport::SafeViewport;
|
|
3
|
+
use ratatui::prelude::*;
|
|
4
|
+
use ratatui::style::Modifier;
|
|
5
|
+
use ratatui::text::{Line, Span};
|
|
6
|
+
use ratatui::widgets::*;
|
|
7
|
+
|
|
8
|
+
// === 4RUNR BRAND COLORS ===
|
|
9
|
+
const BRAND_PURPLE: Color = Color::Rgb(138, 43, 226);
|
|
10
|
+
const BRAND_VIOLET: Color = Color::Rgb(148, 103, 189);
|
|
11
|
+
const CYBER_CYAN: Color = Color::Rgb(0, 255, 255);
|
|
12
|
+
const NEON_GREEN: Color = Color::Rgb(57, 255, 20);
|
|
13
|
+
const AMBER_WARN: Color = Color::Rgb(255, 191, 0);
|
|
14
|
+
const TEXT_PRIMARY: Color = Color::Rgb(230, 230, 240);
|
|
15
|
+
const TEXT_DIM: Color = Color::Rgb(100, 100, 120);
|
|
16
|
+
const TEXT_MUTED: Color = Color::Rgb(60, 60, 80);
|
|
17
|
+
#[allow(dead_code)]
|
|
18
|
+
const BG_PANEL: Color = Color::Rgb(18, 18, 25);
|
|
19
|
+
|
|
20
|
+
// === ANIMATION CONSTANTS ===
|
|
21
|
+
const SPINNERS: [&str; 8] = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
22
|
+
// NOTE: PULSE removed - we use static dots (*, +, -) for status indicators
|
|
23
|
+
// to prevent flashing when typing. Instructions: "DON'T use animated pulse for status dots!"
|
|
24
|
+
|
|
25
|
+
const MIN_COLS: u16 = 80;
|
|
26
|
+
const MIN_ROWS: u16 = 24;
|
|
27
|
+
|
|
28
|
+
/// Render a horizontal separator line that stops before right edge
|
|
29
|
+
fn render_separator(f: &mut Frame, area: Rect) {
|
|
30
|
+
// Stop separator at 85% of terminal width (safe zone)
|
|
31
|
+
let safe_width = (area.width * 85 / 100).min(area.width.saturating_sub(10));
|
|
32
|
+
|
|
33
|
+
if safe_width > 0 {
|
|
34
|
+
let separator_line = "─".repeat(safe_width as usize);
|
|
35
|
+
f.render_widget(
|
|
36
|
+
Paragraph::new(separator_line).style(Style::default().fg(TEXT_DIM)),
|
|
37
|
+
Rect {
|
|
38
|
+
x: area.x,
|
|
39
|
+
y: area.y,
|
|
40
|
+
width: safe_width,
|
|
41
|
+
height: 1,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn render(f: &mut Frame, state: &AppState) {
|
|
48
|
+
// CRITICAL: Hide cursor by default at start of each render
|
|
49
|
+
// Only show it when explicitly set in render_command_box
|
|
50
|
+
// This prevents cursor from appearing in wrong places (like operations log)
|
|
51
|
+
let full_area = f.size();
|
|
52
|
+
f.render_widget(Clear, full_area);
|
|
53
|
+
|
|
54
|
+
let viewport = SafeViewport::new(full_area);
|
|
55
|
+
|
|
56
|
+
if viewport.is_too_small(MIN_COLS, MIN_ROWS) {
|
|
57
|
+
render_too_small(f, &viewport);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let safe = viewport.safe_rect;
|
|
62
|
+
|
|
63
|
+
// Dark background
|
|
64
|
+
f.render_widget(
|
|
65
|
+
Block::default().style(Style::default().bg(Color::Rgb(10, 10, 15))),
|
|
66
|
+
full_area,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Main layout: Header (3) + Content (flex) + Command (4)
|
|
70
|
+
let main_chunks = Layout::default()
|
|
71
|
+
.direction(Direction::Vertical)
|
|
72
|
+
.constraints([
|
|
73
|
+
Constraint::Length(3), // Header
|
|
74
|
+
Constraint::Min(15), // Content
|
|
75
|
+
Constraint::Length(4), // Command bar
|
|
76
|
+
])
|
|
77
|
+
.split(safe);
|
|
78
|
+
|
|
79
|
+
render_header(f, main_chunks[0], state);
|
|
80
|
+
|
|
81
|
+
// === ADD SEPARATOR BELOW HEADER ===
|
|
82
|
+
if main_chunks[1].y > main_chunks[0].y + main_chunks[0].height {
|
|
83
|
+
render_separator(
|
|
84
|
+
f,
|
|
85
|
+
Rect {
|
|
86
|
+
x: safe.x,
|
|
87
|
+
y: main_chunks[0].y + main_chunks[0].height,
|
|
88
|
+
width: safe.width,
|
|
89
|
+
height: 1,
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
render_content(f, main_chunks[1], state);
|
|
95
|
+
|
|
96
|
+
// === ADD SEPARATOR ABOVE COMMAND BOX ===
|
|
97
|
+
if main_chunks[2].y > main_chunks[1].y + main_chunks[1].height {
|
|
98
|
+
render_separator(
|
|
99
|
+
f,
|
|
100
|
+
Rect {
|
|
101
|
+
x: safe.x,
|
|
102
|
+
y: main_chunks[1].y + main_chunks[1].height,
|
|
103
|
+
width: safe.width,
|
|
104
|
+
height: 1,
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
render_command_box(f, main_chunks[2], state);
|
|
110
|
+
|
|
111
|
+
// Perf overlay (if enabled)
|
|
112
|
+
if state.perf_overlay {
|
|
113
|
+
render_perf_overlay(f, full_area, state);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn render_header(f: &mut Frame, area: Rect, state: &AppState) {
|
|
118
|
+
// Bug 2 fix: Ensure 15% margin from right edge (use 85% of width max)
|
|
119
|
+
let max_width = (area.width * 85 / 100).max(10); // At least 10 chars, max 85% of width
|
|
120
|
+
let header_area = Rect {
|
|
121
|
+
x: area.x + 2,
|
|
122
|
+
y: area.y,
|
|
123
|
+
width: max_width.saturating_sub(2), // Additional 2 char margin
|
|
124
|
+
height: 3,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
let spinner = SPINNERS[state.spinner_frame];
|
|
128
|
+
|
|
129
|
+
// Format uptime
|
|
130
|
+
let uptime_str = format_uptime(state.uptime_secs);
|
|
131
|
+
|
|
132
|
+
// Operation mode indicator (Step 7)
|
|
133
|
+
let mode_text = state.operation_mode.as_str();
|
|
134
|
+
let mode_color = match state.operation_mode {
|
|
135
|
+
crate::app::OperationMode::Local => AMBER_WARN,
|
|
136
|
+
crate::app::OperationMode::Connected => NEON_GREEN,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Connection status icon - Phase 1.1: Connection Visibility
|
|
140
|
+
let connection_icon = if state.gateway_healthy && state.connected {
|
|
141
|
+
"[●]" // Green dot - verified & healthy
|
|
142
|
+
} else if state.connected {
|
|
143
|
+
"[○]" // Yellow - connected but not verified
|
|
144
|
+
} else {
|
|
145
|
+
"[ ]" // Gray - not connected
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
let connection_color = if state.gateway_healthy && state.connected {
|
|
149
|
+
NEON_GREEN
|
|
150
|
+
} else if state.connected {
|
|
151
|
+
AMBER_WARN
|
|
152
|
+
} else {
|
|
153
|
+
TEXT_MUTED
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Line 1: Brand + version + mode + uptime - Bug 3 fix: Use "4Runr." with dot (matches brand logo)
|
|
157
|
+
// Version from env 4RUNR_CLI_VERSION (set by Node CLI at launch); fallback for dev without CLI
|
|
158
|
+
let ver_env = std::env::var("4RUNR_CLI_VERSION").unwrap_or_else(|_| "2.9.69".to_string());
|
|
159
|
+
let package_version = ver_env.trim();
|
|
160
|
+
let package_version: &str = if package_version.is_empty() {
|
|
161
|
+
"2.9.69"
|
|
162
|
+
} else {
|
|
163
|
+
package_version
|
|
164
|
+
};
|
|
165
|
+
let brand_line = Line::from(vec![
|
|
166
|
+
Span::styled(
|
|
167
|
+
"4Runr.",
|
|
168
|
+
Style::default()
|
|
169
|
+
.fg(BRAND_PURPLE)
|
|
170
|
+
.add_modifier(Modifier::BOLD),
|
|
171
|
+
),
|
|
172
|
+
Span::styled(" AI AGENT OS", Style::default().fg(BRAND_VIOLET)),
|
|
173
|
+
Span::styled(
|
|
174
|
+
format!(" v{}", package_version),
|
|
175
|
+
Style::default().fg(TEXT_MUTED),
|
|
176
|
+
),
|
|
177
|
+
Span::styled(" Mode: ", Style::default().fg(TEXT_MUTED)),
|
|
178
|
+
Span::styled(
|
|
179
|
+
mode_text,
|
|
180
|
+
Style::default().fg(mode_color).add_modifier(Modifier::BOLD),
|
|
181
|
+
),
|
|
182
|
+
Span::styled(" ", Style::default()),
|
|
183
|
+
Span::styled(
|
|
184
|
+
connection_icon,
|
|
185
|
+
Style::default()
|
|
186
|
+
.fg(connection_color)
|
|
187
|
+
.add_modifier(Modifier::BOLD),
|
|
188
|
+
),
|
|
189
|
+
Span::styled(
|
|
190
|
+
format!(" {} UPTIME: {}", spinner, uptime_str),
|
|
191
|
+
Style::default().fg(TEXT_MUTED),
|
|
192
|
+
),
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
// Line 2: Status bar with Gateway URL - Phase 1.1: Connection Visibility
|
|
196
|
+
// NO long lines that touch right edge (causes scrollbars!)
|
|
197
|
+
let mut status_spans = vec![
|
|
198
|
+
Span::styled(" ", Style::default()), // Indent to align
|
|
199
|
+
Span::styled("*", Style::default().fg(NEON_GREEN)),
|
|
200
|
+
Span::styled(
|
|
201
|
+
" SYSTEM ONLINE ",
|
|
202
|
+
Style::default().fg(NEON_GREEN).add_modifier(Modifier::BOLD),
|
|
203
|
+
),
|
|
204
|
+
Span::styled("*", Style::default().fg(NEON_GREEN)),
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
// Add Gateway info if connected
|
|
208
|
+
if state.connected {
|
|
209
|
+
if let Some(gateway_url) = &state.gateway_url {
|
|
210
|
+
// Extract just the host:port from the URL
|
|
211
|
+
let gateway_display = gateway_url
|
|
212
|
+
.replace("http://", "")
|
|
213
|
+
.replace("https://", "")
|
|
214
|
+
.split('/')
|
|
215
|
+
.next()
|
|
216
|
+
.unwrap_or(gateway_url)
|
|
217
|
+
.to_string();
|
|
218
|
+
|
|
219
|
+
status_spans.push(Span::styled(
|
|
220
|
+
" Gateway: ",
|
|
221
|
+
Style::default().fg(TEXT_MUTED),
|
|
222
|
+
));
|
|
223
|
+
status_spans.push(Span::styled(
|
|
224
|
+
gateway_display,
|
|
225
|
+
Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD),
|
|
226
|
+
));
|
|
227
|
+
|
|
228
|
+
// Add health status if verified
|
|
229
|
+
if state.gateway_healthy {
|
|
230
|
+
status_spans.push(Span::styled(" [✓]", Style::default().fg(NEON_GREEN)));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let status_line = Line::from(status_spans);
|
|
236
|
+
|
|
237
|
+
f.render_widget(
|
|
238
|
+
Paragraph::new(brand_line),
|
|
239
|
+
Rect {
|
|
240
|
+
x: header_area.x,
|
|
241
|
+
y: header_area.y,
|
|
242
|
+
width: header_area.width,
|
|
243
|
+
height: 1,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
f.render_widget(
|
|
247
|
+
Paragraph::new(status_line),
|
|
248
|
+
Rect {
|
|
249
|
+
x: header_area.x,
|
|
250
|
+
y: header_area.y + 1,
|
|
251
|
+
width: header_area.width,
|
|
252
|
+
height: 1,
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Bug 6 fix: Add separator line below header (but stop before right edge)
|
|
257
|
+
let max_sep_width = (header_area.width * 85 / 100).max(20); // Max 85% of width
|
|
258
|
+
let separator = "-".repeat(max_sep_width as usize);
|
|
259
|
+
f.render_widget(
|
|
260
|
+
Paragraph::new(Line::from(Span::styled(
|
|
261
|
+
separator,
|
|
262
|
+
Style::default().fg(TEXT_MUTED),
|
|
263
|
+
))),
|
|
264
|
+
Rect {
|
|
265
|
+
x: header_area.x,
|
|
266
|
+
y: header_area.y + 2,
|
|
267
|
+
width: max_sep_width,
|
|
268
|
+
height: 1,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn format_uptime(secs: u64) -> String {
|
|
274
|
+
let hours = secs / 3600;
|
|
275
|
+
let mins = (secs % 3600) / 60;
|
|
276
|
+
let secs = secs % 60;
|
|
277
|
+
|
|
278
|
+
if hours > 0 {
|
|
279
|
+
format!("{}h {}m {}s", hours, mins, secs)
|
|
280
|
+
} else if mins > 0 {
|
|
281
|
+
format!("{}m {}s", mins, secs)
|
|
282
|
+
} else {
|
|
283
|
+
format!("{}s", secs)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn render_content(f: &mut Frame, area: Rect, state: &AppState) {
|
|
288
|
+
// Two-column layout: Left (system) + Right (logs & capabilities)
|
|
289
|
+
let cols = Layout::default()
|
|
290
|
+
.direction(Direction::Horizontal)
|
|
291
|
+
.constraints([
|
|
292
|
+
Constraint::Percentage(35), // Left panel
|
|
293
|
+
Constraint::Percentage(65), // Right panel
|
|
294
|
+
])
|
|
295
|
+
.split(area);
|
|
296
|
+
|
|
297
|
+
render_left_column(f, cols[0], state);
|
|
298
|
+
|
|
299
|
+
// Right column: Operations Log + Capabilities
|
|
300
|
+
// Bug 2 fix: Ensure 15% margin from right edge (use 85% of width max)
|
|
301
|
+
let max_width = (cols[1].width * 85 / 100).max(10); // At least 10 chars, max 85% of width
|
|
302
|
+
let right_chunks = Layout::default()
|
|
303
|
+
.direction(Direction::Vertical)
|
|
304
|
+
.constraints([
|
|
305
|
+
Constraint::Percentage(70), // Operations log (main focus)
|
|
306
|
+
Constraint::Percentage(30), // Capabilities
|
|
307
|
+
])
|
|
308
|
+
.split(Rect {
|
|
309
|
+
x: cols[1].x,
|
|
310
|
+
y: cols[1].y,
|
|
311
|
+
width: max_width.saturating_sub(2), // Additional 2 char margin for safety
|
|
312
|
+
height: cols[1].height,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
render_center_column(f, right_chunks[0], state);
|
|
316
|
+
|
|
317
|
+
// === ADD SEPARATOR BETWEEN OPERATIONS LOG AND CAPABILITIES ===
|
|
318
|
+
// IMPORTANT: Stop separator well before right edge (don't go too far on x-axis)
|
|
319
|
+
if right_chunks[1].y > right_chunks[0].y + right_chunks[0].height {
|
|
320
|
+
// Use the operations log panel width, not the full column width
|
|
321
|
+
let ops_panel_width = right_chunks[0].width;
|
|
322
|
+
let safe_sep_width = (ops_panel_width * 80 / 100).min(ops_panel_width.saturating_sub(15)); // Stop well before right edge
|
|
323
|
+
render_separator(
|
|
324
|
+
f,
|
|
325
|
+
Rect {
|
|
326
|
+
x: right_chunks[0].x,
|
|
327
|
+
y: right_chunks[0].y + right_chunks[0].height,
|
|
328
|
+
width: safe_sep_width,
|
|
329
|
+
height: 1,
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
render_right_column(f, right_chunks[1], state);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fn render_left_column(f: &mut Frame, area: Rect, state: &AppState) {
|
|
338
|
+
let panel_area = Rect {
|
|
339
|
+
x: area.x + 1,
|
|
340
|
+
y: area.y,
|
|
341
|
+
width: area.width.saturating_sub(2),
|
|
342
|
+
height: area.height,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Split into System Status and Resources
|
|
346
|
+
let chunks = Layout::default()
|
|
347
|
+
.direction(Direction::Vertical)
|
|
348
|
+
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
|
|
349
|
+
.split(panel_area);
|
|
350
|
+
|
|
351
|
+
// === SYSTEM STATUS PANEL ===
|
|
352
|
+
render_panel_border(f, chunks[0], "SYSTEM STATUS", BRAND_PURPLE);
|
|
353
|
+
|
|
354
|
+
let status_content = inner_rect(chunks[0], 2);
|
|
355
|
+
let mut lines = vec![];
|
|
356
|
+
|
|
357
|
+
// Use STATIC dots for status indicators (not animated - prevents flashing when typing)
|
|
358
|
+
// Instructions: "DON'T use animated pulse for status dots - they flash weirdly when typing!"
|
|
359
|
+
|
|
360
|
+
// Posture status - Use ASCII * instead of Unicode ◉
|
|
361
|
+
let posture_color = if state.posture_status == "Healthy" || state.connected {
|
|
362
|
+
NEON_GREEN
|
|
363
|
+
} else {
|
|
364
|
+
AMBER_WARN
|
|
365
|
+
};
|
|
366
|
+
lines.push(Line::from(vec![
|
|
367
|
+
Span::styled("* ", Style::default().fg(posture_color)),
|
|
368
|
+
Span::styled("POSTURE ", Style::default().fg(TEXT_DIM)),
|
|
369
|
+
Span::styled(
|
|
370
|
+
&state.posture_status,
|
|
371
|
+
Style::default()
|
|
372
|
+
.fg(posture_color)
|
|
373
|
+
.add_modifier(Modifier::BOLD),
|
|
374
|
+
),
|
|
375
|
+
]));
|
|
376
|
+
|
|
377
|
+
// Shield status
|
|
378
|
+
let shield_color = match state.shield_mode.as_str() {
|
|
379
|
+
"enforce" => NEON_GREEN,
|
|
380
|
+
"monitor" => AMBER_WARN,
|
|
381
|
+
_ => TEXT_MUTED,
|
|
382
|
+
};
|
|
383
|
+
lines.push(Line::from(vec![
|
|
384
|
+
Span::styled("* ", Style::default().fg(shield_color)),
|
|
385
|
+
Span::styled("SHIELD ", Style::default().fg(TEXT_DIM)),
|
|
386
|
+
Span::styled(
|
|
387
|
+
state.shield_mode.to_uppercase(),
|
|
388
|
+
Style::default()
|
|
389
|
+
.fg(shield_color)
|
|
390
|
+
.add_modifier(Modifier::BOLD),
|
|
391
|
+
),
|
|
392
|
+
Span::styled(
|
|
393
|
+
format!(" ({} active)", state.shield_detectors.len()),
|
|
394
|
+
Style::default().fg(TEXT_DIM),
|
|
395
|
+
),
|
|
396
|
+
]));
|
|
397
|
+
|
|
398
|
+
// Sentinel status
|
|
399
|
+
let sentinel_color = match state.sentinel_state.as_str() {
|
|
400
|
+
"watching" => CYBER_CYAN,
|
|
401
|
+
"triggered" => Color::Rgb(255, 69, 58),
|
|
402
|
+
_ => TEXT_DIM,
|
|
403
|
+
};
|
|
404
|
+
let mut sentinel_line = vec![
|
|
405
|
+
Span::styled("* ", Style::default().fg(sentinel_color)),
|
|
406
|
+
Span::styled("SENTINEL ", Style::default().fg(TEXT_DIM)),
|
|
407
|
+
Span::styled(
|
|
408
|
+
state.sentinel_state.to_uppercase(),
|
|
409
|
+
Style::default()
|
|
410
|
+
.fg(sentinel_color)
|
|
411
|
+
.add_modifier(Modifier::BOLD),
|
|
412
|
+
),
|
|
413
|
+
];
|
|
414
|
+
if state.sentinel_active_runs > 0 {
|
|
415
|
+
sentinel_line.push(Span::styled(
|
|
416
|
+
format!(" ({} runs)", state.sentinel_active_runs),
|
|
417
|
+
Style::default().fg(TEXT_DIM),
|
|
418
|
+
));
|
|
419
|
+
}
|
|
420
|
+
lines.push(Line::from(sentinel_line));
|
|
421
|
+
|
|
422
|
+
// Bug 6 fix: Add visual separator between sections (but stop before right edge)
|
|
423
|
+
// Use ASCII dashes, max 85% of width to avoid scrollbars
|
|
424
|
+
let max_sep_width = (status_content.width * 85 / 100).max(10).min(30); // Max 30 chars or 85% of width
|
|
425
|
+
let separator = "-".repeat(max_sep_width as usize);
|
|
426
|
+
lines.push(Line::from(Span::styled(
|
|
427
|
+
separator,
|
|
428
|
+
Style::default().fg(TEXT_MUTED),
|
|
429
|
+
)));
|
|
430
|
+
|
|
431
|
+
// Show enabled detectors with STATIC dots (not animated)
|
|
432
|
+
for detector in &state.shield_detectors {
|
|
433
|
+
let name = match detector.as_str() {
|
|
434
|
+
"pii" => "PII Detection",
|
|
435
|
+
"injection" => "Injection Block",
|
|
436
|
+
"hallucination" => "Hallucination Check",
|
|
437
|
+
_ => detector.as_str(),
|
|
438
|
+
};
|
|
439
|
+
lines.push(Line::from(vec![
|
|
440
|
+
Span::styled("* ", Style::default().fg(NEON_GREEN)), // Static asterisk
|
|
441
|
+
Span::styled(name, Style::default().fg(TEXT_PRIMARY)),
|
|
442
|
+
]));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Connection status
|
|
446
|
+
if !state.connected {
|
|
447
|
+
lines.push(Line::from(""));
|
|
448
|
+
lines.push(Line::from(vec![
|
|
449
|
+
Span::styled("* ", Style::default().fg(AMBER_WARN)), // Use ASCII * instead of Unicode ⚠
|
|
450
|
+
Span::styled("Demo Mode - Not connected", Style::default().fg(AMBER_WARN)),
|
|
451
|
+
]));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
f.render_widget(Paragraph::new(lines), status_content);
|
|
455
|
+
|
|
456
|
+
// Bug 6 fix: Add separator between System Status and Resources sections
|
|
457
|
+
let max_sep_width = (panel_area.width * 85 / 100).max(10).min(30);
|
|
458
|
+
let separator = "-".repeat(max_sep_width as usize);
|
|
459
|
+
let sep_y = chunks[0].y + chunks[0].height;
|
|
460
|
+
f.render_widget(
|
|
461
|
+
Paragraph::new(Line::from(Span::styled(
|
|
462
|
+
separator,
|
|
463
|
+
Style::default().fg(TEXT_MUTED),
|
|
464
|
+
))),
|
|
465
|
+
Rect {
|
|
466
|
+
x: panel_area.x,
|
|
467
|
+
y: sep_y,
|
|
468
|
+
width: max_sep_width,
|
|
469
|
+
height: 1,
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// === RESOURCES PANEL ===
|
|
474
|
+
render_panel_border(f, chunks[1], "RESOURCES", TEXT_DIM);
|
|
475
|
+
let resources_content = inner_rect(chunks[1], 2);
|
|
476
|
+
|
|
477
|
+
let network_color = if state.connected {
|
|
478
|
+
NEON_GREEN
|
|
479
|
+
} else {
|
|
480
|
+
TEXT_MUTED
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
let mut lines = vec![
|
|
484
|
+
render_progress_bar("CPU", state.cpu, cpu_color(state.cpu)),
|
|
485
|
+
render_progress_bar("MEM", state.mem, mem_color(state.mem)),
|
|
486
|
+
Line::from(""),
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
// Network info - Use ASCII * instead of Unicode ◉
|
|
490
|
+
lines.push(Line::from(vec![
|
|
491
|
+
Span::styled("NET ", Style::default().fg(TEXT_DIM)),
|
|
492
|
+
Span::styled("* ", Style::default().fg(network_color)),
|
|
493
|
+
Span::styled(&state.network_status, Style::default().fg(network_color)),
|
|
494
|
+
]));
|
|
495
|
+
if state.connected {
|
|
496
|
+
lines.push(Line::from(vec![
|
|
497
|
+
Span::styled(" Runs: ", Style::default().fg(TEXT_DIM)),
|
|
498
|
+
Span::styled(
|
|
499
|
+
format!("{}", state.total_runs),
|
|
500
|
+
Style::default().fg(TEXT_PRIMARY),
|
|
501
|
+
),
|
|
502
|
+
]));
|
|
503
|
+
}
|
|
504
|
+
lines.push(Line::from(""));
|
|
505
|
+
lines.push(Line::from(vec![
|
|
506
|
+
Span::styled("Uptime: ", Style::default().fg(TEXT_DIM)),
|
|
507
|
+
Span::styled(
|
|
508
|
+
format_uptime(state.uptime_secs),
|
|
509
|
+
Style::default().fg(TEXT_PRIMARY),
|
|
510
|
+
),
|
|
511
|
+
]));
|
|
512
|
+
|
|
513
|
+
f.render_widget(Paragraph::new(lines), resources_content);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
fn render_panel_border(f: &mut Frame, area: Rect, title: &str, color: Color) {
|
|
517
|
+
// Top border with title - KEEP SHORT to avoid scrollbars!
|
|
518
|
+
// Instructions: "Only draw title, no long horizontal lines"
|
|
519
|
+
// Use ASCII characters that work everywhere
|
|
520
|
+
let title_line = Line::from(vec![
|
|
521
|
+
Span::styled("[ ", Style::default().fg(TEXT_MUTED)), // ASCII [ instead of Unicode ┌
|
|
522
|
+
Span::styled(
|
|
523
|
+
title,
|
|
524
|
+
Style::default().fg(color).add_modifier(Modifier::BOLD),
|
|
525
|
+
),
|
|
526
|
+
Span::styled(" ", Style::default()),
|
|
527
|
+
// Only short dash, not full width - instructions say this causes scrollbars!
|
|
528
|
+
Span::styled("-", Style::default().fg(TEXT_MUTED)), // Just a short dash, not full width
|
|
529
|
+
]);
|
|
530
|
+
// Make sure width doesn't touch right edge
|
|
531
|
+
let title_width = (title.len() as u16 + 4).min(area.width.saturating_sub(6));
|
|
532
|
+
f.render_widget(
|
|
533
|
+
Paragraph::new(title_line),
|
|
534
|
+
Rect {
|
|
535
|
+
x: area.x,
|
|
536
|
+
y: area.y,
|
|
537
|
+
width: title_width, // Only as wide as needed, not full width
|
|
538
|
+
height: 1,
|
|
539
|
+
},
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// NO bottom border - instructions say it causes scrollbars!
|
|
543
|
+
// The visual separation comes from spacing and color
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fn inner_rect(area: Rect, padding: u16) -> Rect {
|
|
547
|
+
Rect {
|
|
548
|
+
x: area.x + padding,
|
|
549
|
+
y: area.y + 1,
|
|
550
|
+
width: area.width.saturating_sub(padding * 2 + 2),
|
|
551
|
+
height: area.height.saturating_sub(2),
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
fn render_progress_bar(label: &str, value: f64, color: Color) -> Line<'static> {
|
|
556
|
+
let bar_width = 12;
|
|
557
|
+
let filled = ((value * bar_width as f64) as usize).min(bar_width);
|
|
558
|
+
let empty = bar_width - filled;
|
|
559
|
+
|
|
560
|
+
Line::from(vec![
|
|
561
|
+
Span::styled(format!("{} ", label), Style::default().fg(TEXT_DIM)),
|
|
562
|
+
Span::styled("█".repeat(filled), Style::default().fg(color)),
|
|
563
|
+
Span::styled("░".repeat(empty), Style::default().fg(TEXT_MUTED)),
|
|
564
|
+
Span::styled(
|
|
565
|
+
format!(" {:>3.0}%", value * 100.0),
|
|
566
|
+
Style::default().fg(color),
|
|
567
|
+
),
|
|
568
|
+
])
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
fn cpu_color(value: f64) -> Color {
|
|
572
|
+
if value > 0.8 {
|
|
573
|
+
Color::Rgb(255, 69, 58)
|
|
574
|
+
}
|
|
575
|
+
// Red
|
|
576
|
+
else if value > 0.5 {
|
|
577
|
+
Color::Rgb(255, 191, 0)
|
|
578
|
+
}
|
|
579
|
+
// Amber
|
|
580
|
+
else {
|
|
581
|
+
NEON_GREEN
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
fn mem_color(value: f64) -> Color {
|
|
586
|
+
if value > 0.8 {
|
|
587
|
+
Color::Rgb(255, 69, 58)
|
|
588
|
+
} else if value > 0.6 {
|
|
589
|
+
Color::Rgb(255, 191, 0)
|
|
590
|
+
} else {
|
|
591
|
+
CYBER_CYAN
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
|
|
596
|
+
let panel_area = Rect {
|
|
597
|
+
x: area.x + 1,
|
|
598
|
+
y: area.y,
|
|
599
|
+
width: area.width.saturating_sub(2),
|
|
600
|
+
height: area.height,
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// === OPERATIONS LOG - More contained/closed appearance ===
|
|
604
|
+
// Draw a proper box border for a more "closed up" look
|
|
605
|
+
let border_block = Block::default()
|
|
606
|
+
.title(" OPERATIONS LOG ")
|
|
607
|
+
.title_style(Style::default().fg(CYBER_CYAN).add_modifier(Modifier::BOLD))
|
|
608
|
+
.borders(Borders::ALL)
|
|
609
|
+
.border_style(Style::default().fg(TEXT_MUTED));
|
|
610
|
+
|
|
611
|
+
f.render_widget(border_block, panel_area);
|
|
612
|
+
|
|
613
|
+
// Content area with padding (inside the border)
|
|
614
|
+
let content_area = Rect {
|
|
615
|
+
x: panel_area.x + 1,
|
|
616
|
+
y: panel_area.y + 1,
|
|
617
|
+
width: panel_area.width.saturating_sub(4), // Leave room for border (2) + scrollbar (2)
|
|
618
|
+
height: panel_area.height.saturating_sub(2), // Top and bottom borders
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
let visible_height = content_area.height as usize;
|
|
622
|
+
let total_logs = state.logs.len();
|
|
623
|
+
let max_scroll = total_logs.saturating_sub(visible_height.max(1));
|
|
624
|
+
|
|
625
|
+
// Calculate which logs to show (scroll_pos = 0 means newest, higher = older)
|
|
626
|
+
let start_idx = state.log_scroll.min(max_scroll);
|
|
627
|
+
let _end_idx = (start_idx + visible_height).min(total_logs);
|
|
628
|
+
|
|
629
|
+
let spinner = SPINNERS[state.spinner_frame];
|
|
630
|
+
|
|
631
|
+
let mut lines = vec![Line::from(vec![
|
|
632
|
+
Span::styled(spinner, Style::default().fg(CYBER_CYAN)),
|
|
633
|
+
Span::styled(
|
|
634
|
+
" Monitoring...",
|
|
635
|
+
Style::default().fg(TEXT_DIM).add_modifier(Modifier::ITALIC),
|
|
636
|
+
),
|
|
637
|
+
])];
|
|
638
|
+
|
|
639
|
+
// Show real logs with proper formatting (reversed order - newest first)
|
|
640
|
+
for log in state.logs.iter().rev().skip(start_idx).take(visible_height) {
|
|
641
|
+
if log.starts_with("[") {
|
|
642
|
+
// Parse log format: [COMPONENT] message
|
|
643
|
+
if let Some(bracket_end) = log.find(']') {
|
|
644
|
+
let component = &log[1..bracket_end];
|
|
645
|
+
let message = log[bracket_end + 1..].trim();
|
|
646
|
+
|
|
647
|
+
let comp_color = match component {
|
|
648
|
+
"GATEWAY" => CYBER_CYAN,
|
|
649
|
+
"SHIELD" => BRAND_PURPLE,
|
|
650
|
+
"SENTINEL" => NEON_GREEN,
|
|
651
|
+
"WORKER" => AMBER_WARN,
|
|
652
|
+
"SYSTEM" => TEXT_DIM,
|
|
653
|
+
"HELP" => CYBER_CYAN,
|
|
654
|
+
_ => TEXT_DIM,
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
lines.push(Line::from(vec![
|
|
658
|
+
Span::styled("> ", Style::default().fg(comp_color)), // Use ASCII > instead of Unicode ▸
|
|
659
|
+
Span::styled(format!("[{}] ", component), Style::default().fg(comp_color)),
|
|
660
|
+
Span::styled(message, Style::default().fg(TEXT_PRIMARY)),
|
|
661
|
+
]));
|
|
662
|
+
} else {
|
|
663
|
+
lines.push(Line::from(Span::styled(
|
|
664
|
+
log.as_str(),
|
|
665
|
+
Style::default().fg(TEXT_PRIMARY),
|
|
666
|
+
)));
|
|
667
|
+
}
|
|
668
|
+
} else if log.starts_with(">") {
|
|
669
|
+
// Command echo
|
|
670
|
+
lines.push(Line::from(vec![
|
|
671
|
+
Span::styled("> ", Style::default().fg(BRAND_PURPLE)), // Use ASCII > instead of Unicode ▸
|
|
672
|
+
Span::styled(log, Style::default().fg(BRAND_PURPLE)),
|
|
673
|
+
]));
|
|
674
|
+
} else {
|
|
675
|
+
lines.push(Line::from(Span::styled(
|
|
676
|
+
log.as_str(),
|
|
677
|
+
Style::default().fg(TEXT_PRIMARY),
|
|
678
|
+
)));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Connection status at bottom
|
|
683
|
+
if !state.connected {
|
|
684
|
+
lines.push(Line::from(""));
|
|
685
|
+
lines.push(Line::from(vec![
|
|
686
|
+
Span::styled("* ", Style::default().fg(AMBER_WARN)), // Use ASCII * instead of Unicode ⚠
|
|
687
|
+
Span::styled(
|
|
688
|
+
"Demo Mode - Not connected to Gateway",
|
|
689
|
+
Style::default().fg(AMBER_WARN),
|
|
690
|
+
),
|
|
691
|
+
]));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
f.render_widget(Paragraph::new(lines), content_area);
|
|
695
|
+
|
|
696
|
+
// === RENDER CLEAN SCROLLBAR (only if scrollable) ===
|
|
697
|
+
// CRITICAL FIX: Only render scrollbar when actually scrollable to prevent flickering
|
|
698
|
+
if total_logs > visible_height && max_scroll > 0 {
|
|
699
|
+
// Scrollbar positioned inside the border, 1 char from right edge
|
|
700
|
+
// Use fixed position to prevent jitter
|
|
701
|
+
let scrollbar_x = panel_area.x + panel_area.width.saturating_sub(3);
|
|
702
|
+
let scrollbar_y = content_area.y;
|
|
703
|
+
let scrollbar_height = content_area.height;
|
|
704
|
+
|
|
705
|
+
// CRITICAL: Use integer math to prevent floating point rounding issues
|
|
706
|
+
// Calculate thumb size (minimum 1 char, proportional to visible/total)
|
|
707
|
+
let thumb_height =
|
|
708
|
+
((visible_height as u32 * scrollbar_height as u32) / total_logs as u32) as u16;
|
|
709
|
+
let thumb_height = thumb_height.max(1).min(scrollbar_height);
|
|
710
|
+
|
|
711
|
+
// Calculate thumb position using integer math for stability
|
|
712
|
+
let thumb_start = if max_scroll > 0 {
|
|
713
|
+
// Use integer division to avoid floating point jitter
|
|
714
|
+
((state.log_scroll as u32 * (scrollbar_height.saturating_sub(thumb_height) as u32))
|
|
715
|
+
/ max_scroll as u32) as u16
|
|
716
|
+
} else {
|
|
717
|
+
0
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// Clamp thumb_start to valid range
|
|
721
|
+
let thumb_start = thumb_start.min(scrollbar_height.saturating_sub(thumb_height));
|
|
722
|
+
let thumb_end = (thumb_start + thumb_height).min(scrollbar_height);
|
|
723
|
+
|
|
724
|
+
// Render scrollbar track (entire height) as single operation to prevent flicker
|
|
725
|
+
// Build scrollbar string first, then render once
|
|
726
|
+
let mut scrollbar_chars = vec![String::from("│"); scrollbar_height as usize];
|
|
727
|
+
|
|
728
|
+
// Fill thumb portion
|
|
729
|
+
for i in thumb_start..thumb_end {
|
|
730
|
+
if i < scrollbar_height {
|
|
731
|
+
scrollbar_chars[i as usize] = String::from("█");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Render entire scrollbar at once (prevents flickering from multiple renders)
|
|
736
|
+
for (i, c) in scrollbar_chars.iter().enumerate() {
|
|
737
|
+
let y = scrollbar_y + i as u16;
|
|
738
|
+
if y < scrollbar_y + scrollbar_height {
|
|
739
|
+
let color = if i >= thumb_start as usize && i < thumb_end as usize {
|
|
740
|
+
CYBER_CYAN
|
|
741
|
+
} else {
|
|
742
|
+
TEXT_MUTED
|
|
743
|
+
};
|
|
744
|
+
f.render_widget(
|
|
745
|
+
Paragraph::new(c.as_str()).style(Style::default().fg(color)),
|
|
746
|
+
Rect {
|
|
747
|
+
x: scrollbar_x,
|
|
748
|
+
y,
|
|
749
|
+
width: 1,
|
|
750
|
+
height: 1,
|
|
751
|
+
},
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
fn render_right_column(f: &mut Frame, area: Rect, state: &AppState) {
|
|
759
|
+
// This now only renders CAPABILITIES (Network is shown in left panel)
|
|
760
|
+
let panel_area = Rect {
|
|
761
|
+
x: area.x + 1,
|
|
762
|
+
y: area.y,
|
|
763
|
+
width: area.width.saturating_sub(4), // Extra margin on right
|
|
764
|
+
height: area.height,
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// === CAPABILITIES PANEL ===
|
|
768
|
+
render_panel_border(f, panel_area, "CAPABILITIES", BRAND_PURPLE);
|
|
769
|
+
let caps_content = inner_rect(panel_area, 2);
|
|
770
|
+
|
|
771
|
+
// Use STATIC indicators (not animated pulse)
|
|
772
|
+
let mut cap_lines: Vec<Line> = vec![];
|
|
773
|
+
|
|
774
|
+
if state.capabilities.is_empty() {
|
|
775
|
+
cap_lines.push(Line::from(vec![
|
|
776
|
+
Span::styled("- ", Style::default().fg(TEXT_MUTED)),
|
|
777
|
+
Span::styled("No agents registered", Style::default().fg(TEXT_DIM)),
|
|
778
|
+
]));
|
|
779
|
+
cap_lines.push(Line::from(vec![
|
|
780
|
+
Span::styled(" ", Style::default()),
|
|
781
|
+
Span::styled(
|
|
782
|
+
"Use DevKit to register agents",
|
|
783
|
+
Style::default().fg(TEXT_MUTED),
|
|
784
|
+
),
|
|
785
|
+
]));
|
|
786
|
+
} else {
|
|
787
|
+
for cap in state
|
|
788
|
+
.capabilities
|
|
789
|
+
.iter()
|
|
790
|
+
.take(caps_content.height.saturating_sub(1) as usize)
|
|
791
|
+
{
|
|
792
|
+
cap_lines.push(Line::from(vec![
|
|
793
|
+
Span::styled("+ ", Style::default().fg(BRAND_PURPLE)), // Static plus sign
|
|
794
|
+
Span::styled(cap.as_str(), Style::default().fg(TEXT_PRIMARY)),
|
|
795
|
+
Span::styled(" ", Style::default()),
|
|
796
|
+
Span::styled("* READY", Style::default().fg(NEON_GREEN)), // Static asterisk
|
|
797
|
+
]));
|
|
798
|
+
}
|
|
799
|
+
cap_lines.push(Line::from(vec![Span::styled(
|
|
800
|
+
format!("{} agents", state.capabilities.len()),
|
|
801
|
+
Style::default().fg(TEXT_DIM),
|
|
802
|
+
)]));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
f.render_widget(Paragraph::new(cap_lines), caps_content);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
fn render_command_box(f: &mut Frame, area: Rect, state: &AppState) {
|
|
809
|
+
// Bug 2 fix: Ensure 15% margin from right edge (use 85% of width max)
|
|
810
|
+
let max_width = (area.width * 85 / 100).max(10); // At least 10 chars, max 85% of width
|
|
811
|
+
let bar_area = Rect {
|
|
812
|
+
x: area.x + 2,
|
|
813
|
+
y: area.y,
|
|
814
|
+
width: max_width.saturating_sub(2), // Additional 2 char margin
|
|
815
|
+
height: area.height,
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// NO separator line - instructions say it causes scrollbars!
|
|
819
|
+
// Just use spacing for visual separation
|
|
820
|
+
|
|
821
|
+
// Command prompt - Use ASCII > instead of Unicode ▶
|
|
822
|
+
// Note: We show a visual cursor character "_" in the text, but the real cursor
|
|
823
|
+
// position is handled separately below
|
|
824
|
+
let prompt_line = Line::from(vec![
|
|
825
|
+
Span::styled("> ", Style::default().fg(BRAND_PURPLE)), // ASCII > instead of Unicode ▶
|
|
826
|
+
Span::styled(
|
|
827
|
+
"4runr",
|
|
828
|
+
Style::default()
|
|
829
|
+
.fg(BRAND_VIOLET)
|
|
830
|
+
.add_modifier(Modifier::BOLD),
|
|
831
|
+
),
|
|
832
|
+
Span::styled(": ", Style::default().fg(TEXT_DIM)), // ASCII : instead of Unicode ›
|
|
833
|
+
Span::styled(&state.command_input, Style::default().fg(TEXT_PRIMARY)),
|
|
834
|
+
Span::styled("_", Style::default().fg(CYBER_CYAN)), // Visual cursor indicator
|
|
835
|
+
]);
|
|
836
|
+
f.render_widget(
|
|
837
|
+
Paragraph::new(prompt_line),
|
|
838
|
+
Rect {
|
|
839
|
+
x: bar_area.x,
|
|
840
|
+
y: bar_area.y + 1,
|
|
841
|
+
width: bar_area.width,
|
|
842
|
+
height: 1,
|
|
843
|
+
},
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
// Help hints - Use ASCII | instead of Unicode │
|
|
847
|
+
let help_line = Line::from(vec![
|
|
848
|
+
Span::styled("Ctrl+C", Style::default().fg(BRAND_VIOLET)),
|
|
849
|
+
Span::styled(" exit | ", Style::default().fg(TEXT_MUTED)), // ASCII | instead of Unicode │
|
|
850
|
+
Span::styled("F10", Style::default().fg(BRAND_VIOLET)),
|
|
851
|
+
Span::styled(" quit | ", Style::default().fg(TEXT_MUTED)), // ASCII | instead of Unicode │
|
|
852
|
+
Span::styled("help", Style::default().fg(BRAND_VIOLET)),
|
|
853
|
+
Span::styled(" commands", Style::default().fg(TEXT_MUTED)),
|
|
854
|
+
]);
|
|
855
|
+
f.render_widget(
|
|
856
|
+
Paragraph::new(help_line),
|
|
857
|
+
Rect {
|
|
858
|
+
x: bar_area.x,
|
|
859
|
+
y: bar_area.y + 2,
|
|
860
|
+
width: bar_area.width,
|
|
861
|
+
height: 1,
|
|
862
|
+
},
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
// CRITICAL FIX: Only set cursor position when user is actively typing
|
|
866
|
+
// This prevents cursor from appearing in wrong places (operations log, etc.)
|
|
867
|
+
// IMPORTANT: In Ratatui, set_cursor() shows the cursor, so we only call it here
|
|
868
|
+
if state.command_focused || !state.command_input.is_empty() {
|
|
869
|
+
// Calculate correct cursor position: "> 4runr: " = 9 chars
|
|
870
|
+
let prompt_len = 9u16; // "> 4runr: " = 9 characters ("> " + "4runr" + ": " = 2+5+2 = 9)
|
|
871
|
+
let input_len = state.command_input.len() as u16;
|
|
872
|
+
let cursor_x = bar_area.x + prompt_len + input_len;
|
|
873
|
+
let cursor_y = bar_area.y + 1;
|
|
874
|
+
|
|
875
|
+
// Ensure cursor doesn't go beyond the safe area (respects 15% margin)
|
|
876
|
+
let max_x = (bar_area.x + bar_area.width).saturating_sub(1);
|
|
877
|
+
let final_x = cursor_x.min(max_x);
|
|
878
|
+
|
|
879
|
+
// Only set cursor if we're actually at the input field
|
|
880
|
+
// This prevents cursor from appearing elsewhere (operations log, etc.)
|
|
881
|
+
f.set_cursor(final_x, cursor_y);
|
|
882
|
+
}
|
|
883
|
+
// IMPORTANT: If not focused and no input, we don't call set_cursor()
|
|
884
|
+
// This keeps the cursor hidden (hidden at start of main loop)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
fn render_too_small(f: &mut Frame, viewport: &SafeViewport) {
|
|
888
|
+
let msg = vec![
|
|
889
|
+
Line::from(Span::styled(
|
|
890
|
+
"4Runr.",
|
|
891
|
+
Style::default()
|
|
892
|
+
.fg(BRAND_PURPLE)
|
|
893
|
+
.add_modifier(Modifier::BOLD),
|
|
894
|
+
)), // Bug 3 fix: Use "4Runr." with dot
|
|
895
|
+
Line::from(""),
|
|
896
|
+
Line::from(Span::styled(
|
|
897
|
+
"Terminal too small",
|
|
898
|
+
Style::default().fg(Color::Rgb(255, 69, 58)),
|
|
899
|
+
)),
|
|
900
|
+
Line::from(""),
|
|
901
|
+
Line::from(Span::styled(
|
|
902
|
+
format!("Current: {}x{}", viewport.safe_cols, viewport.safe_rows),
|
|
903
|
+
Style::default().fg(TEXT_DIM),
|
|
904
|
+
)),
|
|
905
|
+
Line::from(Span::styled(
|
|
906
|
+
format!("Required: {}x{}", MIN_COLS, MIN_ROWS),
|
|
907
|
+
Style::default().fg(TEXT_PRIMARY),
|
|
908
|
+
)),
|
|
909
|
+
];
|
|
910
|
+
|
|
911
|
+
f.render_widget(
|
|
912
|
+
Paragraph::new(msg).alignment(Alignment::Center),
|
|
913
|
+
viewport.safe_rect,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
fn render_perf_overlay(f: &mut Frame, area: Rect, state: &AppState) {
|
|
918
|
+
// Calculate RPS from recent render durations
|
|
919
|
+
let rps = if !state.render_durations.is_empty() {
|
|
920
|
+
let avg_ms: f64 =
|
|
921
|
+
state.render_durations.iter().sum::<u64>() as f64 / state.render_durations.len() as f64;
|
|
922
|
+
if avg_ms > 0.0 {
|
|
923
|
+
(1000.0 / avg_ms) as u64
|
|
924
|
+
} else {
|
|
925
|
+
0
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
0
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
let last_render_ms = if !state.render_durations.is_empty() {
|
|
932
|
+
*state.render_durations.back().unwrap()
|
|
933
|
+
} else {
|
|
934
|
+
0
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
let overlay_text = format!(
|
|
938
|
+
"PERF OVERLAY (F12 to toggle)\n\
|
|
939
|
+
RPS: {} | Last render: {}ms | Total renders: {}\n\
|
|
940
|
+
Render scheduled: {} | Log writes: {}",
|
|
941
|
+
rps,
|
|
942
|
+
last_render_ms,
|
|
943
|
+
state.render_count,
|
|
944
|
+
state.render_scheduled_count,
|
|
945
|
+
state.log_write_count
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
let overlay_area = Rect {
|
|
949
|
+
x: area.width.saturating_sub(60),
|
|
950
|
+
y: 2,
|
|
951
|
+
width: 58,
|
|
952
|
+
height: 5,
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
let block = Block::default()
|
|
956
|
+
.borders(Borders::ALL)
|
|
957
|
+
.border_style(Style::default().fg(Color::Yellow))
|
|
958
|
+
.title(" Performance ");
|
|
959
|
+
|
|
960
|
+
let paragraph = Paragraph::new(overlay_text)
|
|
961
|
+
.block(block)
|
|
962
|
+
.style(Style::default().fg(Color::White))
|
|
963
|
+
.wrap(Wrap { trim: true });
|
|
964
|
+
|
|
965
|
+
f.render_widget(paragraph, overlay_area);
|
|
966
|
+
}
|