Compare commits
4 Commits
master
...
feature/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a2c327f7b | ||
|
|
658a8eb744 | ||
|
|
7dd8ae4b24 | ||
|
|
d8c548e22b |
@ -23,6 +23,11 @@ What it does by default:
|
|||||||
1. `cargo build --release --locked`
|
1. `cargo build --release --locked`
|
||||||
2. `cargo install --path . --force --locked`
|
2. `cargo install --path . --force --locked`
|
||||||
|
|
||||||
|
> **Note:** In an interactive terminal (TTY), `./install.sh` automatically runs the
|
||||||
|
> guided installer with interactive onboarding (`--guided --interactive-onboard`).
|
||||||
|
> The silent build-and-install described above only applies when the script is run
|
||||||
|
> non-interactively (e.g., piped or in CI).
|
||||||
|
|
||||||
### Resource preflight and pre-built flow
|
### Resource preflight and pre-built flow
|
||||||
|
|
||||||
Source builds typically require at least:
|
Source builds typically require at least:
|
||||||
|
|||||||
@ -11,6 +11,7 @@ use crate::security::SecurityPolicy;
|
|||||||
use crate::tools::{self, Tool};
|
use crate::tools::{self, Tool};
|
||||||
use crate::util::truncate_with_ellipsis;
|
use crate::util::truncate_with_ellipsis;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use console::{style, Term};
|
||||||
use regex::{Regex, RegexSet};
|
use regex::{Regex, RegexSet};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
@ -106,6 +107,69 @@ pub(crate) const PROGRESS_MIN_INTERVAL_MS: u64 = 500;
|
|||||||
/// Used before streaming the final answer so progress lines are replaced by the clean response.
|
/// Used before streaming the final answer so progress lines are replaced by the clean response.
|
||||||
pub(crate) const DRAFT_CLEAR_SENTINEL: &str = "\x00CLEAR\x00";
|
pub(crate) const DRAFT_CLEAR_SENTINEL: &str = "\x00CLEAR\x00";
|
||||||
|
|
||||||
|
// ── Interactive agent UI helpers ─────────────────────────────────────
|
||||||
|
|
||||||
|
fn agent_clear_screen() {
|
||||||
|
let _ = Term::stdout().clear_screen();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_agent_banner(provider: &str, model: &str) {
|
||||||
|
println!(
|
||||||
|
"{}\n{} {} | {} {}\n{}\n",
|
||||||
|
style("ZeroClaw Agent").color256(39).bold(),
|
||||||
|
style("provider:").dim(),
|
||||||
|
style(provider).bold(),
|
||||||
|
style("model:").dim(),
|
||||||
|
style(model).bold(),
|
||||||
|
style("Type /help for commands.").dim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_agent_prompt() {
|
||||||
|
print!("{} ", style("agent>").color256(45).bold());
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_agent_help() {
|
||||||
|
println!(
|
||||||
|
"{}\n {} Show this help message\n {} Clear conversation history\n {} Exit interactive mode\n",
|
||||||
|
style("Commands").color256(45).bold(),
|
||||||
|
style("/help").bold(),
|
||||||
|
style("/clear /new").bold(),
|
||||||
|
style("/quit /exit").bold(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_agent_response(response: &str) {
|
||||||
|
println!("\n{} {}\n", style("ZeroClaw").color256(39).bold(), response,);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_agent_error(error: &str) {
|
||||||
|
eprintln!("{} {}", style("Error:").red().bold(), style(error).red());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_agent_compaction() {
|
||||||
|
println!("{}", style("Context compacted.").dim());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_tool_start(tool_name: &str) {
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
style("[tool]").color256(111).bold(),
|
||||||
|
style(tool_name).bold(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_tool_complete(tool_name: &str, success: bool) {
|
||||||
|
let icon = if success { "done" } else { "failed" };
|
||||||
|
println!(
|
||||||
|
"{} {} {}",
|
||||||
|
style("[tool]").color256(111).bold(),
|
||||||
|
style(tool_name).bold(),
|
||||||
|
style(icon).dim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract a short hint from tool call arguments for progress display.
|
/// Extract a short hint from tool call arguments for progress display.
|
||||||
fn truncate_tool_args_for_progress(name: &str, args: &serde_json::Value, max_len: usize) -> String {
|
fn truncate_tool_args_for_progress(name: &str, args: &serde_json::Value, max_len: usize) -> String {
|
||||||
let hint = match name {
|
let hint = match name {
|
||||||
@ -2622,6 +2686,8 @@ pub(crate) async fn run_tool_call_loop(
|
|||||||
};
|
};
|
||||||
tracing::debug!(tool = %tool_name, "Sending progress start to draft");
|
tracing::debug!(tool = %tool_name, "Sending progress start to draft");
|
||||||
let _ = tx.send(progress).await;
|
let _ = tx.send(progress).await;
|
||||||
|
} else if !silent {
|
||||||
|
print_tool_start(&tool_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
executable_indices.push(idx);
|
executable_indices.push(idx);
|
||||||
@ -2693,6 +2759,8 @@ pub(crate) async fn run_tool_call_loop(
|
|||||||
};
|
};
|
||||||
tracing::debug!(tool = %call.name, secs, "Sending progress complete to draft");
|
tracing::debug!(tool = %call.name, secs, "Sending progress complete to draft");
|
||||||
let _ = tx.send(format!("{icon} {} ({secs}s)\n", call.name)).await;
|
let _ = tx.send(format!("{icon} {} ({secs}s)\n", call.name)).await;
|
||||||
|
} else if !silent {
|
||||||
|
print_tool_complete(&call.name, outcome.success);
|
||||||
}
|
}
|
||||||
|
|
||||||
ordered_results[*idx] = Some((call.name.clone(), call.tool_call_id.clone(), outcome));
|
ordered_results[*idx] = Some((call.name.clone(), call.tool_call_id.clone(), outcome));
|
||||||
@ -3114,23 +3182,21 @@ pub async fn run(
|
|||||||
println!("{response}");
|
println!("{response}");
|
||||||
observer.record_event(&ObserverEvent::TurnComplete);
|
observer.record_event(&ObserverEvent::TurnComplete);
|
||||||
} else {
|
} else {
|
||||||
println!("🦀 ZeroClaw Interactive Mode");
|
agent_clear_screen();
|
||||||
println!("Type /help for commands.\n");
|
print_agent_banner(provider_name, model_name);
|
||||||
let cli = crate::channels::CliChannel::new();
|
|
||||||
|
|
||||||
// Persistent conversation history across turns
|
// Persistent conversation history across turns
|
||||||
let mut history = vec![ChatMessage::system(&system_prompt)];
|
let mut history = vec![ChatMessage::system(&system_prompt)];
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
print!("> ");
|
print_agent_prompt();
|
||||||
let _ = std::io::stdout().flush();
|
|
||||||
|
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
match std::io::stdin().read_line(&mut input) {
|
match std::io::stdin().read_line(&mut input) {
|
||||||
Ok(0) => break,
|
Ok(0) => break,
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("\nError reading input: {e}\n");
|
print_agent_error(&format!("reading input: {e}"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3140,20 +3206,28 @@ pub async fn run(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match user_input.as_str() {
|
match user_input.as_str() {
|
||||||
"/quit" | "/exit" => break,
|
"/quit" | "/exit" => {
|
||||||
|
println!("{}", style("Closing ZeroClaw agent.").dim());
|
||||||
|
break;
|
||||||
|
}
|
||||||
"/help" => {
|
"/help" => {
|
||||||
println!("Available commands:");
|
print_agent_help();
|
||||||
println!(" /help Show this help message");
|
|
||||||
println!(" /clear /new Clear conversation history");
|
|
||||||
println!(" /quit /exit Exit interactive mode\n");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
"/clear" | "/new" => {
|
"/clear" | "/new" => {
|
||||||
println!(
|
println!(
|
||||||
"This will clear the current conversation and delete all session memory."
|
"{}",
|
||||||
|
style(
|
||||||
|
"This will clear the current conversation and delete all session memory."
|
||||||
|
)
|
||||||
|
.yellow()
|
||||||
);
|
);
|
||||||
println!("Core memories (long-term facts/preferences) will be preserved.");
|
println!(
|
||||||
print!("Continue? [y/N] ");
|
"{}",
|
||||||
|
style("Core memories (long-term facts/preferences) will be preserved.")
|
||||||
|
.dim()
|
||||||
|
);
|
||||||
|
print!("{} ", style("Continue? [y/N]").bold());
|
||||||
let _ = std::io::stdout().flush();
|
let _ = std::io::stdout().flush();
|
||||||
|
|
||||||
let mut confirm = String::new();
|
let mut confirm = String::new();
|
||||||
@ -3161,7 +3235,7 @@ pub async fn run(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !matches!(confirm.trim().to_lowercase().as_str(), "y" | "yes") {
|
if !matches!(confirm.trim().to_lowercase().as_str(), "y" | "yes") {
|
||||||
println!("Cancelled.\n");
|
println!("{}", style("Cancelled.").dim());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3178,9 +3252,15 @@ pub async fn run(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cleared > 0 {
|
if cleared > 0 {
|
||||||
println!("Conversation cleared ({cleared} memory entries removed).\n");
|
println!(
|
||||||
|
"{}",
|
||||||
|
style(format!(
|
||||||
|
"Conversation cleared ({cleared} memory entries removed)."
|
||||||
|
))
|
||||||
|
.green()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("Conversation cleared.\n");
|
println!("{}", style("Conversation cleared.").green());
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -3235,19 +3315,12 @@ pub async fn run(
|
|||||||
{
|
{
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("\nError: {e}\n");
|
print_agent_error(&e.to_string());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
final_output = response.clone();
|
final_output = response.clone();
|
||||||
if let Err(e) = crate::channels::Channel::send(
|
print_agent_response(&response);
|
||||||
&cli,
|
|
||||||
&crate::channels::traits::SendMessage::new(format!("\n{response}\n"), "user"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
eprintln!("\nError sending CLI response: {e}\n");
|
|
||||||
}
|
|
||||||
observer.record_event(&ObserverEvent::TurnComplete);
|
observer.record_event(&ObserverEvent::TurnComplete);
|
||||||
|
|
||||||
// Auto-compaction before hard trimming to preserve long-context signal.
|
// Auto-compaction before hard trimming to preserve long-context signal.
|
||||||
@ -3260,7 +3333,7 @@ pub async fn run(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if compacted {
|
if compacted {
|
||||||
println!("🧹 Auto-compaction complete");
|
print_agent_compaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
src/main.rs
81
src/main.rs
@ -77,6 +77,7 @@ mod service;
|
|||||||
mod skillforge;
|
mod skillforge;
|
||||||
mod skills;
|
mod skills;
|
||||||
mod tools;
|
mod tools;
|
||||||
|
mod tui;
|
||||||
mod tunnel;
|
mod tunnel;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
@ -213,6 +214,39 @@ Examples:
|
|||||||
gateway_command: Option<zeroclaw::GatewayCommands>,
|
gateway_command: Option<zeroclaw::GatewayCommands>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Open the terminal UI connected to the gateway
|
||||||
|
#[command(long_about = "\
|
||||||
|
Open the terminal UI connected to the gateway.
|
||||||
|
|
||||||
|
Connects to the ZeroClaw gateway websocket chat endpoint and opens a
|
||||||
|
clean terminal-first chat session with blue ZeroClaw styling.
|
||||||
|
|
||||||
|
If --token is not provided, ZeroClaw reuses the first paired gateway token
|
||||||
|
from your config when one is available.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
zeroclaw tui
|
||||||
|
zeroclaw tui --url ws://127.0.0.1:42617 --token <token>
|
||||||
|
zeroclaw tui --session main --deliver
|
||||||
|
zeroclaw tui --url https://zeroclawlabs.ai --session prod")]
|
||||||
|
Tui {
|
||||||
|
/// Gateway websocket or base URL; defaults to ws://<gateway.host>:<gateway.port>/ws/chat
|
||||||
|
#[arg(long)]
|
||||||
|
url: Option<String>,
|
||||||
|
|
||||||
|
/// Gateway bearer token; falls back to first paired token from config when available
|
||||||
|
#[arg(long)]
|
||||||
|
token: Option<String>,
|
||||||
|
|
||||||
|
/// Session label to show in the TUI and forward to the gateway metadata
|
||||||
|
#[arg(long)]
|
||||||
|
session: Option<String>,
|
||||||
|
|
||||||
|
/// Forward delivery intent metadata for downstream gateways that honor it
|
||||||
|
#[arg(long)]
|
||||||
|
deliver: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Start long-running autonomous runtime (gateway + channels + heartbeat + scheduler)
|
/// Start long-running autonomous runtime (gateway + channels + heartbeat + scheduler)
|
||||||
#[command(long_about = "\
|
#[command(long_about = "\
|
||||||
Start the long-running autonomous daemon.
|
Start the long-running autonomous daemon.
|
||||||
@ -678,10 +712,15 @@ async fn main() -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize logging - respects RUST_LOG env var, defaults to INFO
|
// Initialize logging - respects RUST_LOG env var.
|
||||||
|
// In interactive agent mode, default to "error" so tracing logs don't pollute the chat.
|
||||||
|
let default_filter = match &cli.command {
|
||||||
|
Commands::Agent { message: None, .. } => "error",
|
||||||
|
_ => "info",
|
||||||
|
};
|
||||||
let subscriber = fmt::Subscriber::builder()
|
let subscriber = fmt::Subscriber::builder()
|
||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter)),
|
||||||
)
|
)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
@ -930,6 +969,13 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Commands::Tui {
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
session,
|
||||||
|
deliver,
|
||||||
|
} => tui::run_tui(&config, url, token, session, deliver).await,
|
||||||
|
|
||||||
Commands::Daemon { port, host } => {
|
Commands::Daemon { port, host } => {
|
||||||
let port = port.unwrap_or(config.gateway.port);
|
let port = port.unwrap_or(config.gateway.port);
|
||||||
let host = host.unwrap_or_else(|| config.gateway.host.clone());
|
let host = host.unwrap_or_else(|| config.gateway.host.clone());
|
||||||
@ -2158,6 +2204,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tui_cli_accepts_gateway_examples() {
|
||||||
|
let cli = Cli::try_parse_from([
|
||||||
|
"zeroclaw",
|
||||||
|
"tui",
|
||||||
|
"--url",
|
||||||
|
"ws://127.0.0.1:18789",
|
||||||
|
"--token",
|
||||||
|
"zc-token",
|
||||||
|
"--session",
|
||||||
|
"main",
|
||||||
|
"--deliver",
|
||||||
|
])
|
||||||
|
.expect("tui invocation should parse");
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Tui {
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
session,
|
||||||
|
deliver,
|
||||||
|
} => {
|
||||||
|
assert_eq!(url.as_deref(), Some("ws://127.0.0.1:18789"));
|
||||||
|
assert_eq!(token.as_deref(), Some("zc-token"));
|
||||||
|
assert_eq!(session.as_deref(), Some("main"));
|
||||||
|
assert!(deliver);
|
||||||
|
}
|
||||||
|
other => panic!("expected tui command, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_parses_estop_default_engage() {
|
fn cli_parses_estop_default_engage() {
|
||||||
let cli = Cli::try_parse_from(["zeroclaw", "estop"]).expect("estop command should parse");
|
let cli = Cli::try_parse_from(["zeroclaw", "estop"]).expect("estop command should parse");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user