1use crate::runner::Runner;
4use anyhow::Result;
5use compact_str::CompactString;
6use futures_core::Stream;
7use futures_util::StreamExt;
8use rustyline::error::ReadlineError;
9use std::{io::Write, path::PathBuf, pin::pin};
10
11pub struct ChatRepl<R: Runner> {
13 runner: R,
14 agent: CompactString,
15 editor: rustyline::DefaultEditor,
16 history_path: Option<PathBuf>,
17}
18
19impl<R: Runner> ChatRepl<R> {
20 pub fn new(runner: R, agent: CompactString) -> Result<Self> {
22 let mut editor = rustyline::DefaultEditor::new()?;
23 let history_path = history_file_path();
24 if let Some(ref path) = history_path {
25 let _ = editor.load_history(path);
26 }
27 Ok(Self {
28 runner,
29 agent,
30 editor,
31 history_path,
32 })
33 }
34
35 pub async fn run(&mut self) -> Result<()> {
37 println!("Walrus chat (Ctrl+D to exit, Ctrl+C to cancel)");
38 println!("---");
39
40 loop {
41 match self.editor.readline("> ") {
42 Ok(line) => {
43 let line = line.trim().to_string();
44 if line.is_empty() {
45 continue;
46 }
47 let _ = self.editor.add_history_entry(&line);
48 let stream = self.runner.stream(&self.agent, &line);
49 stream_to_terminal(stream).await?;
50 }
51 Err(ReadlineError::Interrupted) => continue,
52 Err(ReadlineError::Eof) => break,
53 Err(e) => return Err(e.into()),
54 }
55 }
56
57 self.save_history();
58 Ok(())
59 }
60
61 fn save_history(&mut self) {
63 if let Some(ref path) = self.history_path {
64 if let Some(parent) = path.parent() {
65 let _ = std::fs::create_dir_all(parent);
66 }
67 let _ = self.editor.save_history(path);
68 }
69 }
70}
71
72fn history_file_path() -> Option<PathBuf> {
74 dirs::home_dir().map(|d| d.join(".walrus").join("history"))
75}
76
77async fn stream_to_terminal(stream: impl Stream<Item = Result<String>>) -> Result<()> {
81 let mut stream = pin!(stream);
82
83 loop {
84 tokio::select! {
85 chunk = stream.next() => {
86 match chunk {
87 Some(Ok(text)) => {
88 print!("{text}");
89 std::io::stdout().flush().ok();
90 }
91 Some(Err(e)) => {
92 eprintln!("\nError: {e}");
93 break;
94 }
95 None => break,
96 }
97 }
98 _ = tokio::signal::ctrl_c() => {
99 println!();
100 break;
101 }
102 }
103 }
104
105 println!();
106 Ok(())
107}