buildwithnexus 0.10.6 → 0.10.7

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.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "buildwithnexus"
3
- version = "0.10.6"
3
+ version = "0.10.7"
4
4
  edition = "2021"
5
5
  description = "A hilariously fast agentic AI CLI harness — remote or local models, full TUI with modes, memory, skills, hooks, and image input"
6
6
  license = "MIT"
@@ -681,7 +681,15 @@ fn collect_text_tool_calls(
681
681
  .get("arguments")
682
682
  .or_else(|| obj.get("input"))
683
683
  .cloned()
684
- .unwrap_or_else(|| serde_json::json!({}));
684
+ .unwrap_or_else(|| {
685
+ let mut map = serde_json::Map::new();
686
+ for (k, v) in obj {
687
+ if k != "name" && k != "tool_name" && k != "type" && k != "id" {
688
+ map.insert(k.clone(), v.clone());
689
+ }
690
+ }
691
+ serde_json::Value::Object(map)
692
+ });
685
693
  out.push(text_tool_call(name, input));
686
694
  }
687
695
 
@@ -224,12 +224,17 @@ fn attr(code: &str, s: &str) -> String {
224
224
  if no_color() {
225
225
  return s.to_string();
226
226
  }
227
- let reset = if code == "1" {
228
- "\x1b[22m".to_string()
229
- } else if ALT_SCREEN.load(Ordering::Relaxed) {
230
- format!("\x1b[0m\x1b[{}m", sgr_fg(TEXT))
231
- } else {
232
- "\x1b[0m".to_string()
227
+ let reset = match code {
228
+ "1" => "\x1b[22m".to_string(),
229
+ "3" => "\x1b[23m".to_string(),
230
+ "4" => "\x1b[24m".to_string(),
231
+ _ => {
232
+ if ALT_SCREEN.load(Ordering::Relaxed) {
233
+ format!("\x1b[0m\x1b[{}m", sgr_fg(TEXT))
234
+ } else {
235
+ "\x1b[0m".to_string()
236
+ }
237
+ }
233
238
  };
234
239
  format!("\x1b[{code}m{s}{reset}")
235
240
  }
@@ -237,6 +242,12 @@ fn attr(code: &str, s: &str) -> String {
237
242
  pub fn bold(s: &str) -> String {
238
243
  attr("1", s)
239
244
  }
245
+ pub fn italic(s: &str) -> String {
246
+ attr("3", s)
247
+ }
248
+ pub fn underline(s: &str) -> String {
249
+ attr("4", s)
250
+ }
240
251
  pub fn dim(s: &str) -> String {
241
252
  paint(MUTED, s)
242
253
  }
@@ -564,7 +575,9 @@ fn json_value_looks_like_tool_call(value: &serde_json::Value) -> bool {
564
575
  }
565
576
  let has_name = obj.get("name").and_then(|v| v.as_str()).is_some()
566
577
  || obj.get("tool_name").and_then(|v| v.as_str()).is_some();
567
- let has_args = obj.get("arguments").is_some() || obj.get("input").is_some();
578
+ let has_args = obj.get("arguments").is_some()
579
+ || obj.get("input").is_some()
580
+ || obj.keys().any(|k| k != "name" && k != "tool_name" && k != "type" && k != "id");
568
581
  let openai_function = obj
569
582
  .get("function")
570
583
  .and_then(|v| v.as_object())
@@ -652,6 +665,11 @@ fn typeahead() -> &'static std::sync::Mutex<TypeAheadState> {
652
665
  })
653
666
  }
654
667
 
668
+ fn message_queue() -> &'static std::sync::Mutex<Vec<String>> {
669
+ static MQ: std::sync::OnceLock<std::sync::Mutex<Vec<String>>> = std::sync::OnceLock::new();
670
+ MQ.get_or_init(|| std::sync::Mutex::new(Vec::new()))
671
+ }
672
+
655
673
  /// Non-blocking drain of pending key events during agent processing.
656
674
  /// Buffers printable input; Ctrl+C clears the buffer and signals an interrupt.
