4runr-os 2.9.47 → 2.9.48

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.
@@ -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.47";
147
+ const PACKAGE_VERSION: &str = "2.9.48";
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)),
@@ -2,7 +2,7 @@
2
2
  /// Wizard for setting up Gateway connection options
3
3
 
4
4
  use ratatui::prelude::*;
5
- use ratatui::widgets::{Block, Borders, Paragraph, Wrap, Clear, List, ListItem, ListState};
5
+ use ratatui::widgets::{Block, Borders, Paragraph, Wrap, Clear, List, ListItem};
6
6
  use crate::app::{AppState, GatewayOption};
7
7
 
8
8
  // === 4RUNR BRAND COLORS (matching layout.rs) ===
@@ -18,8 +18,13 @@ 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
- // 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);
21
+ // Per-frame render ID so we can correlate all logs from the same draw
22
+ static mut RENDER_ID: u64 = 0;
23
+ let render_id = unsafe {
24
+ RENDER_ID += 1;
25
+ RENDER_ID
26
+ };
27
+ eprintln!("[SETUP-PORTAL] R#{} START selected_option={:?}", render_id, state.setup_portal.selected_option);
23
28
 
24
29
  static mut RENDER_COUNT: u64 = 0;
25
30
  unsafe {
@@ -133,7 +138,7 @@ pub fn render(f: &mut Frame, state: &mut AppState) {
133
138
 
134
139
  render_header(f, chunks[0], state);
135
140
  render_description(f, chunks[1]);
136
- render_content(f, chunks[2], state);
141
+ render_content(f, chunks[2], state, render_id);
137
142
 
138
143
  if has_error {
139
144
  render_error_box(f, chunks[3], state);
@@ -182,127 +187,79 @@ fn render_description(f: &mut Frame, area: Rect) {
182
187
  f.render_widget(text, inner);
183
188
  }
184
189
 
185
- fn render_content(f: &mut Frame, area: Rect, state: &mut AppState) {
186
- // DEBUG: Log content area
190
+ fn render_content(f: &mut Frame, area: Rect, state: &mut AppState, render_id: u64) {
187
191
  static mut LOG_COUNT: u64 = 0;
188
192
  unsafe {
189
193
  LOG_COUNT += 1;
190
194
  if LOG_COUNT % 10 == 1 {
191
- eprintln!("[SETUP-PORTAL] render_content - area: x={}, y={}, w={}, h={}",
192
- area.x, area.y, area.width, area.height);
195
+ eprintln!("[SETUP-PORTAL] R#{} render_content area: {}x{}", render_id, area.width, area.height);
193
196
  }
194
197
  }
195
-
196
- // Split content into two columns: Options (left) and Details (right)
198
+
197
199
  let chunks = Layout::default()
198
200
  .direction(Direction::Horizontal)
199
201
  .constraints([
200
- Constraint::Percentage(40), // Options list
201
- Constraint::Percentage(60), // Details panel
202
+ Constraint::Percentage(40),
203
+ Constraint::Percentage(60),
202
204
  ])
203
205
  .split(area);
204
-
205
- // DEBUG: Log content chunks
206
- unsafe {
207
- if LOG_COUNT % 10 == 1 {
208
- eprintln!("[SETUP-PORTAL] Content chunks[0] (options): x={}, y={}, w={}, h={}",
209
- chunks[0].x, chunks[0].y, chunks[0].width, chunks[0].height);
210
- eprintln!("[SETUP-PORTAL] Content chunks[1] (details): x={}, y={}, w={}, h={}",
211
- chunks[1].x, chunks[1].y, chunks[1].width, chunks[1].height);
212
- }
213
- }
214
-
215
- render_options_list(f, chunks[0], state);
216
- render_option_details(f, chunks[1], state);
206
+
207
+ render_options_list(f, chunks[0], state, render_id);
208
+ render_option_details(f, chunks[1], state, render_id);
217
209
  }
218
210
 
219
- fn render_options_list(f: &mut Frame, area: Rect, state: &mut AppState) {
220
- // DEBUG: Log options list rendering
221
- static mut LOG_COUNT: u64 = 0;
222
- unsafe {
223
- LOG_COUNT += 1;
224
- if LOG_COUNT % 10 == 1 {
225
- eprintln!("[SETUP-PORTAL] render_options_list - area: x={}, y={}, w={}, h={}",
226
- area.x, area.y, area.width, area.height);
227
- }
228
- }
229
-
230
- // CRITICAL: Fill entire area with background so every cell is overwritten.
231
- // Without this, List only paints rows with items; leftover rows show previous frame (duplication).
211
+ fn render_options_list(f: &mut Frame, area: Rect, state: &mut AppState, render_id: u64) {
232
212
  let fill = Block::default().style(Style::default().bg(BG_PANEL));
233
213
  f.render_widget(fill, area);
234
-
214
+
235
215
  let options = vec![
236
216
  ("Local Bundle", GatewayOption::LocalBundle),
237
217
  ("4Runr Server", GatewayOption::CloudServer),
238
218
  ("Custom URL", GatewayOption::CustomUrl),
239
219
  ];
240
-
241
- // Find selected index from canonical source of truth (selected_option)
220
+
221
+ let selected_option = &state.setup_portal.selected_option;
242
222
  let selected_index = options.iter()
243
- .position(|(_, opt)| opt == &state.setup_portal.selected_option)
223
+ .position(|(_, opt)| opt == selected_option)
244
224
  .unwrap_or(0);
245
225
 
246
- // CRITICAL: Force ListState to match selected_option every frame.
247
- // This prevents drift (e.g. from widget internals or multiple render paths).
248
- state.setup_portal.list_state.select(Some(selected_index));
226
+ eprintln!("[SETUP-PORTAL] R#{} LIST selected_index={} selected_option={:?}", render_id, selected_index, state.setup_portal.selected_option);
249
227
 
250
- unsafe {
251
- if LOG_COUNT % 10 == 1 {
252
- eprintln!("[SETUP-PORTAL] Options count: {}", options.len());
253
- }
254
- }
255
-
256
228
  let items: Vec<ListItem> = options
257
229
  .iter()
258
230
  .map(|(name, option)| {
259
- let is_selected = &state.setup_portal.selected_option == option;
231
+ let is_selected = selected_option == option;
260
232
  let style = if is_selected {
261
233
  Style::default().fg(CYBER_CYAN).bold()
262
234
  } else {
263
235
  Style::default().fg(TEXT_MUTED)
264
236
  };
265
-
266
237
  let icon = match option {
267
238
  GatewayOption::LocalBundle => "[■]",
268
239
  GatewayOption::CloudServer => "[≡]",
269
240
  GatewayOption::CustomUrl => "[◆]",
270
241
  };
271
-
272
242
  let prefix = if is_selected { "► " } else { " " };
273
243
  let text = format!("{}{} {}", prefix, icon, name);
274
244
  ListItem::new(text).style(style)
275
245
  })
276
246
  .collect();
277
-
278
- // Use Block to properly contain and clear the area
247
+
279
248
  let block = Block::default()
280
249
  .borders(Borders::ALL)
281
250
  .title(" Gateway Options ")
282
251
  .border_style(Style::default().fg(CYBER_CYAN))
283
252
  .style(Style::default().bg(BG_PANEL));
284
-
285
- let list = List::new(items)
286
- .block(block)
287
- .highlight_style(Style::default().fg(CYBER_CYAN).bold().bg(BG_PANEL));
288
-
289
- // CRITICAL: Pass the mutable reference to the persisted ListState
290
- f.render_stateful_widget(list, area, &mut state.setup_portal.list_state);
253
+
254
+ // STATELESS: Render list without ListState so only our item styling (► + color) shows selection.
255
+ // This removes any second source of highlight that could drift from selected_option.
256
+ let list = List::new(items).block(block);
257
+ f.render_widget(list, area);
291
258
  }
292
259
 
293
- fn render_option_details(f: &mut Frame, area: Rect, state: &AppState) {
294
- // DEBUG: Every frame - state used for details panel
295
- eprintln!("[SETUP-PORTAL] DETAILS selected_option={:?}", state.setup_portal.selected_option);
260
+ fn render_option_details(f: &mut Frame, area: Rect, state: &AppState, render_id: u64) {
261
+ eprintln!("[SETUP-PORTAL] R#{} DETAILS selected_option={:?}", render_id, state.setup_portal.selected_option);
296
262
 
297
- static mut LOG_COUNT: u64 = 0;
298
- unsafe {
299
- LOG_COUNT += 1;
300
- if LOG_COUNT % 10 == 1 {
301
- eprintln!("[SETUP-PORTAL] render_option_details - area: x={}, y={}, w={}, h={}",
302
- area.x, area.y, area.width, area.height);
303
- }
304
- }
305
-
306
263
  // CRITICAL: Fill entire area with background so every cell is overwritten.
307
264
  // Without this, Paragraph may not paint every cell; leftover shows previous option (corruption).
308
265
  let fill = Block::default().style(Style::default().bg(BG_PANEL));
@@ -392,19 +349,9 @@ fn render_option_details(f: &mut Frame, area: Rect, state: &AppState) {
392
349
  ]
393
350
  ),
394
351
  };
395
-
396
- // DEBUG: Log what content is being rendered
397
- unsafe {
398
- if LOG_COUNT % 10 == 1 {
399
- eprintln!("[SETUP-PORTAL] Title: '{}'", title);
400
- eprintln!("[SETUP-PORTAL] Content lines: {}", content.len());
401
- for (i, line) in content.iter().take(3).enumerate() {
402
- let preview = format!("{:?}", line).chars().take(50).collect::<String>();
403
- eprintln!("[SETUP-PORTAL] content[{}]: {}", i, preview);
404
- }
405
- }
406
- }
407
-
352
+
353
+ eprintln!("[SETUP-PORTAL] R#{} DETAILS title='{}' lines={}", render_id, title.trim(), content.len());
354
+
408
355
  // Use Block to properly contain and clear the area
409
356
  let block = Block::default()
410
357
  .title(title)
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.9.47",
3
+ "version": "2.9.48",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.9.47: Setup Portal - sync ListState from selected_option every frame so highlight never drifts. v2.9.46: Persisted ListState. ⚠️ Pre-MVP / Development Phase",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.9.48: Setup Portal - stateless list (single source of truth), per-frame R# debug. v2.9.47: ListState sync. ⚠️ Pre-MVP / Development Phase",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",