buildwithnexus 0.10.3 → 0.10.4

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.
@@ -15,10 +15,13 @@ use crossterm::event::{
15
15
  poll, read, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
16
16
  Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind,
17
17
  };
18
- use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
18
+ use crossterm::terminal::{
19
+ disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen,
20
+ };
19
21
  use crossterm::{execute, queue};
20
22
 
21
23
  static RAW: AtomicBool = AtomicBool::new(false);
24
+ static ALT_SCREEN: AtomicBool = AtomicBool::new(false);
22
25
  // Set by poll_typeahead() when it absorbs a Ctrl+C so interrupted() still fires.
23
26
  static TYPEAHEAD_INTERRUPTED: AtomicBool = AtomicBool::new(false);
24
27
 
@@ -358,13 +361,14 @@ pub fn poll_typeahead() {
358
361
  any = true;
359
362
  }
360
363
  }
361
- // Show a dim preview of buffered input only when new keystrokes arrived.
364
+ // Show queued input in the persistent composer, not in scrollback.
362
365
  if any {
363
366
  if let Ok(ta) = typeahead().lock() {
364
367
  if !ta.buf.is_empty() {
365
- let before: String = ta.buf[..ta.cursor].iter().collect();
366
- let after: String = ta.buf[ta.cursor..].iter().collect();
367
- line(&dim(&format!(" ⌨ {before}{after}")));
368
+ let mut scroll = 0usize;
369
+ render_composer(&format!("{} {} ", dim("queued"), accent("›")), &ta.buf, ta.cursor, &mut scroll);
370
+ } else {
371
+ clear_composer();
368
372
  }
369
373
  }
370
374
  }
@@ -381,6 +385,89 @@ fn take_typeahead() -> (Vec<char>, usize) {
381
385
  }
382
386
  }
383
387
 
388
+ fn term_size() -> (u16, u16) {
389
+ crossterm::terminal::size().unwrap_or((80, 24))
390
+ }
391
+
392
+ fn composer_row() -> u16 {
393
+ term_size().1.saturating_sub(1)
394
+ }
395
+
396
+ fn strip_ansi(s: &str) -> String {
397
+ let mut out = String::new();
398
+ let mut chars = s.chars().peekable();
399
+ while let Some(c) = chars.next() {
400
+ if c == '\x1b' {
401
+ for d in chars.by_ref() {
402
+ if d.is_ascii_alphabetic() {
403
+ break;
404
+ }
405
+ }
406
+ } else {
407
+ out.push(c);
408
+ }
409
+ }
410
+ out
411
+ }
412
+
413
+ fn prompt_width(prompt: &str) -> u16 {
414
+ strip_ansi(prompt).chars().count().min(u16::MAX as usize) as u16
415
+ }
416
+
417
+ fn set_output_region() {
418
+ if !ALT_SCREEN.load(Ordering::Relaxed) {
419
+ return;
420
+ }
421
+ let (_, h) = term_size();
422
+ let bottom = h.saturating_sub(1).max(1);
423
+ print!("\x1b[1;{bottom}r\x1b[1;1H");
424
+ flush();
425
+ }
426
+
427
+ fn reset_output_region() {
428
+ if ALT_SCREEN.load(Ordering::Relaxed) {
429
+ print!("\x1b[r");
430
+ flush();
431
+ }
432
+ }
433
+
434
+ fn clear_composer() {
435
+ if !ALT_SCREEN.load(Ordering::Relaxed) {
436
+ return;
437
+ }
438
+ let mut out = io::stdout();
439
+ let _ = queue!(out, MoveTo(0, composer_row()), Clear(ClearType::CurrentLine));
440
+ let _ = out.flush();
441
+ }
442
+
443
+ fn render_composer(prompt: &str, buf: &[char], cursor: usize, scroll: &mut usize) {
444
+ if !ALT_SCREEN.load(Ordering::Relaxed) {
445
+ return;
446
+ }
447
+ let (width, _) = term_size();
448
+ let pwidth = prompt_width(prompt);
449
+ let avail = width.saturating_sub(pwidth).max(8) as usize;
450
+ let (s, col) = viewport(cursor, avail, *scroll);
451
+ *scroll = s;
452
+ let end = (s + avail).min(buf.len());
453
+ let shown: String = buf[s..end].iter().collect();
454
+ let mut out = io::stdout();
455
+ let _ = queue!(out, MoveTo(0, composer_row()), Clear(ClearType::CurrentLine));
456
+ let _ = write!(out, "{prompt}{shown}");
457
+ let _ = queue!(out, MoveTo(pwidth.saturating_add(col as u16), composer_row()));
458
+ let _ = out.flush();
459
+ }
460
+
461
+ fn echo_submitted(prompt: &str, text: &str) {
462
+ if ALT_SCREEN.load(Ordering::Relaxed) {
463
+ clear_composer();
464
+ line(&format!("{prompt}{text}"));
465
+ } else {
466
+ print!("\r\n");
467
+ flush();
468
+ }
469
+ }
470
+
384
471
  // ── startup banner ───────────────────────────────────────────────────────────
