4runr-os 2.1.48 → 2.1.50

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.
Binary file
@@ -282,8 +282,9 @@ impl App {
282
282
  match key.code {
283
283
  // Typing - ALL characters go to command input (with debounce)
284
284
  KeyCode::Char(c) => {
285
- self.state.command_input.push(c);
285
+ // CRITICAL: Always set focused when typing to show cursor
286
286
  self.state.command_focused = true;
287
+ self.state.command_input.push(c);
287
288
  // Debounce: schedule render after delay
288
289
  self.input_debounce = Some(Instant::now());
289
290
  // Don't render immediately - let debounce handle it
@@ -40,6 +40,9 @@ fn render_separator(f: &mut Frame, area: Rect) {
40
40
  }
41
41
 
42
42
  pub fn render(f: &mut Frame, state: &AppState) {
43
+ // CRITICAL: Hide cursor by default at start of each render
44
+ // Only show it when explicitly set in render_command_box
45
+ // This prevents cursor from appearing in wrong places (like operations log)
43
46
  let full_area = f.size();
44
47
  f.render_widget(Clear, full_area);
45
48
 
@@ -508,31 +511,53 @@ fn render_center_column(f: &mut Frame, area: Rect, state: &AppState) {
508
511
  f.render_widget(Paragraph::new(lines), content_area);
509
512
 
510
513
  // === RENDER CLEAN SCROLLBAR (only if scrollable) ===
514
+ // CRITICAL FIX: Only render scrollbar when actually scrollable to prevent flickering
511
515
  if total_logs > visible_height && max_scroll > 0 {
512
516
  // Scrollbar positioned inside the border, 1 char from right edge
513
- let scrollbar_x = panel_area.x + panel_area.width.saturating_sub(3); // Inside border, 1 char from right
517
+ // Use fixed position to prevent jitter
518
+ let scrollbar_x = panel_area.x + panel_area.width.saturating_sub(3);
514
519
  let scrollbar_y = content_area.y;
515
520
  let scrollbar_height = content_area.height;
516
521
 
517
- // Calculate scrollbar thumb size (proportional to visible/total ratio)
518
- let thumb_height = ((visible_height as f32 / total_logs as f32) * scrollbar_height as f32).max(1.0) as u16;
519
- let thumb_height = thumb_height.min(scrollbar_height);
522
+ // CRITICAL: Use integer math to prevent floating point rounding issues
523
+ // Calculate thumb size (minimum 1 char, proportional to visible/total)
524
+ let thumb_height = ((visible_height as u32 * scrollbar_height as u32) / total_logs as u32) as u16;
525
+ let thumb_height = thumb_height.max(1).min(scrollbar_height);
520
526
 
521
- // Calculate thumb position based on scroll offset
522
- let scroll_ratio = if max_scroll > 0 { state.log_scroll as f32 / max_scroll as f32 } else { 0.0 };
523
- let thumb_start = (scroll_ratio * (scrollbar_height - thumb_height) as f32) as u16;
527
+ // Calculate thumb position using integer math for stability
528
+ let thumb_start = if max_scroll > 0 {
529
+ // Use integer division to avoid floating point jitter
530
+ ((state.log_scroll as u32 * (scrollbar_height.saturating_sub(thumb_height) as u32)) / max_scroll as u32) as u16
531
+ } else {
532
+ 0
533
+ };
534
+
535
+ // Clamp thumb_start to valid range
536
+ let thumb_start = thumb_start.min(scrollbar_height.saturating_sub(thumb_height));
537
+ let thumb_end = (thumb_start + thumb_height).min(scrollbar_height);
538
+
539
+ // Render scrollbar track (entire height) as single operation to prevent flicker
540
+ // Build scrollbar string first, then render once
541
+ let mut scrollbar_chars = vec![String::from("│"); scrollbar_height as usize];
524
542
 
525
- // Draw clean scrollbar with better visibility
526
- for i in 0..scrollbar_height {
527
- let y = scrollbar_y + i;
543
+ // Fill thumb portion
544
+ for i in thumb_start..thumb_end {
545
+ if i < scrollbar_height {
546
+ scrollbar_chars[i as usize] = String::from("█");
547
+ }
548
+ }
549
+
550
+ // Render entire scrollbar at once (prevents flickering from multiple renders)
551
+ for (i, c) in scrollbar_chars.iter().enumerate() {
552
+ let y = scrollbar_y + i as u16;
528
553
  if y < scrollbar_y + scrollbar_height {
529
- let (c, color) = if i >= thumb_start && i < thumb_start + thumb_height {
530
- ("█", CYBER_CYAN) // Thumb in cyan (matches panel title)
554
+ let color = if i >= thumb_start as usize && i < thumb_end as usize {
555
+ CYBER_CYAN
531
556
  } else {
532
- ("│", TEXT_MUTED) // Track in muted gray
557
+ TEXT_MUTED
533
558
  };
534
559
  f.render_widget(
535
- Paragraph::new(c).style(Style::default().fg(color)),
560
+ Paragraph::new(c.as_str()).style(Style::default().fg(color)),
536
561
  Rect { x: scrollbar_x, y, width: 1, height: 1 }
537
562
  );
538
563
  }
@@ -598,13 +623,14 @@ fn render_command_box(f: &mut Frame, area: Rect, state: &AppState) {
598
623
  // Just use spacing for visual separation
599
624
 
600
625
  // Command prompt - Use ASCII > instead of Unicode ▶
601
- let cursor = if state.command_focused || !state.command_input.is_empty() { "_" } else { "_" };
626
+ // Note: We show a visual cursor character "_" in the text, but the real cursor
627
+ // position is handled separately below
602
628
  let prompt_line = Line::from(vec![
603
629
  Span::styled("> ", Style::default().fg(BRAND_PURPLE)), // ASCII > instead of Unicode ▶
604
630
  Span::styled("4runr", Style::default().fg(BRAND_VIOLET).add_modifier(Modifier::BOLD)),
605
631
  Span::styled(": ", Style::default().fg(TEXT_DIM)), // ASCII : instead of Unicode ›
606
632
  Span::styled(&state.command_input, Style::default().fg(TEXT_PRIMARY)),
607
- Span::styled(cursor, Style::default().fg(CYBER_CYAN)),
633
+ Span::styled("_", Style::default().fg(CYBER_CYAN)), // Visual cursor indicator
608
634
  ]);
609
635
  f.render_widget(Paragraph::new(prompt_line), Rect {
610
636
  x: bar_area.x, y: bar_area.y + 1, width: bar_area.width, height: 1
@@ -623,12 +649,26 @@ fn render_command_box(f: &mut Frame, area: Rect, state: &AppState) {
623
649
  x: bar_area.x, y: bar_area.y + 2, width: bar_area.width, height: 1
624
650
  });
625
651
 
626
- // Cursor position when focused
627
- if state.command_focused {
628
- let cursor_x = bar_area.x + 8 + state.command_input.len() as u16; // +8 for "> 4runr: "
652
+ // CRITICAL FIX: Only set cursor position when user is actively typing
653
+ // This prevents cursor from appearing in wrong places (operations log, etc.)
654
+ // IMPORTANT: In Ratatui, set_cursor() shows the cursor, so we only call it here
655
+ if state.command_focused || !state.command_input.is_empty() {
656
+ // Calculate correct cursor position: "> 4runr: " = 9 chars
657
+ let prompt_len = 9u16; // "> 4runr: " = 9 characters ("> " + "4runr" + ": " = 2+5+2 = 9)
658
+ let input_len = state.command_input.len() as u16;
659
+ let cursor_x = bar_area.x + prompt_len + input_len;
629
660
  let cursor_y = bar_area.y + 1;
630
- f.set_cursor(cursor_x.min(bar_area.x + bar_area.width - 1), cursor_y);
661
+
662
+ // Ensure cursor doesn't go beyond the safe area (respects 15% margin)
663
+ let max_x = (bar_area.x + bar_area.width).saturating_sub(1);
664
+ let final_x = cursor_x.min(max_x);
665
+
666
+ // Only set cursor if we're actually at the input field
667
+ // This prevents cursor from appearing elsewhere (operations log, etc.)
668
+ f.set_cursor(final_x, cursor_y);
631
669
  }
670
+ // IMPORTANT: If not focused and no input, we don't call set_cursor()
671
+ // This keeps the cursor hidden (hidden at start of main loop)
632
672
  }
633
673
 
634
674
  fn render_too_small(f: &mut Frame, viewport: &SafeViewport) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.1.48",
3
+ "version": "2.1.50",
4
4
  "type": "module",
5
5
  "description": "4Runr AI Agent OS - Interactive terminal for managing AI agents",
6
6
  "main": "dist/index.js",