From d101892bdf08163ddd31ba4d4b6d1629184142a6 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 3 Mar 2026 15:40:24 -0500 Subject: [PATCH] Switch LLM chat input to dialoguer and use ColorfulTheme everywhere Replace raw stdin.lock().lines() in run_chat with dialoguer::Input using spawn_blocking for TTY, with a stdin fallback for non-TTY. Apply ColorfulTheme::default() to all dialoguer widgets in both arc-llm and arc-workflows ConsoleInterviewer. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/arc-llm/Cargo.toml | 1 + crates/arc-llm/src/cli.rs | 34 +++++++++++++------ .../arc-workflows/src/interviewer/console.rs | 11 +++--- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/crates/arc-llm/Cargo.toml b/crates/arc-llm/Cargo.toml index 9ac5dbb76..3e8266426 100644 --- a/crates/arc-llm/Cargo.toml +++ b/crates/arc-llm/Cargo.toml @@ -28,6 +28,7 @@ base64.workspace = true bytes.workspace = true tokio-util.workspace = true clap.workspace = true +dialoguer.workspace = true tracing.workspace = true arc-util = { path = "../arc-util" } diff --git a/crates/arc-llm/src/cli.rs b/crates/arc-llm/src/cli.rs index 98c8efa91..8b61d1de2 100644 --- a/crates/arc-llm/src/cli.rs +++ b/crates/arc-llm/src/cli.rs @@ -1,4 +1,7 @@ -use std::io::{self, BufRead, IsTerminal, Read, Write}; +use std::io::{self, IsTerminal, Read, Write}; + +use dialoguer::console::Term; +use dialoguer::theme::ColorfulTheme; use std::time::Duration; use anyhow::{bail, Context, Result}; @@ -221,17 +224,28 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> { eprintln!("Using model: {model_id}"); let mut messages: Vec = Vec::new(); - let stdin = io::stdin(); - let mut lines = stdin.lock().lines(); + let is_tty = io::stdin().is_terminal(); loop { - eprint!("> "); - io::stderr().flush()?; - - let line = match lines.next() { - Some(Ok(line)) => line, - Some(Err(e)) => return Err(e.into()), - None => break, // EOF + let line = if is_tty { + let result = tokio::task::spawn_blocking(|| { + dialoguer::Input::::with_theme(&ColorfulTheme::default()) + .with_prompt(">") + .interact_on(&Term::stderr()) + }) + .await?; + match result { + Ok(line) => line, + Err(_) => break, + } + } else { + eprint!("> "); + io::stderr().flush()?; + let mut buf = String::new(); + if io::stdin().read_line(&mut buf)? == 0 { + break; + } + buf.trim_end().to_string() }; let trimmed = line.trim(); diff --git a/crates/arc-workflows/src/interviewer/console.rs b/crates/arc-workflows/src/interviewer/console.rs index 71eeafb59..c642dfd3a 100644 --- a/crates/arc-workflows/src/interviewer/console.rs +++ b/crates/arc-workflows/src/interviewer/console.rs @@ -3,6 +3,7 @@ use std::io::IsTerminal; use arc_util::terminal::Styles; use async_trait::async_trait; use dialoguer::console::Term; +use dialoguer::theme::ColorfulTheme; use tokio::io::{AsyncBufReadExt, BufReader}; use super::{Answer, AnswerValue, Interviewer, Question, QuestionType}; @@ -69,7 +70,7 @@ fn ask_select_interactive(question: &Question) -> Answer { all_items.push("Other (free text)...".to_string()); } - let selection = dialoguer::Select::new() + let selection = dialoguer::Select::with_theme(&ColorfulTheme::default()) .with_prompt(&question.text) .items(&all_items) .default(0) @@ -78,7 +79,7 @@ fn ask_select_interactive(question: &Question) -> Answer { match selection { Ok(Some(idx)) if has_freeform && idx == question.options.len() => { // User chose the free-text option - dialoguer::Input::::new() + dialoguer::Input::::with_theme(&ColorfulTheme::default()) .with_prompt("Enter your response") .interact_on(&Term::stderr()) .map_or_else(|_| Answer::skipped(), Answer::text) @@ -103,7 +104,7 @@ fn ask_multi_select_interactive(question: &Question) -> Answer { .map(|opt| format!("{} - {}", opt.key, opt.label)) .collect(); - let selection = dialoguer::MultiSelect::new() + let selection = dialoguer::MultiSelect::with_theme(&ColorfulTheme::default()) .with_prompt(&question.text) .items(&items) .interact_on_opt(&Term::stderr()); @@ -124,7 +125,7 @@ fn ask_multi_select_interactive(question: &Question) -> Answer { /// Ask a yes/no or confirmation question using dialoguer's `Confirm` widget on a TTY. fn ask_confirm_interactive(question: &Question) -> Answer { - let confirmed = dialoguer::Confirm::new() + let confirmed = dialoguer::Confirm::with_theme(&ColorfulTheme::default()) .with_prompt(&question.text) .default(true) .interact_on_opt(&Term::stderr()); @@ -137,7 +138,7 @@ fn ask_confirm_interactive(question: &Question) -> Answer { /// Ask a freeform question using dialoguer's `Input` widget on a TTY. fn ask_freeform_interactive(question: &Question) -> Answer { - dialoguer::Input::::new() + dialoguer::Input::::with_theme(&ColorfulTheme::default()) .with_prompt(&question.text) .interact_on(&Term::stderr()) .map_or_else(|_| Answer::skipped(), Answer::text)