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.
- package/harness/Cargo.toml +1 -1
- package/harness/src/agent.rs +9 -1
- package/harness/src/tui.rs +203 -39
- package/package.json +1 -1
- package/scripts/postinstall.js +4 -1
- package/scripts/resolve-binary.js +2 -0
package/harness/Cargo.toml
CHANGED
package/harness/src/agent.rs
CHANGED
|
@@ -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(||
|
|
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
|
|
package/harness/src/tui.rs
CHANGED
|
@@ -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 =
|
|
228
|
-
"\x1b[22m".to_string()
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
862
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
1039
|
+
(None, trimmed)
|
|
867
1040
|
};
|
|
868
1041
|
|
|
869
|
-
let
|
|
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
|
|
890
|
-
format!("{} {} {}", indent,
|
|
1044
|
+
if let Some(pref) = prefix_span {
|
|
1045
|
+
format!("{} {} {}", indent, pref, formatted_rest)
|
|
891
1046
|
} else {
|
|
892
|
-
format!("{}{}", indent,
|
|
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.
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|