385
472
  // Gradient wordmark: each letter of "buildwithnexus" shifts across purple→cyan→green.
386
473
  fn wordmark() -> String {
@@ -464,31 +551,51 @@ pub fn context_meter(used: usize, total: usize) {
464
551
  ));
465
552
  }
466
553
 
467
- // Enter raw mode (and capture panics to restore the terminal even on crash).
468
- // We render inline on the primary screen so scrollback and text selection work.
554
+ // Enter the alternate screen and raw mode (and capture panics to restore the
555
+ // terminal even on crash). The bottom row is reserved for the composer; output
556
+ // scrolls in the region above it.
469
557
  pub fn enter_alt(raw: bool) {
558
+ if raw {
559
+ let mut out = io::stdout();
560
+ let _ = execute!(out, EnterAlternateScreen, Clear(ClearType::All), MoveTo(0, 0));
561
+ ALT_SCREEN.store(true, Ordering::Relaxed);
562
+ set_output_region();
563
+ }
470
564
  if raw && enable_raw_mode().is_ok() {
471
565
  RAW.store(true, Ordering::Relaxed);
472
566
  let _ = execute!(io::stdout(), EnableBracketedPaste, EnableMouseCapture);
473
567
  }
474
568
  let prev = std::panic::take_hook();
475
569
  std::panic::set_hook(Box::new(move |info| {
476
- let _ = execute!(io::stdout(), DisableBracketedPaste, DisableMouseCapture);
570
+ reset_output_region();
571
+ let _ = execute!(io::stdout(), DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen);
572
+ ALT_SCREEN.store(false, Ordering::Relaxed);
477
573
  let _ = disable_raw_mode();
478
574
  prev(info);
479
575
  }));
480
576
  }
481
577
 
482
578
  pub fn leave_alt() {
579
+ clear_composer();
580
+ reset_output_region();
483
581
  if RAW.swap(false, Ordering::Relaxed) {
484
582
  let _ = execute!(io::stdout(), DisableBracketedPaste, DisableMouseCapture);
485
583
  let _ = disable_raw_mode();
486
584
  }
585
+ if ALT_SCREEN.swap(false, Ordering::Relaxed) {
586
+ let _ = execute!(io::stdout(), LeaveAlternateScreen);
587
+ }
487
588
  }
488
589
 
489
590
  pub fn clear() {
490
- print!("\x1b[2J\x1b[H");
491
- flush();
591
+ if ALT_SCREEN.load(Ordering::Relaxed) {
592
+ let _ = execute!(io::stdout(), Clear(ClearType::All), MoveTo(0, 0));
593
+ set_output_region();
594
+ clear_composer();
595
+ } else {
596
+ print!("\x1b[2J\x1b[H");
597
+ flush();
598
+ }
492
599
  }
493
600
 
