4runr-os 2.9.44 → 2.9.46

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.
@@ -343,6 +343,7 @@ pub struct SetupPortalState {
343
343
  pub detecting: bool,
344
344
  pub detection_result: Option<DetectionResult>,
345
345
  pub error: Option<String>,
346
+ pub list_state: ratatui::widgets::ListState, // CRITICAL: Must persist across frames
346
347
  }
347
348
 
348
349
  #[derive(Debug, Clone)]
@@ -366,11 +367,14 @@ pub struct CloudStatus {
366
367
 
367
368
  impl Default for SetupPortalState {
368
369
  fn default() -> Self {
370
+ let mut list_state = ratatui::widgets::ListState::default();
371
+ list_state.select(Some(0)); // Start with first option selected
369
372
  Self {
370
373
  selected_option: GatewayOption::LocalBundle,
371
374
  detecting: false,
372
375
  detection_result: None,
373
376
  error: None,
377
+ list_state,
374
378
  }
375
379
  }
376
380
  }
@@ -378,6 +382,7 @@ impl Default for SetupPortalState {
378
382
  impl SetupPortalState {
379
383
  pub fn cycle_option(&mut self, direction: i8) {
380
384
  use GatewayOption::*;
385
+ let prev = self.selected_option.clone();
381
386
  self.selected_option = match (&self.selected_option, direction) {
382
387
  // Forward cycling (Down key, Scroll Down)
383
388
  (LocalBundle, 1) => CloudServer,
@@ -389,6 +394,17 @@ impl SetupPortalState {
389
394
  (CustomUrl, -1) => CloudServer,
390
395
  _ => self.selected_option.clone(),
391
396
  };
397
+
398
+ // CRITICAL: Update the persisted ListState index
399
+ let new_index = match self.selected_option {
400
+ LocalBundle => 0,
401
+ CloudServer => 1,
402
+ CustomUrl => 2,
403
+ };
404
+ self.list_state.select(Some(new_index));
405
+
406
+ eprintln!("[SETUP-PORTAL] cycle_option direction={} prev={:?} -> new={:?} list_index={}",
407
+ direction, prev, self.selected_option, new_index);
392
408
  }
393
409
 
394
410
  pub fn reset(&mut self) {
@@ -915,7 +931,7 @@ impl App {
915
931
  }
916
932
  }
917
933
 
918
- pub fn render(&self, f: &mut Frame) {
934
+ pub fn render(&mut self, f: &mut Frame) {
919
935
  // Use new navigation system - render based on current screen
920
936
  let current = self.state.navigation.current_screen();
921
937
 
@@ -968,7 +984,7 @@ impl App {
968
984
  // Render Setup Portal as standalone screen (not overlay)
969
985
  // This eliminates double-rendering and improves performance
970
986
  use crate::ui::setup_portal;
971
- setup_portal::render(f, &self.state);
987
+ setup_portal::render(f, &mut self.state);
972
988
  }
973
989
  Screen::Confirmation { message, action } => {
974
990
  // TODO: Implement confirmation popup
@@ -1799,12 +1815,13 @@ impl App {
1799
1815
  // SETUP PORTAL INPUT HANDLING
1800
1816
  // ============================================================
1801
1817
 
1802
- fn handle_setup_portal_input(&mut self, key: KeyEvent, ws_client: Option<&WebSocketClient>) -> anyhow::Result<bool> {
1818
+ fn handle_setup_portal_input(&mut self, key: KeyEvent, _ws_client: Option<&WebSocketClient>) -> anyhow::Result<bool> {
1819
+ eprintln!("[SETUP-PORTAL] INPUT key={:?} (selected_option={:?})", key.code, self.state.setup_portal.selected_option);
1803
1820
  // Guard: Don't process if already detecting
1804
1821
  if self.state.setup_portal.detecting {
1805
1822
  return Ok(false);
1806
1823
  }
1807
-
1824
+
1808
1825
  match key.code {
1809
1826
  // ESC - Cancel and close portal
1810
1827
  KeyCode::Esc => {
@@ -1824,11 +1841,13 @@ impl App {
1824
1841
 
1825
1842
  // Up/Down - Navigate options (immediate render for responsiveness)
1826
1843
  KeyCode::Up => {
1844
+ eprintln!("[SETUP-PORTAL] KEY Up -> cycle_option(-1)");
1827
1845
  self.state.setup_portal.cycle_option(-1);
1828
1846
  self.request_immediate_render("setup_nav_up");
1829
1847
  }
1830
1848
 
1831
1849
  KeyCode::Down => {
1850
+ eprintln!("[SETUP-PORTAL] KEY Down -> cycle_option(1)");
1832
1851
  self.state.setup_portal.cycle_option(1);
1833
1852
  self.request_immediate_render("setup_nav_down");
1834
1853
  }
@@ -632,12 +632,12 @@ fn main() -> Result<()> {
632
632
  if !app.state.setup_portal.detecting {
633
633
  match mouse.kind {
634
634
  MouseEventKind::ScrollUp => {
635
- // Navigate up (cycle backward)
635
+ eprintln!("[SETUP-PORTAL] MOUSE ScrollUp -> cycle_option(-1)");
636
636
  app.state.setup_portal.cycle_option(-1);
637
637
  app.request_immediate_render("setup_portal_scroll_up");
638
638
  }
639
639
  MouseEventKind::ScrollDown => {
640
- // Navigate down (cycle forward)
640
+ eprintln!("[SETUP-PORTAL] MOUSE ScrollDown -> cycle_option(1)");
641
641
  app.state.setup_portal.cycle_option(1);
642
642
  app.request_immediate_render("setup_portal_scroll_down");
643
643
  }
@@ -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.44";
147
+ const PACKAGE_VERSION: &str = "2.9.46";
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)),
@@ -17,19 +17,20 @@ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
17
17
  const ERROR_RED: Color = Color::Rgb(255, 69, 69);
18
18
 
19
19
  /// Render the Setup Portal screen - Full-screen standalone portal
20
- pub fn render(f: &mut Frame, state: &AppState) {
21
- // DEBUG: Track render calls
20
+ pub fn render(f: &mut Frame, state: &mut AppState) {
21
+ // DEBUG: Every frame log the selection state used for this draw (no throttle)
22
+ eprintln!("[SETUP-PORTAL] FRAME selected_option={:?}", state.setup_portal.selected_option);
23
+
22
24
  static mut RENDER_COUNT: u64 = 0;
23
25
  unsafe {
24
26
  RENDER_COUNT += 1;
25
- if RENDER_COUNT % 10 == 1 { // Log every 10th render to avoid spam
27
+ if RENDER_COUNT % 10 == 1 {
26
28
  eprintln!("[SETUP-PORTAL] Render #{}", RENDER_COUNT);
27
29
  }
28
30
  }
29
-
31
+
30
32
  let area = f.size();
31
-
32
- // DEBUG: Log terminal size
33
+
33
34
  unsafe {
34
35
  if RENDER_COUNT % 10 == 1 {
35
36
  eprintln!("[SETUP-PORTAL] Terminal size: {}x{}", area.width, area.height);
@@ -181,7 +182,7 @@ fn render_description(f: &mut Frame, area: Rect) {
181
182
  f.render_widget(text, inner);
182
183
  }
183
184
 
184
- fn render_content(f: &mut Frame, area: Rect, state: &AppState) {
185
+ fn render_content(f: &mut Frame, area: Rect, state: &mut AppState) {
185
186
  // DEBUG: Log content area
186
187
  static mut LOG_COUNT: u64 = 0;
187
188
  unsafe {
@@ -215,7 +216,7 @@ fn render_content(f: &mut Frame, area: Rect, state: &AppState) {
215
216
  render_option_details(f, chunks[1], state);
216
217
  }
217
218
 
218
- fn render_options_list(f: &mut Frame, area: Rect, state: &AppState) {
219
+ fn render_options_list(f: &mut Frame, area: Rect, state: &mut AppState) {
219
220
  // DEBUG: Log options list rendering
220
221
  static mut LOG_COUNT: u64 = 0;
221
222
  unsafe {
@@ -241,12 +242,12 @@ fn render_options_list(f: &mut Frame, area: Rect, state: &AppState) {
241
242
  let selected_index = options.iter()
242
243
  .position(|(_, opt)| opt == &state.setup_portal.selected_option)
243
244
  .unwrap_or(0);
244
-
245
- // DEBUG: Log selected index
245
+
246
+ // DEBUG: Every frame - exact state used for list (so we can match what's drawn)
247
+ eprintln!("[SETUP-PORTAL] LIST selected_index={} selected_option={:?}", selected_index, state.setup_portal.selected_option);
248
+
246
249
  unsafe {
247
250
  if LOG_COUNT % 10 == 1 {
248
- eprintln!("[SETUP-PORTAL] Selected index: {}, option: {:?}",
249
- selected_index, state.setup_portal.selected_option);
250
251
  eprintln!("[SETUP-PORTAL] Options count: {}", options.len());
251
252
  }
252
253
  }
@@ -273,9 +274,11 @@ fn render_options_list(f: &mut Frame, area: Rect, state: &AppState) {
273
274
  })
274
275
  .collect();
275
276
 
276
- // Create ListState with selected index
277
- let mut list_state = ListState::default();
278
- list_state.select(Some(selected_index));
277
+ // CRITICAL: Use the PERSISTED ListState from app state (not a new one each frame)
278
+ // This is required for Ratatui's stateful widget to maintain selection across frames
279
+ let list_state_selected = state.setup_portal.list_state.selected();
280
+ eprintln!("[SETUP-PORTAL] LIST About to render: list_state.selected()={:?} selected_index={} selected_option={:?}",
281
+ list_state_selected, selected_index, state.setup_portal.selected_option);
279
282
 
280
283
  // Use Block to properly contain and clear the area
281
284
  let block = Block::default()
@@ -288,18 +291,20 @@ fn render_options_list(f: &mut Frame, area: Rect, state: &AppState) {
288
291
  .block(block)
289
292
  .highlight_style(Style::default().fg(CYBER_CYAN).bold().bg(BG_PANEL));
290
293
 
291
- f.render_stateful_widget(list, area, &mut list_state);
294
+ // CRITICAL: Pass the mutable reference to the persisted ListState
295
+ f.render_stateful_widget(list, area, &mut state.setup_portal.list_state);
292
296
  }
293
297
 
294
298
  fn render_option_details(f: &mut Frame, area: Rect, state: &AppState) {
295
- // DEBUG: Log details rendering
299
+ // DEBUG: Every frame - state used for details panel
300
+ eprintln!("[SETUP-PORTAL] DETAILS selected_option={:?}", state.setup_portal.selected_option);
301
+
296
302
  static mut LOG_COUNT: u64 = 0;
297
303
  unsafe {
298
304
  LOG_COUNT += 1;
299
305
  if LOG_COUNT % 10 == 1 {
300
- eprintln!("[SETUP-PORTAL] render_option_details - area: x={}, y={}, w={}, h={}",
306
+ eprintln!("[SETUP-PORTAL] render_option_details - area: x={}, y={}, w={}, h={}",
301
307
  area.x, area.y, area.width, area.height);
302
- eprintln!("[SETUP-PORTAL] Selected option: {:?}", state.setup_portal.selected_option);
303
308
  }
304
309
  }
305
310
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.9.44",
3
+ "version": "2.9.46",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.9.44: Setup Portal navigation fix - fill background before List/details to fix duplication and character corruption. v2.9.43: Release prep. ⚠️ Pre-MVP / Development Phase",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.9.46: Setup Portal navigation fix - persisted ListState so selection matches visual. v2.9.45: Debug logging. v2.9.44: Fill-background fix. ⚠️ Pre-MVP / Development Phase",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",