Skip to main content

oxios_cli/
interactive.rs

1//! Interactive readline loop using reedline.
2//!
3//! Runs the main REPL: read user input, dispatch meta-commands,
4//! forward messages to the channel, and display responses.
5
6use anyhow::Result;
7use reedline::{DefaultPrompt, DefaultPromptSegment, Reedline, Signal};
8
9use crate::channel::CliChannelHandle;
10use crate::commands::MetaCommand;
11
12/// The interactive read-eval-print loop.
13pub struct InteractiveLoop {
14    /// Handle to inject messages into the gateway.
15    handle: CliChannelHandle,
16    /// The reedline line editor.
17    editor: Reedline,
18    /// The prompt to display.
19    prompt: DefaultPrompt,
20}
21
22impl InteractiveLoop {
23    /// Create a new interactive loop.
24    pub fn new(handle: CliChannelHandle) -> Self {
25        let editor = Reedline::create();
26        let prompt = DefaultPrompt::default();
27
28        Self {
29            handle,
30            editor,
31            prompt,
32        }
33    }
34
35    /// Create with a custom prompt label.
36    pub fn with_prompt_label(handle: CliChannelHandle, left: &str) -> Self {
37        let editor = Reedline::create();
38        let prompt = DefaultPrompt::new(
39            DefaultPromptSegment::Basic(left.to_string()),
40            DefaultPromptSegment::Empty,
41        );
42
43        Self {
44            handle,
45            editor,
46            prompt,
47        }
48    }
49
50    /// Run the interactive loop until `.quit` or EOF.
51    ///
52    /// This is a blocking call. For use inside `tokio::task::spawn_blocking`
53    /// or a dedicated thread.
54    pub async fn run(&mut self) -> Result<()> {
55        println!("Oxios CLI — type .help for commands\n");
56
57        loop {
58            let signal = self.editor.read_line(&self.prompt);
59
60            match signal {
61                Ok(Signal::Success(line)) => {
62                    let trimmed = line.trim().to_string();
63                    if trimmed.is_empty() {
64                        continue;
65                    }
66
67                    // Check for meta-commands.
68                    if let Some(cmd) = MetaCommand::parse(&trimmed) {
69                        if self.handle_meta(cmd)? {
70                            break; // .quit
71                        }
72                        continue;
73                    }
74
75                    // Forward to the gateway.
76                    self.handle.send_user_message(trimmed).await?;
77                    self.handle.touch_session();
78
79                    // NOTE: The response will arrive asynchronously via the
80                    // Channel::send() implementation (printed to stdout).
81                    // In a future iteration, we could wait for a response here
82                    // for a synchronous feel, but for now the gateway routes
83                    // the response back through the channel.
84                }
85                Ok(Signal::CtrlC) => {
86                    println!("\n(Ctrl+C again to quit, or type .quit)");
87                }
88                Ok(Signal::CtrlD) => {
89                    println!("\nGoodbye!");
90                    break;
91                }
92                Err(err) => {
93                    tracing::error!("Readline error: {err}");
94                    break;
95                }
96            }
97        }
98
99        Ok(())
100    }
101
102    /// Handle a meta-command. Returns `true` if we should quit.
103    fn handle_meta(&self, cmd: MetaCommand) -> Result<bool> {
104        match cmd {
105            MetaCommand::Quit => {
106                println!("Goodbye!");
107                Ok(true)
108            }
109            MetaCommand::Help => {
110                print!("{}", MetaCommand::help_text());
111                Ok(false)
112            }
113            MetaCommand::Reset => {
114                self.handle.reset_session();
115                println!("Session reset.");
116                Ok(false)
117            }
118            MetaCommand::Model(Some(name)) => {
119                println!("Switching model to: {name}");
120                // TODO: wire to kernel model switching
121                Ok(false)
122            }
123            MetaCommand::Model(None) => {
124                println!("Current model: (default)");
125                Ok(false)
126            }
127            MetaCommand::Persona(Some(name)) => {
128                println!("Switching persona to: {name}");
129                // TODO: wire to kernel persona switching
130                Ok(false)
131            }
132            MetaCommand::Persona(None) => {
133                println!("Current persona: (default)");
134                Ok(false)
135            }
136            MetaCommand::Clear => {
137                // ANSI clear screen.
138                print!("\x1b[2J\x1b[H");
139                Ok(false)
140            }
141        }
142    }
143}