494
601
  pub fn line(s: &str) {
@@ -644,7 +751,11 @@ fn viewport(cursor: usize, avail: usize, scroll: usize) -> (usize, usize) {
644
751
  (s, cursor - s)
645
752
  }
646
753
 
647
- fn redraw(start: (u16, u16), buf: &[char], cursor: usize, scroll: &mut usize) {
754
+ fn redraw(prompt: &str, start: (u16, u16), buf: &[char], cursor: usize, scroll: &mut usize) {
755
+ if ALT_SCREEN.load(Ordering::Relaxed) {
756
+ render_composer(prompt, buf, cursor, scroll);
757
+ return;
758
+ }
648
759
  let width = crossterm::terminal::size().map(|(w, _)| w).unwrap_or(80);
649
760
  let avail = width.saturating_sub(start.0).max(8) as usize;
650
761
  let (s, col) = viewport(cursor, avail, *scroll);
@@ -695,10 +806,11 @@ fn edit_in_editor(current: &str) -> Option<String> {
695
806
  // Slash commands the REPL handles directly. Kept in sync with the match in lib.rs.
696
807
  const SLASH_COMMANDS_BASE: &[&str] = &[
697
808
  "/help", "/clear", "/new", "/resume", "/init",
809
+ "/plan", "/build", "/brainstorm", "/doctor", "/debug",
698
810
  "/mode", "/model", "/permissions", "/compact",
699
- "/review", "/commit", "/pr",
700
- "/schedule", "/loop", "/workflows", "/btw",
701
- "/config", "/memory", "/skills", "/exit", "/quit",
811
+ "/review", "/commit", "/pr", "/diff", "/context",
812
+ "/schedule", "/loop", "/workflows", "/tasks", "/btw",
813
+ "/config", "/memory", "/skills", "/agents", "/checkpoints", "/undo", "/rewind", "/exit", "/quit",
702
814
  ];
703
815
 
704
816
  fn load_slash_commands() -> Vec<String> {
@@ -805,24 +917,32 @@ fn read_line_raw(prompt: &str) -> Option<RawLine> {
805
917
  }
806
918
 
807
919
  fn read_line_raw_prefill(prompt: &str, prefill: Vec<char>, prefill_cur: usize) -> Option<RawLine> {
808
- print!("{prompt}");
809
- flush();
810
- let mut start = crossterm::cursor::position().unwrap_or((0, 0));
920
+ if !ALT_SCREEN.load(Ordering::Relaxed) {
921
+ print!("{prompt}");
922
+ flush();
923
+ }
924
+ let mut start = if ALT_SCREEN.load(Ordering::Relaxed) {
925
+ (prompt_width(prompt), composer_row())
926
+ } else {
927
+ crossterm::cursor::position().unwrap_or((0, 0))
928
+ };
811
929
  let mut buf: Vec<char> = prefill;
812
930
  let mut cursor = prefill_cur.min(buf.len());
813
931
  let mut scroll = 0usize;
814
- if !buf.is_empty() {
815
- redraw(start, &buf, cursor, &mut scroll);
816
- }
932
+ redraw(prompt, start, &buf, cursor, &mut scroll);
817
933
  let mut hist_idx: Option<usize> = None;
818
934
  let mut kill = String::new();
819
935
 
820
936
  macro_rules! reline {
821
937
  () => {{
822
- print!("\r{prompt}");
823
- flush();
824
- start = crossterm::cursor::position().unwrap_or(start);
825
- redraw(start, &buf, cursor, &mut scroll);
938
+ if ALT_SCREEN.load(Ordering::Relaxed) {
939
+ start = (prompt_width(prompt), composer_row());
940
+ } else {
941
+ print!("\r{prompt}");
942
+ flush();
943
+ start = crossterm::cursor::position().unwrap_or(start);
944
+ }
945
+ redraw(prompt, start, &buf, cursor, &mut scroll);
826
946
  }};
827
947
  }
828
948
 
@@ -839,7 +959,7 @@ fn read_line_raw_prefill(prompt: &str, prefill: Vec<char>, prefill_cur: usize) -
839
959
  buf.insert(cursor, c);
840
960
  cursor += 1;
841
961
  }
842
- redraw(start, &buf, cursor, &mut scroll);
962
+ redraw(prompt, start, &buf, cursor, &mut scroll);
843
963
  continue;
844
964
  }
845
965
  Ok(Event::Mouse(m)) => {
@@ -849,7 +969,7 @@ fn read_line_raw_prefill(prompt: &str, prefill: Vec<char>, prefill_cur: usize) -
849
969
  if col >= start.0 as usize {
850
970
  let offset = col - start.0 as usize + scroll;
851
971
  cursor = offset.min(buf.len());
852
- redraw(start, &buf, cursor, &mut scroll);
972
+ redraw(prompt, start, &buf, cursor, &mut scroll);
853
973
  }
854
974
  }
855
975
  continue;
@@ -863,50 +983,56 @@ fn read_line_raw_prefill(prompt: &str, prefill: Vec<char>, prefill_cur: usize) -
863
983
  // Shift+Tab → cycle mode (clear the line and signal the REPL).
864
984
  KeyCode::BackTab => {
865
985
  buf.clear();
866
- print!("\r\n");
986
+ clear_composer();
987
+ flush();
988
+ return Some(RawLine::CycleMode);
989
+ }
990
+ KeyCode::Tab if ev.modifiers.contains(KeyModifiers::SHIFT) => {
991
+ buf.clear();
992
+ clear_composer();
867
993
  flush();
868
994
  return Some(RawLine::CycleMode);
869
995
  }
870
996
  KeyCode::Char('c') if ctrl => {
871
997
  if buf.is_empty() {
872
- print!("\r\n");
998
+ clear_composer();
873
999
  flush();
874
1000
  return None;
875
1001
  }
876
1002
  buf.clear();
877
1003
  cursor = 0;
878
- redraw(start, &buf, cursor, &mut scroll);
1004
+ redraw(prompt, start, &buf, cursor, &mut scroll);
879
1005
  }
880
1006
  KeyCode::Char('d') if ctrl => {
881
1007
  if buf.is_empty() {
882
- print!("\r\n");
1008
+ clear_composer();
883
1009
  flush();
884
1010
  return None;
885
1011
  }
886
1012
  }
887
- KeyCode::Char('a') if ctrl => { cursor = 0; redraw(start, &buf, cursor, &mut scroll); }
888
- KeyCode::Char('e') if ctrl => { cursor = buf.len(); redraw(start, &buf, cursor, &mut scroll); }
1013
+ KeyCode::Char('a') if ctrl => { cursor = 0; redraw(prompt, start, &buf, cursor, &mut scroll); }
1014
+ KeyCode::Char('e') if ctrl => { cursor = buf.len(); redraw(prompt, start, &buf, cursor, &mut scroll); }
889
1015
  KeyCode::Char('u') if ctrl => {
890
1016
  kill = buf[..cursor].iter().collect();
891
1017
  buf.drain(..cursor);
892
1018
  cursor = 0;
893
- redraw(start, &buf, cursor, &mut scroll);
1019
+ redraw(prompt, start, &buf, cursor, &mut scroll);
894
1020
  }
895
1021
  KeyCode::Char('k') if ctrl => {
896
1022
  kill = buf[cursor..].iter().collect();
897
1023
  buf.truncate(cursor);
898
- redraw(start, &buf, cursor, &mut scroll);
1024
+ redraw(prompt, start, &buf, cursor, &mut scroll);
899
1025
  }
900
1026
  KeyCode::Char('w') if ctrl => {
901
1027
  let i = prev_word(&buf, cursor);
902
1028
  kill = buf[i..cursor].iter().collect();
903
1029
  buf.drain(i..cursor);
904
1030
  cursor = i;
905
- redraw(start, &buf, cursor, &mut scroll);
1031
+ redraw(prompt, start, &buf, cursor, &mut scroll);
906
1032
  }
907
1033
  KeyCode::Char('y') if ctrl => {
908
1034
  for c in kill.clone().chars() { buf.insert(cursor, c); cursor += 1; }
909
- redraw(start, &buf, cursor, &mut scroll);
1035
+ redraw(prompt, start, &buf, cursor, &mut scroll);
910
1036
  }
911
1037
  KeyCode::Char('l') if ctrl => reline!(),
912
1038
  KeyCode::Char('g') if ctrl => {
@@ -967,15 +1093,15 @@ fn read_line_raw_prefill(prompt: &str, prefill: Vec<char>, prefill_cur: usize) -
967
1093
  }
968
1094
  reline!();
969
1095
  }
970
- KeyCode::Char('b') if alt => { cursor = prev_word(&buf, cursor); redraw(start, &buf, cursor, &mut scroll); }
971
- KeyCode::Char('f') if alt => { cursor = next_word(&buf, cursor); redraw(start, &buf, cursor, &mut scroll); }
972
- KeyCode::Char(c) if !ctrl && !alt => { buf.insert(cursor, c); cursor += 1; redraw(start, &buf, cursor, &mut scroll); }
973
- KeyCode::Backspace => { if cursor > 0 { buf.remove(cursor - 1); cursor -= 1; redraw(start, &buf, cursor, &mut scroll); } }
974
- KeyCode::Delete => { if cursor < buf.len() { buf.remove(cursor); redraw(start, &buf, cursor, &mut scroll); } }
975
- KeyCode::Left => { cursor = cursor.saturating_sub(1); redraw(start, &buf, cursor, &mut scroll); }
976
- KeyCode::Right => { if cursor < buf.len() { cursor += 1; redraw(start, &buf, cursor, &mut scroll); } }
977
- KeyCode::Home => { cursor = 0; redraw(start, &buf, cursor, &mut scroll); }
978
- KeyCode::End => { cursor = buf.len(); redraw(start, &buf, cursor, &mut scroll); }
1096
+ KeyCode::Char('b') if alt => { cursor = prev_word(&buf, cursor); redraw(prompt, start, &buf, cursor, &mut scroll); }
1097
+ KeyCode::Char('f') if alt => { cursor = next_word(&buf, cursor); redraw(prompt, start, &buf, cursor, &mut scroll); }
1098
+ KeyCode::Char(c) if !ctrl && !alt => { buf.insert(cursor, c); cursor += 1; redraw(prompt, start, &buf, cursor, &mut scroll); }
1099
+ KeyCode::Backspace => { if cursor > 0 { buf.remove(cursor - 1); cursor -= 1; redraw(prompt, start, &buf, cursor, &mut scroll); } }
1100
+ KeyCode::Delete => { if cursor < buf.len() { buf.remove(cursor); redraw(prompt, start, &buf, cursor, &mut scroll); } }
1101
+ KeyCode::Left => { cursor = cursor.saturating_sub(1); redraw(prompt, start, &buf, cursor, &mut scroll); }
1102
+ KeyCode::Right => { if cursor < buf.len() { cursor += 1; redraw(prompt, start, &buf, cursor, &mut scroll); } }
1103
+ KeyCode::Home => { cursor = 0; redraw(prompt, start, &buf, cursor, &mut scroll); }
1104
+ KeyCode::End => { cursor = buf.len(); redraw(prompt, start, &buf, cursor, &mut scroll); }
979
1105
  KeyCode::Up => {
980
1106
  if let Ok(h) = history().lock() {
981
1107
  if !h.is_empty() {
@@ -985,7 +1111,7 @@ fn read_line_raw_prefill(prompt: &str, prefill: Vec<char>, prefill_cur: usize) -
985
1111
  cursor = buf.len();
986
1112
  }
987
1113
  }
988
- redraw(start, &buf, cursor, &mut scroll);
1114
+ redraw(prompt, start, &buf, cursor, &mut scroll);
989
1115
  }
990
1116
  KeyCode::Down => {
991
1117
  if let Ok(h) = history().lock() {
@@ -994,18 +1120,20 @@ fn read_line_raw_prefill(prompt: &str, prefill: Vec<char>, prefill_cur: usize) -
994
1120
  _ => { hist_idx = None; buf.clear(); cursor = 0; }
995
1121
  }
996
1122
  }
997
- redraw(start, &buf, cursor, &mut scroll);
1123
+ redraw(prompt, start, &buf, cursor, &mut scroll);
998
1124
  }
999
1125
  KeyCode::Enter => {
1000
1126
  let cont = cursor > 0 && buf[cursor - 1] == '\\';
1001
1127
  if cont {
1002
1128
  buf.remove(cursor - 1);
1003
1129
  cursor -= 1;
1004
- redraw(start, &buf, cursor, &mut scroll);
1130
+ redraw(prompt, start, &buf, cursor, &mut scroll);
1005
1131
  }
1006
- print!("\r\n");
1007
- flush();
1008
- return Some(RawLine::Submit(buf.iter().collect(), cont));
1132
+ let text: String = buf.iter().collect();
1133
+ if !cont {
1134
+ echo_submitted(prompt, &text);
1135
+ }
1136
+ return Some(RawLine::Submit(text, cont));
1009
1137
  }
1010
1138
  KeyCode::Tab => {
1011
1139
  let (tok_start, token) = token_at(&buf, cursor);
@@ -1019,25 +1147,25 @@ fn read_line_raw_prefill(prompt: &str, prefill: Vec<char>, prefill_cur: usize) -
1019
1147
  buf.insert(cursor, ' ');
1020
1148
  cursor += 1;
1021
1149
  }
1022
- redraw(start, &buf, cursor, &mut scroll);
1150
+ redraw(prompt, start, &buf, cursor, &mut scroll);
1023
1151
  } else if cands.len() > 1 {
1024
1152
  let common = common_prefix(&cands);
1025
1153
  if common.chars().count() > token.chars().count() {
1026
1154
  let new: Vec<char> = common.chars().collect();
1027
1155
  buf.splice(tok_start..cursor, new.iter().copied());
1028
1156
  cursor = tok_start + new.len();
1029
- redraw(start, &buf, cursor, &mut scroll);
1157
+ redraw(prompt, start, &buf, cursor, &mut scroll);
1030
1158
  } else {
1031
- print!("\r\n");
1159
+ clear_composer();
1032
1160
  for c in &cands {
1033
- print!(" {}\r\n", dim(c));
1161
+ line(&format!(" {}", dim(c)));
1034
1162
  }
1035
1163
  flush();
1036
1164
  reline!();
1037
1165
  }
1038
1166
  }
1039
1167
  }