657
675
  pub fn poll_typeahead() {
@@ -700,6 +718,65 @@ pub fn poll_typeahead() {
700
718
  Err(_) => continue,
701
719
  };
702
720
  match k.code {
721
+ KeyCode::Enter => {
722
+ let text: String = ta.buf.iter().collect();
723
+ let trimmed = text.trim();
724
+ if !trimmed.is_empty() {
725
+ if let Ok(mut mq) = message_queue().lock() {
726
+ mq.push(trimmed.to_string());
727
+ }
728
+ ta.buf.clear();
729
+ ta.cursor = 0;
730
+ drop(ta);
731
+ render_output();
732
+ clear_composer();
733
+ render_footer();
734
+ render_queued_composer();
735
+ }
736
+ continue;
737
+ }
738
+ KeyCode::Up if !alt => {
739
+ if ta.buf.is_empty() {
740
+ if let Ok(mut mq) = message_queue().lock() {
741
+ if let Some(last) = mq.pop() {
742
+ ta.buf = last.chars().collect();
743
+ ta.cursor = ta.buf.len();
744
+ drop(ta);
745
+ render_output();
746
+ clear_composer();
747
+ render_footer();
748
+ render_queued_composer();
749
+ }
750
+ }
751
+ }
752
+ continue;
753
+ }
754
+ KeyCode::Char('q') if ctrl => {
755
+ if let Ok(mut mq) = message_queue().lock() {
756
+ if let Some(last) = mq.pop() {
757
+ ta.buf = last.chars().collect();
758
+ ta.cursor = ta.buf.len();
759
+ drop(ta);
760
+ render_output();
761
+ clear_composer();
762
+ render_footer();
763
+ render_queued_composer();
764
+ }
765
+ }
766
+ continue;
767
+ }
768
+ KeyCode::Char('x') if ctrl => {
769
+ if let Ok(mut mq) = message_queue().lock() {
770
+ if mq.pop().is_some() {
771
+ drop(ta);
772
+ render_output();
773
+ clear_composer();
774
+ render_footer();
775
+ render_queued_composer();
776
+ }
777
+ }
778
+ continue;
779
+ }
703
780
  KeyCode::Char('c') if ctrl => {
704
781
  TYPEAHEAD_INTERRUPTED.store(true, Ordering::Relaxed);
705
782
  ta.buf.clear();
@@ -764,6 +841,28 @@ pub fn render_queued_composer() {
764
841
  return;
765
842
  }
766
843
  if let Ok(ta) = typeahead().lock() {
844
+ if let Ok(mq) = message_queue().lock() {
845
+ let mut out = io::stdout();
846
+ let c_row = composer_row();
847
+ let q_len = mq.len() as u16;
848
+ for (i, msg) in mq.iter().enumerate() {
849
+ let row = c_row.saturating_sub(q_len).saturating_add(i as u16);
850
+ let _ = queue!(
851
+ out,
852
+ MoveTo(0, row),
853
+ Clear(ClearType::CurrentLine)
854
+ );
855
+ let _ = write!(
856
+ out,
857
+ " {} {} {} {}",
858
+ dim("├─"),
859
+ dim("queued:"),
860
+ bold(msg),
861
+ dim("(Ctrl+Q edit, Ctrl+X rm)")
862
+ );
863
+ }
864
+ let _ = out.flush();
865
+ }
767
866
  let mut scroll = 0usize;
768
867
  render_composer(
769
868
  &format!("{} {} ", dim("queued"), accent("›")),
@@ -791,16 +890,21 @@ fn term_size() -> (u16, u16) {
791
890
  }
792
891
 
793
892
  fn reserved_rows() -> u16 {
893
+ let q_len = message_queue().lock().map(|q| q.len()).unwrap_or(0) as u16;
794
894
  if ALT_SCREEN.load(Ordering::Relaxed) {
795
- 2
895
+ 2 + q_len
796
896
  } else {
797
- 1
897
+ 1 + q_len
798
898
  }
799
899
  }
800
900
 
801
901
  fn composer_row() -> u16 {
802
902
  let (_, h) = term_size();
803
- h.saturating_sub(reserved_rows()).min(h.saturating_sub(1))
903
+ if ALT_SCREEN.load(Ordering::Relaxed) {
904
+ h.saturating_sub(2).min(h.saturating_sub(1))
905
+ } else {
906
+ h.saturating_sub(1)
907
+ }
804
908
  }
805
909
 
806
910
  fn footer_row() -> u16 {
@@ -843,6 +947,65 @@ pub fn str_width(s: &str) -> usize {
843
947
  s.chars().map(char_width).sum()
844
948
  }
845
949
 
950
+ fn format_links(s: &str) -> String {
951
+ let mut out = String::new();
952
+ let mut rest = s;
953
+ while let Some(start) = rest.find('[') {
954
+ if let Some(mid) = rest[start..].find("](") {
955
+ let mid_abs = start + mid;
956
+ if let Some(end) = rest[mid_abs..].find(')') {
957
+ let end_abs = mid_abs + end;
958
+ out.push_str(&rest[..start]);
959
+ let label = &rest[start + 1..mid_abs];
960
+ let url = &rest[mid_abs + 2..end_abs];
961
+ out.push_str(&format!("{} {}", underline(&cyan(label)), dim(&format!("({url})"))));
962
+ rest = &rest[end_abs + 1..];
963
+ continue;
964
+ }
965
+ }
966
+ out.push_str(&rest[..start + 1]);
967
+ rest = &rest[start + 1..];
968
+ }
969
+ out.push_str(rest);
970
+ out
971
+ }
972
+
973
+ fn format_italics(s: &str) -> String {
974
+ let mut out = String::new();
975
+ let parts: Vec<&str> = s.split('*').collect();
976
+ for (i, part) in parts.iter().enumerate() {
977
+ if i % 2 == 1 && parts.len() > 1 {
978
+ out.push_str(&italic(part));
979
+ } else {
980
+ out.push_str(part);
981
+ }
982
+ }
983
+ out
984
+ }
985
+
986
+ fn format_inline_md(text: &str) -> String {
987
+ let mut out = String::new();
988
+ let code_parts: Vec<&str> = text.split('`').collect();
989
+ for (i, part) in code_parts.iter().enumerate() {
990
+ if i % 2 == 1 {
991
+ out.push_str(&yellow(part));
992
+ } else {
993
+ let with_links = format_links(part);
994
+ let mut bold_out = String::new();
995
+ let bold_parts: Vec<&str> = with_links.split("**").collect();
996
+ for (j, bpart) in bold_parts.iter().enumerate() {
997
+ if j % 2 == 1 {
998
+ bold_out.push_str(&bold(bpart));
999
+ } else {
1000
+ bold_out.push_str(&format_italics(bpart));
1001
+ }
1002
+ }
1003
+ out.push_str(&bold_out);
1004
+ }
1005
+ }
1006
+ out
1007
+ }
1008
+
846
1009
  pub fn render_md_line(s: &str) -> String {
847
1010
  if no_color() {
848
1011
  return s.to_string();
@@ -850,46 +1013,38 @@ pub fn render_md_line(s: &str) -> String {
850
1013
  let trimmed = s.trim_start();
851
1014
  let indent = &s[..s.len().saturating_sub(trimmed.len())];
852
1015
  if let Some(header) = trimmed.strip_prefix("### ") {
853
- return format!("{}{}", indent, bold(&blue(header)));
1016
+ return format!("{}{}", indent, bold(&blue(&format_inline_md(header))));
854
1017
  }
855
1018
  if let Some(header) = trimmed.strip_prefix("## ") {
856
- return format!("{}{}", indent, bold(&cyan(header)));
1019
+ return format!("{}{}", indent, bold(&cyan(&format_inline_md(header))));
857
1020
  }
858
1021
  if let Some(header) = trimmed.strip_prefix("# ") {
859
- return format!("{}{}", indent, bold(&accent(header)));
1022
+ return format!("{}{}", indent, bold(&accent(&format_inline_md(header))));
860
1023
  }
861
- let (is_bullet, rest) = if let Some(r) = trimmed.strip_prefix("- ") {
862
- (true, r)
1024
+ if let Some(quote) = trimmed.strip_prefix("> ") {
1025
+ return format!("{} {} {}", indent, dim("│"), italic(&dim(&format_inline_md(quote))));
1026
+ }
1027
+ let (prefix_span, rest) = if let Some(r) = trimmed.strip_prefix("- ") {
1028
+ (Some(dim("•")), r)
863
1029
  } else if let Some(r) = trimmed.strip_prefix("* ") {
864
- (true, r)
1030
+ (Some(dim("•")), r)
1031
+ } else if let Some(idx) = trimmed.find(". ") {
1032
+ if idx > 0 && idx <= 3 && trimmed[..idx].chars().all(|c| c.is_ascii_digit()) {
1033
+ let num_str = &trimmed[..idx + 1];
1034
+ (Some(bold(&cyan(num_str))), &trimmed[idx + 2..])
1035
+ } else {
1036
+ (None, trimmed)
1037
+ }
865
1038
  } else {
866
- (false, trimmed)
1039
+ (None, trimmed)
867
1040
  };
868
1041
 
869
- let mut out = String::new();
870
- let parts: Vec<&str> = rest.split("**").collect();
871
- for (i, part) in parts.iter().enumerate() {
872
- let piece = if i % 2 == 1 {
873
- bold(part)
874
- } else {
875
- let mut cp_out = String::new();
876
- let code_parts: Vec<&str> = part.split('`').collect();
877
- for (j, cpart) in code_parts.iter().enumerate() {
878
- if j % 2 == 1 {
879
- cp_out.push_str(&yellow(cpart));
880
- } else {
881
- cp_out.push_str(cpart);
882
- }
883
- }
884
- cp_out
885
- };
886
- out.push_str(&piece);
887
- }
1042
+ let formatted_rest = format_inline_md(rest);
888
1043
 
889
- if is_bullet {
890
- format!("{} {} {}", indent, dim("•"), out)
1044
+ if let Some(pref) = prefix_span {
1045
+ format!("{} {} {}", indent, pref, formatted_rest)
891
1046
  } else {
892
- format!("{}{}", indent, out)
1047
+ format!("{}{}", indent, formatted_rest)
893
1048
  }
894
1049
  }
895
1050
 
@@ -1061,6 +1216,7 @@ fn render_output() {
1061
1216
  *rows = plain_rows;
1062
1217
  }
1063
1218
  let _ = out.flush();
1219
+ render_queued_composer();
1064
1220
  }
1065
1221
 
1066
1222
  fn scroll_output(delta: isize) {
@@ -1773,6 +1929,14 @@ pub fn ask(prompt: &str) -> Option<String> {
1773
1929
  // submits. Shift+Tab returns CycleMode without submitting.
1774
1930
  // Pre-fills the first line with any keystrokes typed during agent processing.
1775
1931
  pub fn ask_task(prompt: &str) -> Option<InputEvent> {
1932
+ if let Ok(mut mq) = message_queue().lock() {
1933
+ if !mq.is_empty() {
1934
+ let msg = mq.remove(0);
1935
+ push_history(&msg);
1936
+ echo_submitted(prompt, &msg);
1937
+ return Some(InputEvent::Text(msg));
1938
+ }
1939
+ }
1776
1940
  if !is_raw() {
1777
1941
  return ask(prompt).map(InputEvent::Text);
1778
1942
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buildwithnexus",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
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",
@@ -78,7 +78,10 @@ function buildFromSource() {
78
78
  const r = spawnSync('cargo', args, { stdio: 'inherit' });
79
79
  if (r.status !== 0) return false;
80
80
  fs.mkdirSync(path.join(ROOT, 'bin'), { recursive: true });
81
- fs.copyFileSync(path.join(ROOT, 'harness', 'target', 'release', 'buildwithnexus' + ext()), installedBinary());
81
+ const rootTarget = path.join(ROOT, 'target', 'release', 'buildwithnexus' + ext());
82
+ const harnessTarget = path.join(ROOT, 'harness', 'target', 'release', 'buildwithnexus' + ext());
83
+ const built = fs.existsSync(rootTarget) ? rootTarget : harnessTarget;
84
+ fs.copyFileSync(built, installedBinary());
82
85
  try { fs.chmodSync(installedBinary(), 0o755); } catch {}
83
86
  return true;
84
87
  }
@@ -26,6 +26,8 @@ function installedBinary() {
26
26
  }
27
27
 
28
28
  function devBinary() {
29
+ const rootTarget = path.join(ROOT, 'target', 'release', 'buildwithnexus' + ext());
30
+ if (fs.existsSync(rootTarget)) return rootTarget;
29
31
  return path.join(ROOT, 'harness', 'target', 'release', 'buildwithnexus' + ext());
30
32
  }
31
33