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`
|
||||
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
|
||||
|
||||
Source builds typically require at least:
|
||||
|
||||
@ -11,6 +11,7 @@ use crate::security::SecurityPolicy;
|
||||
use crate::tools::{self, Tool};
|
||||
use crate::util::truncate_with_ellipsis;
|
||||
use anyhow::Result;
|
||||
use console::{style, Term};
|
||||
use regex::{Regex, RegexSet};
|
||||
use std::collections::HashSet;
|
||||
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.
|
||||
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.
|
||||
fn truncate_tool_args_for_progress(name: &str, args: &serde_json::Value, max_len: usize) -> String {
|
||||
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");
|
||||
let _ = tx.send(progress).await;
|
||||
} else if !silent {
|
||||
print_tool_start(&tool_name);
|
||||
}
|
||||
|
||||
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");
|
||||
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));
|
||||
@ -3114,23 +3182,21 @@ pub async fn run(
|
||||
println!("{response}");
|
||||
observer.record_event(&ObserverEvent::TurnComplete);
|
||||
} else {
|
||||
println!("🦀 ZeroClaw Interactive Mode");
|
||||
println!("Type /help for commands.\n");
|
||||
let cli = crate::channels::CliChannel::new();
|
||||
agent_clear_screen();
|
||||
print_agent_banner(provider_name, model_name);
|
||||
|
||||
// Persistent conversation history across turns
|
||||
let mut history = vec![ChatMessage::system(&system_prompt)];
|
||||
|
||||
loop {
|
||||
print!("> ");
|
||||
let _ = std::io::stdout().flush();
|
||||
print_agent_prompt();
|
||||
|
||||
let mut input = String::new();
|
||||
match std::io::stdin().read_line(&mut input) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("\nError reading input: {e}\n");
|
||||
print_agent_error(&format!("reading input: {e}"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -3140,20 +3206,28 @@ pub async fn run(
|
||||
continue;
|
||||
}
|
||||
match user_input.as_str() {
|
||||
"/quit" | "/exit" => break,
|
||||
"/quit" | "/exit" => {
|
||||
println!("{}", style("Closing ZeroClaw agent.").dim());
|
||||
break;
|
||||
}
|
||||
"/help" => {
|
||||
println!("Available commands:");
|
||||
println!(" /help Show this help message");
|
||||
println!(" /clear /new Clear conversation history");
|
||||
println!(" /quit /exit Exit interactive mode\n");
|
||||
print_agent_help();
|
||||
continue;
|
||||
}
|
||||
"/clear" | "/new" => {
|
||||
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.");
|
||||
print!("Continue? [y/N] ");
|
||||
println!(
|
||||
"{}",
|
||||
style("Core memories (long-term facts/preferences) will be preserved.")
|
||||
.dim()
|
||||
);
|
||||
print!("{} ", style("Continue? [y/N]").bold());
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut confirm = String::new();
|
||||
@ -3161,7 +3235,7 @@ pub async fn run(
|
||||
continue;
|
||||
}
|
||||
if !matches!(confirm.trim().to_lowercase().as_str(), "y" | "yes") {
|
||||
println!("Cancelled.\n");
|
||||
println!("{}", style("Cancelled.").dim());
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -3178,9 +3252,15 @@ pub async fn run(
|
||||
}
|
||||
}
|
||||
if cleared > 0 {
|
||||
println!("Conversation cleared ({cleared} memory entries removed).\n");
|
||||
println!(
|
||||
"{}",
|
||||
style(format!(
|
||||
"Conversation cleared ({cleared} memory entries removed)."
|
||||
))
|
||||
.green()
|
||||
);
|
||||
} else {
|
||||
println!("Conversation cleared.\n");
|
||||
println!("{}", style("Conversation cleared.").green());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@ -3235,19 +3315,12 @@ pub async fn run(
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
eprintln!("\nError: {e}\n");
|
||||
print_agent_error(&e.to_string());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
final_output = response.clone();
|
||||
if let Err(e) = crate::channels::Channel::send(
|
||||
&cli,
|
||||
&crate::channels::traits::SendMessage::new(format!("\n{response}\n"), "user"),
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("\nError sending CLI response: {e}\n");
|
||||
}
|
||||
print_agent_response(&response);
|
||||
observer.record_event(&ObserverEvent::TurnComplete);
|
||||
|
||||
// Auto-compaction before hard trimming to preserve long-context signal.
|
||||
@ -3260,7 +3333,7 @@ pub async fn run(
|
||||
.await
|
||||
{
|
||||
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 skills;
|
||||
mod tools;
|
||||
mod tui;
|
||||
mod tunnel;
|
||||
mod util;
|
||||
|
||||
@ -213,6 +214,39 @@ Examples:
|
||||
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)
|
||||
#[command(long_about = "\
|
||||
Start the long-running autonomous daemon.
|
||||
@ -678,10 +712,15 @@ async fn main() -> Result<()> {
|
||||
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()
|
||||
.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();
|
||||
|
||||
@ -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 } => {
|
||||
let port = port.unwrap_or(config.gateway.port);
|
||||
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]
|
||||
fn cli_parses_estop_default_engage() {
|
||||
let cli = Cli::try_parse_from(["zeroclaw", "estop"]).expect("estop command should parse");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user