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.
- package/harness/Cargo.lock +1 -1
- package/harness/Cargo.toml +1 -1
- package/harness/src/agent.rs +10 -4
- package/harness/src/checkpoint.rs +104 -0
- package/harness/src/config.rs +1 -1
- package/harness/src/lib.rs +296 -39
- package/harness/src/onboarding.rs +50 -8
- package/harness/src/report.rs +2 -2
- package/harness/src/tools.rs +218 -3
- package/harness/src/tui.rs +184 -56
- package/package.json +3 -3
- package/scripts/postinstall.js +6 -1
package/harness/src/tui.rs
CHANGED
|
@@ -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::{
|
|
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
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
468
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
1159
|
+
clear_composer();
|
|
1032
1160
|
for c in &cands {
|
|
1033
|
-
|
|
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.
|
|
4
|
-
"description": "A hilariously fast, agentic AI CLI harness in Rust — remote (API key) or local models,
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -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 (
|
|
92
|
+
if (isLocalCheckout() && hasCargo() && buildFromSource()) return true;
|
|
93
|
+
if (existing() && !isLocalCheckout()) return true;
|
|
89
94
|
|
|
90
95
|
const t = target();
|
|
91
96
|
if (t) {
|