1040
- KeyCode::Esc => { buf.clear(); cursor = 0; redraw(start, &buf, cursor, &mut scroll); }
1168
+ KeyCode::Esc => { buf.clear(); cursor = 0; redraw(prompt, start, &buf, cursor, &mut scroll); }
1041
1169
  _ => {}
1042
1170
  }
1043
1171
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "buildwithnexus",
3
- "version": "0.10.3",
4
- "description": "A hilariously fast, agentic AI CLI harness in Rust — remote (API key) or local models, inline terminal UI with sessions, plan/build/brainstorm modes, and hooks",
3
+ "version": "0.10.4",
4
+ "description": "A hilariously fast, agentic AI CLI harness in Rust — remote (API key) or local models, full-screen TUI with sessions, plan/build/brainstorm modes, and hooks",
5
5
  "bin": {
6
6
  "buildwithnexus": "bin/buildwithnexus.js",
7
7
  "bwn": "bin/buildwithnexus.js"
8
8
  },
9
9
  "files": [
10
- "bin/",
10
+ "bin/buildwithnexus.js",
11
11
  "scripts/resolve-binary.js",
12
12
  "scripts/postinstall.js",
13
13
  "harness/Cargo.toml",
@@ -83,9 +83,14 @@ function buildFromSource() {
83
83
  return true;
84
84
  }
85
85
 
86
+ function isLocalCheckout() {
87
+ return fs.existsSync(path.join(ROOT, '.git'));
88
+ }
89
+
86
90
  async function obtain() {
87
91
  if (process.env.BWN_SKIP_INSTALL) return false;
88
- if (existing()) return true;
92
+ if (isLocalCheckout() && hasCargo() && buildFromSource()) return true;
93
+ if (existing() && !isLocalCheckout()) return true;
89
94
 
90
95
  const t = target();
91
96
  if (t) {