feat(repl): use rustyline for UTF-8 input and history support

Replace stdin().read_line() with rustyline::DefaultEditor to improve
interactive CLI experience:

- Proper UTF-8 input support
- Command history with up/down arrow keys
- Better error handling for Ctrl-C/Ctrl-D
- Improved user confirmation prompts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
killf 2026-02-26 14:43:57 +08:00 committed by Argenis
parent e2f23f45eb
commit 08f7f355d8
3 changed files with 103 additions and 14 deletions

89
Cargo.lock generated
View File

@ -970,6 +970,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clipboard-win"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
[[package]]
name = "cmake"
version = "0.1.57"
@ -1730,6 +1739,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "enumflags2"
version = "0.7.12"
@ -1791,6 +1806,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "error-code"
version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]]
name = "esp-idf-part"
version = "0.6.0"
@ -1969,6 +1990,17 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fd-lock"
version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
@ -2461,6 +2493,15 @@ dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "hostname"
version = "0.4.2"
@ -3846,6 +3887,15 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nibble_vec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.26.4"
@ -5012,6 +5062,16 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -5601,6 +5661,28 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rustyline"
version = "17.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564"
dependencies = [
"bitflags 2.11.0",
"cfg-if",
"clipboard-win",
"fd-lock",
"home",
"libc",
"log",
"memchr",
"nix 0.30.1",
"radix_trie",
"unicode-segmentation",
"unicode-width 0.2.2",
"utf8parse",
"windows-sys 0.60.2",
]
[[package]]
name = "ruzstd"
version = "0.8.2"
@ -6995,6 +7077,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
@ -8393,6 +8481,7 @@ dependencies = [
"rust-embed",
"rustls",
"rustls-pki-types",
"rustyline",
"schemars",
"scopeguard",
"serde",

View File

@ -111,6 +111,7 @@ cron = "0.15"
# Interactive CLI prompts
dialoguer = { version = "0.12", features = ["fuzzy-select"] }
rustyline = "17.0"
console = "0.16"
# Hardware discovery (device path globbing)

View File

@ -12,6 +12,7 @@ use crate::tools::{self, Tool};
use crate::util::truncate_with_ellipsis;
use anyhow::Result;
use regex::{Regex, RegexSet};
use rustyline::error::ReadlineError;
use std::collections::{BTreeSet, HashSet};
use std::fmt::Write;
use std::io::Write as _;
@ -1744,25 +1745,26 @@ pub async fn run(
// Persistent conversation history across turns
let mut history = vec![ChatMessage::system(&system_prompt)];
// Reusable readline editor for UTF-8 input support
let mut rl = rustyline::DefaultEditor::new()?;
loop {
print!("> ");
let _ = std::io::stdout().flush();
let mut input = String::new();
match std::io::stdin().read_line(&mut input) {
Ok(0) => break,
Ok(_) => {}
let input = match rl.readline("> ") {
Ok(line) => line,
Err(ReadlineError::Interrupted | ReadlineError::Eof) => {
break;
}
Err(e) => {
eprintln!("\nError reading input: {e}\n");
break;
}
}
};
let user_input = input.trim().to_string();
if user_input.is_empty() {
continue;
}
rl.add_history_entry(&input)?;
match user_input.as_str() {
"/quit" | "/exit" => break,
"/help" => {
@ -1777,18 +1779,15 @@ pub async fn run(
"This will clear the current conversation and delete all session memory."
);
println!("Core memories (long-term facts/preferences) will be preserved.");
print!("Continue? [y/N] ");
let _ = std::io::stdout().flush();
let confirm = rl.readline("Continue? [y/N] ").unwrap_or_default();
let mut confirm = String::new();
if std::io::stdin().read_line(&mut confirm).is_err() {
continue;
}
if !matches!(confirm.trim().to_lowercase().as_str(), "y" | "yes") {
println!("Cancelled.\n");
continue;
}
// Ensure prior prompts are not navigable after reset.
rl.clear_history()?;
history.clear();
history.push(ChatMessage::system(&system_prompt));
// Clear conversation and daily memory