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) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-03 15:40:24 -05:00
parent 859da7d035
commit d101892bdf
3 changed files with 31 additions and 15 deletions

View file

@ -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" }

View file

@ -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<Message> = 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::<String>::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();

View file

@ -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::<String>::new()
dialoguer::Input::<String>::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::<String>::new()
dialoguer::Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt(&question.text)
.interact_on(&Term::stderr())
.map_or_else(|_| Answer::skipped(), Answer::text)