Builds the ratatui TUI: EngineHandle bridging the async engine to the render loop, chat viewport rendering with a markdown renderer, input handling, a permission modal wired to the real oneshot ask path, and a session picker. Adds TestBackend snapshot tests covering empty session, tool cards (running/completed/error), permission modal, session picker, and message rendering.
157 lines
4.0 KiB
Rust
157 lines
4.0 KiB
Rust
mod app;
|
|
mod input;
|
|
mod markdown;
|
|
mod modal;
|
|
mod render;
|
|
mod state;
|
|
mod terminal;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use harness_core::event::RunOutcome;
|
|
|
|
const DEFAULT_MODEL: &str = "anthropic/claude-sonnet-4-5";
|
|
|
|
fn print_usage() {
|
|
eprintln!("usage: harness [run -p \"<prompt>\" [-m provider/model]] | [tui]");
|
|
}
|
|
|
|
fn parse_run_args(args: &[String]) -> Option<(String, Option<String>)> {
|
|
let mut prompt = None;
|
|
let mut model = None;
|
|
let mut i = 0;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"-p" | "--prompt" => {
|
|
prompt = args.get(i + 1).cloned();
|
|
i += 2;
|
|
}
|
|
"-m" | "--model" => {
|
|
model = args.get(i + 1).cloned();
|
|
i += 2;
|
|
}
|
|
_ => i += 1,
|
|
}
|
|
}
|
|
prompt.map(|p| (p, model))
|
|
}
|
|
|
|
async fn run_headless(args: &[String]) -> i32 {
|
|
let Some((prompt, model_arg)) = parse_run_args(args) else {
|
|
print_usage();
|
|
return 2;
|
|
};
|
|
|
|
let cwd = match std::env::current_dir() {
|
|
Ok(cwd) => cwd,
|
|
Err(e) => {
|
|
eprintln!("error: could not read cwd: {e}");
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
let app = match harness_app::App::init(cwd) {
|
|
Ok(app) => app,
|
|
Err(e) => {
|
|
eprintln!("error: {e}");
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
let model_ref = model_arg
|
|
.or_else(|| app.config().model.clone())
|
|
.unwrap_or_else(|| DEFAULT_MODEL.to_string());
|
|
|
|
match app.run_prompt(prompt, &model_ref).await {
|
|
Ok((RunOutcome::Stopped, session_id)) => {
|
|
let text = app.final_text(&session_id).await.unwrap_or_default();
|
|
println!("{text}");
|
|
0
|
|
}
|
|
Ok((RunOutcome::Aborted, _)) => {
|
|
eprintln!("run aborted");
|
|
1
|
|
}
|
|
Ok((RunOutcome::Errored { message }, _)) => {
|
|
eprintln!("error: {message}");
|
|
1
|
|
}
|
|
Err(e) => {
|
|
eprintln!("error: {e}");
|
|
1
|
|
}
|
|
}
|
|
}
|
|
|
|
fn log_dir() -> PathBuf {
|
|
dirs::data_dir()
|
|
.unwrap_or_else(std::env::temp_dir)
|
|
.join("ai-harness")
|
|
.join("log")
|
|
}
|
|
|
|
fn setup_tracing() -> anyhow::Result<tracing_appender::non_blocking::WorkerGuard> {
|
|
let log_dir = log_dir();
|
|
std::fs::create_dir_all(&log_dir)?;
|
|
let file_appender = tracing_appender::rolling::daily(log_dir, "harness-tui.log");
|
|
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
|
tracing_subscriber::fmt()
|
|
.with_writer(non_blocking)
|
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
|
.init();
|
|
Ok(guard)
|
|
}
|
|
|
|
async fn run_tui() -> i32 {
|
|
let cwd = match std::env::current_dir() {
|
|
Ok(cwd) => cwd,
|
|
Err(e) => {
|
|
eprintln!("error: could not read cwd: {e}");
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
let _guard = match setup_tracing() {
|
|
Ok(guard) => guard,
|
|
Err(e) => {
|
|
eprintln!("error: could not initialize logging: {e}");
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
let mut app = match crate::app::App::new(cwd).await {
|
|
Ok(app) => app,
|
|
Err(e) => {
|
|
tracing::error!(error = %e, "failed to start TUI");
|
|
eprintln!("error: {e}");
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
if let Err(e) = app.run().await {
|
|
tracing::error!(error = %e, "TUI error");
|
|
eprintln!("error: {e}");
|
|
return 1;
|
|
}
|
|
0
|
|
}
|
|
|
|
#[tokio::main(flavor = "current_thread")]
|
|
async fn main() {
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
let exit_code = match args.first().map(String::as_str) {
|
|
Some("run") => run_headless(&args[1..]).await,
|
|
Some("tui") | None => run_tui().await,
|
|
Some("help") | Some("--help") | Some("-h") => {
|
|
print_usage();
|
|
0
|
|
}
|
|
Some(cmd) => {
|
|
eprintln!("unknown command: {cmd}");
|
|
print_usage();
|
|
2
|
|
}
|
|
};
|
|
std::process::exit(exit_code);
|
|
}
|