4runr-os 2.9.50 → 2.9.52

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.
@@ -344,8 +344,6 @@ pub struct SetupPortalState {
344
344
  pub detection_result: Option<DetectionResult>,
345
345
  pub error: Option<String>,
346
346
  pub list_state: ratatui::widgets::ListState,
347
- /// Last option we drew; clear terminal only when this differs from selected_option (reduces flicker).
348
- pub last_rendered_option: Option<GatewayOption>,
349
347
  }
350
348
 
351
349
  #[derive(Debug, Clone)]
@@ -377,7 +375,6 @@ impl Default for SetupPortalState {
377
375
  detection_result: None,
378
376
  error: None,
379
377
  list_state,
380
- last_rendered_option: None,
381
378
  }
382
379
  }
383
380
  }
@@ -385,7 +382,6 @@ impl Default for SetupPortalState {
385
382
  impl SetupPortalState {
386
383
  pub fn cycle_option(&mut self, direction: i8) {
387
384
  use GatewayOption::*;
388
- let prev = self.selected_option.clone();
389
385
  self.selected_option = match (&self.selected_option, direction) {
390
386
  // Forward cycling (Down key, Scroll Down)
391
387
  (LocalBundle, 1) => CloudServer,
@@ -405,9 +401,6 @@ impl SetupPortalState {
405
401
  CustomUrl => 2,
406
402
  };
407
403
  self.list_state.select(Some(new_index));
408
-
409
- eprintln!("[SETUP-PORTAL] cycle_option direction={} prev={:?} -> new={:?} list_index={}",
410
- direction, prev, self.selected_option, new_index);
411
404
  }
412
405
 
413
406
  pub fn reset(&mut self) {
@@ -1819,7 +1812,6 @@ impl App {
1819
1812
  // ============================================================
1820
1813
 
1821
1814
  fn handle_setup_portal_input(&mut self, key: KeyEvent, _ws_client: Option<&WebSocketClient>) -> anyhow::Result<bool> {
1822
- eprintln!("[SETUP-PORTAL] INPUT key={:?} (selected_option={:?})", key.code, self.state.setup_portal.selected_option);
1823
1815
  // Guard: Don't process if already detecting
1824
1816
  if self.state.setup_portal.detecting {
1825
1817
  return Ok(false);
@@ -1844,13 +1836,11 @@ impl App {
1844
1836
 
1845
1837
  // Up/Down - Navigate options (immediate render for responsiveness)
1846
1838
  KeyCode::Up => {
1847
- eprintln!("[SETUP-PORTAL] KEY Up -> cycle_option(-1)");
1848
1839
  self.state.setup_portal.cycle_option(-1);
1849
1840
  self.request_immediate_render("setup_nav_up");
1850
1841
  }
1851
1842
 
1852
1843
  KeyCode::Down => {
1853
- eprintln!("[SETUP-PORTAL] KEY Down -> cycle_option(1)");
1854
1844
  self.state.setup_portal.cycle_option(1);
1855
1845
  self.request_immediate_render("setup_nav_down");
1856
1846
  }
@@ -591,23 +591,13 @@ fn main() -> Result<()> {
591
591
  let current_screen = app.state.navigation.current_screen().clone();
592
592
  let is_portal_active = matches!(current_screen, crate::screens::Screen::ConnectionPortal | crate::screens::Screen::SetupPortal);
593
593
 
594
- // Clear on screen switch, or on Setup Portal only when selection just changed
595
- // (avoids flicker from clearing every frame while still fixing list/details mismatch)
594
+ // Clear only on screen switch (no clear on Setup Portal nav = no flicker)
596
595
  let is_switching = match (&previous_screen, &current_screen) {
597
596
  (Some(prev), current) => prev != current,
598
597
  (None, _) => true,
599
598
  };
600
599
 
601
- let setup_selection_changed = match &current_screen {
602
- crate::screens::Screen::SetupPortal => {
603
- let last = app.state.setup_portal.last_rendered_option.as_ref();
604
- let current = &app.state.setup_portal.selected_option;
605
- last != Some(current)
606
- }
607
- _ => false,
608
- };
609
-
610
- if is_switching || setup_selection_changed {
600
+ if is_switching {
611
601
  terminal.clear()?;
612
602
  }
613
603
 
@@ -640,12 +630,10 @@ fn main() -> Result<()> {
640
630
  if !app.state.setup_portal.detecting {
641
631
  match mouse.kind {
642
632
  MouseEventKind::ScrollUp => {
643
- eprintln!("[SETUP-PORTAL] MOUSE ScrollUp -> cycle_option(-1)");
644
633
  app.state.setup_portal.cycle_option(-1);
645
634
  app.request_immediate_render("setup_portal_scroll_up");
646
635
  }
647
636
  MouseEventKind::ScrollDown => {
648
- eprintln!("[SETUP-PORTAL] MOUSE ScrollDown -> cycle_option(1)");
649
637
  app.state.setup_portal.cycle_option(1);
650
638
  app.request_immediate_render("setup_portal_scroll_down");
651
639
  }
@@ -144,7 +144,7 @@ fn render_header(f: &mut Frame, area: Rect, state: &AppState) {
144
144
 
145
145
  // Line 1: Brand + version + mode + uptime - Bug 3 fix: Use "4Runr." with dot (matches brand logo)
146
146
  // Use npm package version (2.9.24) - matches package.json
147
- const PACKAGE_VERSION: &str = "2.9.50";
147
+ const PACKAGE_VERSION: &str = "2.9.52";
148
148
  let brand_line = Line::from(vec![
149
149
  Span::styled("4Runr.", Style::default().fg(BRAND_PURPLE).add_modifier(Modifier::BOLD)),
150
150
  Span::styled(" AI AGENT OS", Style::default().fg(BRAND_VIOLET)),
@@ -18,40 +18,22 @@ const ERROR_RED: Color = Color::Rgb(255, 69, 69);
18
18
 
19
19
  /// Render the Setup Portal screen - Full-screen standalone portal
20
20
  pub fn render(f: &mut Frame, state: &mut AppState) {
21
- // Per-frame render ID so we can correlate all logs from the same draw
21
+ // Per-frame render ID (used for passing to content/details; debug logs removed to avoid terminal spam)
22
22
  static mut RENDER_ID: u64 = 0;
23
23
  let render_id = unsafe {
24
24
  RENDER_ID += 1;
25
25
  RENDER_ID
26
26
  };
27
- eprintln!("[SETUP-PORTAL] R#{} START selected_option={:?}", render_id, state.setup_portal.selected_option);
28
-
29
- static mut RENDER_COUNT: u64 = 0;
30
- unsafe {
31
- RENDER_COUNT += 1;
32
- if RENDER_COUNT % 10 == 1 {
33
- eprintln!("[SETUP-PORTAL] Render #{}", RENDER_COUNT);
34
- }
35
- }
36
27
 
37
28
  let area = f.size();
38
29
 
39
- unsafe {
40
- if RENDER_COUNT % 10 == 1 {
41
- eprintln!("[SETUP-PORTAL] Terminal size: {}x{}", area.width, area.height);
42
- }
43
- }
44
-
45
30
  // CRITICAL: Validate terminal size before rendering
46
31
  // This prevents character corruption when terminal size is wrong on initial render
47
32
  if area.width == 0 || area.height == 0 {
48
- eprintln!("[SETUP-PORTAL] ⚠️ Terminal size invalid: {}x{} - skipping render", area.width, area.height);
49
33
  return;
50
34
  }
51
-
52
- // Ensure minimum size for portal (85x25)
35
+
53
36
  if area.width < 85 || area.height < 25 {
54
- eprintln!("[SETUP-PORTAL] ⚠️ Terminal too small: {}x{} (required: 85x25)", area.width, area.height);
55
37
  render_too_small(f, area);
56
38
  return;
57
39
  }
@@ -76,15 +58,7 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
76
58
  width: portal_width,
77
59
  height: portal_height,
78
60
  };
79
-
80
- // DEBUG: Log portal area calculations
81
- unsafe {
82
- if RENDER_COUNT % 10 == 1 {
83
- eprintln!("[SETUP-PORTAL] Portal area: x={}, y={}, w={}, h={}",
84
- portal_area.x, portal_area.y, portal_area.width, portal_area.height);
85
- }
86
- }
87
-
61
+
88
62
  // Split portal into sections
89
63
  use ratatui::layout::{Constraint, Direction, Layout};
90
64
 
@@ -111,31 +85,7 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
111
85
  .direction(Direction::Vertical)
112
86
  .constraints(constraints)
113
87
  .split(portal_area);
114
-
115
- // DEBUG: Log layout chunks and selected option
116
- unsafe {
117
- if RENDER_COUNT % 10 == 1 {
118
- eprintln!("[SETUP-PORTAL] Selected option: {:?}", state.setup_portal.selected_option);
119
- eprintln!("[SETUP-PORTAL] Has error: {}", has_error);
120
- eprintln!("[SETUP-PORTAL] Chunks count: {}", chunks.len());
121
- for (i, chunk) in chunks.iter().enumerate() {
122
- eprintln!("[SETUP-PORTAL] chunks[{}]: x={}, y={}, w={}, h={}",
123
- i, chunk.x, chunk.y, chunk.width, chunk.height);
124
- }
125
- // Check for overlaps
126
- for i in 0..chunks.len() {
127
- for j in (i+1)..chunks.len() {
128
- let a = &chunks[i];
129
- let b = &chunks[j];
130
- if a.x < b.x + b.width && a.x + a.width > b.x &&
131
- a.y < b.y + b.height && a.y + a.height > b.y {
132
- eprintln!("[SETUP-PORTAL] ⚠️ OVERLAP: chunks[{}] overlaps chunks[{}]!", i, j);
133
- }
134
- }
135
- }
136
- }
137
- }
138
-
88
+
139
89
  render_header(f, chunks[0], state);
140
90
  render_description(f, chunks[1]);
141
91
  render_content(f, chunks[2], state, render_id);
@@ -146,9 +96,6 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
146
96
  } else {
147
97
  render_actions(f, chunks[3], state);
148
98
  }
149
-
150
- // So main loop only clears terminal when selection actually changed (reduces flicker).
151
- state.setup_portal.last_rendered_option = Some(state.setup_portal.selected_option.clone());
152
99
  }
153
100
 
154
101
  fn render_header(f: &mut Frame, area: Rect, state: &AppState) {
@@ -191,14 +138,6 @@ fn render_description(f: &mut Frame, area: Rect) {
191
138
  }
192
139
 
193
140
  fn render_content(f: &mut Frame, area: Rect, state: &mut AppState, render_id: u64) {
194
- static mut LOG_COUNT: u64 = 0;
195
- unsafe {
196
- LOG_COUNT += 1;
197
- if LOG_COUNT % 10 == 1 {
198
- eprintln!("[SETUP-PORTAL] R#{} render_content area: {}x{}", render_id, area.width, area.height);
199
- }
200
- }
201
-
202
141
  let chunks = Layout::default()
203
142
  .direction(Direction::Horizontal)
204
143
  .constraints([
@@ -211,10 +150,7 @@ fn render_content(f: &mut Frame, area: Rect, state: &mut AppState, render_id: u6
211
150
  render_option_details(f, chunks[1], state, render_id);
212
151
  }
213
152
 
214
- fn render_options_list(f: &mut Frame, area: Rect, state: &mut AppState, render_id: u64) {
215
- let fill = Block::default().style(Style::default().bg(BG_PANEL));
216
- f.render_widget(fill, area);
217
-
153
+ fn render_options_list(f: &mut Frame, area: Rect, state: &mut AppState, _render_id: u64) {
218
154
  let options = vec![
219
155
  ("Local Bundle", GatewayOption::LocalBundle),
220
156
  ("4Runr Server", GatewayOption::CloudServer),
@@ -222,12 +158,10 @@ fn render_options_list(f: &mut Frame, area: Rect, state: &mut AppState, render_i
222
158
  ];
223
159
 
224
160
  let selected_option = &state.setup_portal.selected_option;
225
- let selected_index = options.iter()
161
+ let _selected_index = options.iter()
226
162
  .position(|(_, opt)| opt == selected_option)
227
163
  .unwrap_or(0);
228
164
 
229
- eprintln!("[SETUP-PORTAL] R#{} LIST selected_index={} selected_option={:?}", render_id, selected_index, state.setup_portal.selected_option);
230
-
231
165
  let items: Vec<ListItem> = options
232
166
  .iter()
233
167
  .map(|(name, option)| {
@@ -248,26 +182,26 @@ fn render_options_list(f: &mut Frame, area: Rect, state: &mut AppState, render_i
248
182
  })
249
183
  .collect();
250
184
 
185
+ // CRITICAL: Render block first, then fill inner, then list to ensure full paint
251
186
  let block = Block::default()
252
187
  .borders(Borders::ALL)
253
188
  .title(" Gateway Options ")
254
189
  .border_style(Style::default().fg(CYBER_CYAN))
255
190
  .style(Style::default().bg(BG_PANEL));
256
191
 
257
- // STATELESS: Render list without ListState so only our item styling (► + color) shows selection.
258
- // This removes any second source of highlight that could drift from selected_option.
259
- let list = List::new(items).block(block);
260
- f.render_widget(list, area);
261
- }
192
+ let inner = block.inner(area);
193
+ f.render_widget(block, area);
194
+
195
+ // Fill inner area
196
+ let fill = Block::default().style(Style::default().bg(BG_PANEL));
197
+ f.render_widget(fill, inner);
262
198
 
263
- fn render_option_details(f: &mut Frame, area: Rect, state: &AppState, render_id: u64) {
264
- eprintln!("[SETUP-PORTAL] R#{} DETAILS selected_option={:?}", render_id, state.setup_portal.selected_option);
199
+ // STATELESS: Render list without ListState so only our item styling (► + color) shows selection
200
+ let list = List::new(items).style(Style::default().bg(BG_PANEL));
201
+ f.render_widget(list, inner);
202
+ }
265
203
 
266
- // CRITICAL: Fill entire area with background so every cell is overwritten.
267
- // Without this, Paragraph may not paint every cell; leftover shows previous option (corruption).
268
- let fill = Block::default().style(Style::default().bg(BG_PANEL));
269
- f.render_widget(fill, area);
270
-
204
+ fn render_option_details(f: &mut Frame, area: Rect, state: &AppState, _render_id: u64) {
271
205
  let selected_option = &state.setup_portal.selected_option;
272
206
  let detection_result = &state.setup_portal.detection_result;
273
207
 
@@ -353,31 +287,33 @@ fn render_option_details(f: &mut Frame, area: Rect, state: &AppState, render_id:
353
287
  ),
354
288
  };
355
289
 
356
- eprintln!("[SETUP-PORTAL] R#{} DETAILS title='{}' lines={}", render_id, title.trim(), content.len());
357
-
358
- // Use Block to properly contain and clear the area
290
+ // CRITICAL: Render Block first (clears its area), then Paragraph separately to force full paint
359
291
  let block = Block::default()
360
292
  .title(title)
361
293
  .borders(Borders::ALL)
362
294
  .border_style(Style::default().fg(TEXT_DIM))
363
295
  .style(Style::default().bg(BG_PANEL));
364
296
 
365
- // CRITICAL: Use the standard Ratatui pattern - Paragraph with .block()
366
- // This is more reliable than rendering Block and Paragraph separately
297
+ let inner = block.inner(area);
298
+ f.render_widget(block, area);
299
+
300
+ // Fill the inner area with background explicitly
301
+ let fill = Block::default().style(Style::default().bg(BG_PANEL));
302
+ f.render_widget(fill, inner);
303
+
304
+ // Now render paragraph (content only, no block)
367
305
  let paragraph = Paragraph::new(content)
368
- .block(block)
369
306
  .wrap(Wrap { trim: true })
370
- .style(Style::default().bg(BG_PANEL)); // Ensure background fills the block
307
+ .style(Style::default().bg(BG_PANEL));
371
308
 
372
309
  // Validate area before rendering
373
310
  let terminal_size = f.size();
374
- if area.width == 0 || area.height == 0 ||
375
- area.x >= terminal_size.width || area.y >= terminal_size.height {
376
- eprintln!("[SETUP-PORTAL] ⚠️ ERROR: Invalid area, skipping render");
311
+ if inner.width == 0 || inner.height == 0 ||
312
+ inner.x >= terminal_size.width || inner.y >= terminal_size.height {
377
313
  return;
378
314
  }
379
315
 
380
- f.render_widget(paragraph, area);
316
+ f.render_widget(paragraph, inner);
381
317
  }
382
318
 
383
319
  fn render_error_box(f: &mut Frame, area: Rect, state: &AppState) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.9.50",
3
+ "version": "2.9.52",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.9.50: Setup Portal - clear only when selection changes (fixes flicker, keeps list/details in sync). v2.9.49: Per-frame clear. ⚠️ Pre-MVP / Development Phase",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.9.52: Setup Portal - block+fill+widget render (no flicker, no corruption). v2.9.51: No clear on nav, removed debug logs. ⚠️ Pre-MVP / Development Phase",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",