A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add slash commands and placeholder hint to REPL (#42)

* Add slash commands and placeholder hint to REPL

Implement slash commands (/help, /?, /bye) and add rustyline-based
placeholder text "Send a message (/? for help)" for better UX.

* removed exit command from repl

* Refactor REPL: upgrade rustyline and improve slash commands

- Upgrade rustyline from 14.0 to 17.0
- Extract slash command handling to separate function
- Rename editor variable for clarity (rl -> editor)
- Remove >> prefix from CLI output
- Add else block to break loop when relays exhausted
- Update no reply message to be more helpful

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

authored by

Kshitij Taneja
Claude Opus 4.5
and committed by
GitHub
b09d0c6f fe14a70e

+249 -36
+107
Cargo.lock
··· 117 117 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 118 118 119 119 [[package]] 120 + name = "cfg_aliases" 121 + version = "0.2.1" 122 + source = "registry+https://github.com/rust-lang/crates.io-index" 123 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 124 + 125 + [[package]] 120 126 name = "clap" 121 127 version = "4.5.54" 122 128 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 155 161 version = "0.7.6" 156 162 source = "registry+https://github.com/rust-lang/crates.io-index" 157 163 checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" 164 + 165 + [[package]] 166 + name = "clipboard-win" 167 + version = "5.4.1" 168 + source = "registry+https://github.com/rust-lang/crates.io-index" 169 + checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" 170 + dependencies = [ 171 + "error-code", 172 + ] 158 173 159 174 [[package]] 160 175 name = "colorchoice" ··· 248 263 ] 249 264 250 265 [[package]] 266 + name = "endian-type" 267 + version = "0.1.2" 268 + source = "registry+https://github.com/rust-lang/crates.io-index" 269 + checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 270 + 271 + [[package]] 251 272 name = "equivalent" 252 273 version = "1.0.2" 253 274 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 262 283 "libc", 263 284 "windows-sys 0.61.2", 264 285 ] 286 + 287 + [[package]] 288 + name = "error-code" 289 + version = "3.3.2" 290 + source = "registry+https://github.com/rust-lang/crates.io-index" 291 + checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" 265 292 266 293 [[package]] 267 294 name = "fastrand" ··· 270 297 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 271 298 272 299 [[package]] 300 + name = "fd-lock" 301 + version = "4.0.4" 302 + source = "registry+https://github.com/rust-lang/crates.io-index" 303 + checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" 304 + dependencies = [ 305 + "cfg-if", 306 + "rustix", 307 + "windows-sys 0.59.0", 308 + ] 309 + 310 + [[package]] 273 311 name = "find-msvc-tools" 274 312 version = "0.1.6" 275 313 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 486 524 "tokio", 487 525 "ureq", 488 526 "windows-sys 0.60.2", 527 + ] 528 + 529 + [[package]] 530 + name = "home" 531 + version = "0.5.12" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" 534 + dependencies = [ 535 + "windows-sys 0.61.2", 489 536 ] 490 537 491 538 [[package]] ··· 855 902 ] 856 903 857 904 [[package]] 905 + name = "nibble_vec" 906 + version = "0.1.0" 907 + source = "registry+https://github.com/rust-lang/crates.io-index" 908 + checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 909 + dependencies = [ 910 + "smallvec", 911 + ] 912 + 913 + [[package]] 914 + name = "nix" 915 + version = "0.30.1" 916 + source = "registry+https://github.com/rust-lang/crates.io-index" 917 + checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 918 + dependencies = [ 919 + "bitflags", 920 + "cfg-if", 921 + "cfg_aliases", 922 + "libc", 923 + ] 924 + 925 + [[package]] 858 926 name = "nom" 859 927 version = "8.0.0" 860 928 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1020 1088 checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1021 1089 1022 1090 [[package]] 1091 + name = "radix_trie" 1092 + version = "0.2.1" 1093 + source = "registry+https://github.com/rust-lang/crates.io-index" 1094 + checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 1095 + dependencies = [ 1096 + "endian-type", 1097 + "nibble_vec", 1098 + ] 1099 + 1100 + [[package]] 1023 1101 name = "rand" 1024 1102 version = "0.9.2" 1025 1103 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1172 1250 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1173 1251 1174 1252 [[package]] 1253 + name = "rustyline" 1254 + version = "17.0.2" 1255 + source = "registry+https://github.com/rust-lang/crates.io-index" 1256 + checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" 1257 + dependencies = [ 1258 + "bitflags", 1259 + "cfg-if", 1260 + "clipboard-win", 1261 + "fd-lock", 1262 + "home", 1263 + "libc", 1264 + "log", 1265 + "memchr", 1266 + "nix", 1267 + "radix_trie", 1268 + "unicode-segmentation", 1269 + "unicode-width", 1270 + "utf8parse", 1271 + "windows-sys 0.60.2", 1272 + ] 1273 + 1274 + [[package]] 1175 1275 name = "ryu" 1176 1276 version = "1.0.22" 1177 1277 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1429 1529 "hf-hub", 1430 1530 "owo-colors", 1431 1531 "reqwest", 1532 + "rustyline", 1432 1533 "serde", 1433 1534 "serde_json", 1434 1535 "tilekit", ··· 1579 1680 version = "1.0.22" 1580 1681 source = "registry+https://github.com/rust-lang/crates.io-index" 1581 1682 checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 1683 + 1684 + [[package]] 1685 + name = "unicode-segmentation" 1686 + version = "1.12.0" 1687 + source = "registry+https://github.com/rust-lang/crates.io-index" 1688 + checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1582 1689 1583 1690 [[package]] 1584 1691 name = "unicode-width"
+1
tiles/Cargo.toml
··· 14 14 owo-colors = "4" 15 15 futures-util = "0.3" 16 16 hf-hub = {version = "0.4", features = ["tokio"]} 17 + rustyline = "17.0"
+141 -36
tiles/src/runtime/mlx.rs
··· 4 4 use futures_util::StreamExt; 5 5 use owo_colors::OwoColorize; 6 6 use reqwest::{Client, StatusCode}; 7 + use rustyline::completion::Completer; 8 + use rustyline::highlight::Highlighter; 9 + use rustyline::hint::Hinter; 10 + use rustyline::history::DefaultHistory; 11 + use rustyline::validate::Validator; 12 + use rustyline::{Config, Editor, Helper}; 7 13 use serde_json::{Value, json}; 8 14 use std::fs::File; 9 - use std::io::Write; 10 15 use std::path::PathBuf; 16 + use std::process::Command; 11 17 use std::process::Stdio; 12 18 use std::time::Duration; 13 19 use std::{env, fs}; 14 - use std::{io, process::Command}; 15 20 use tilekit::modelfile::Modelfile; 16 21 use tokio::time::sleep; 17 22 pub struct MLXRuntime {} ··· 170 175 } 171 176 } 172 177 178 + struct TilesHinter; 179 + 180 + impl Hinter for TilesHinter { 181 + type Hint = String; 182 + 183 + fn hint(&self, line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option<Self::Hint> { 184 + if line.is_empty() { 185 + Some("Send a message (/? for help)".to_string()) 186 + } else { 187 + None 188 + } 189 + } 190 + } 191 + 192 + impl Completer for TilesHinter { 193 + type Candidate = String; 194 + } 195 + 196 + impl Highlighter for TilesHinter { 197 + fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> { 198 + std::borrow::Cow::Owned(format!("\x1b[2m{}\x1b[0m", hint)) 199 + } 200 + } 201 + 202 + impl Validator for TilesHinter {} 203 + 204 + impl Helper for TilesHinter {} 205 + 206 + enum SlashCommand { 207 + Continue, 208 + Exit, 209 + NotACommand, 210 + } 211 + 212 + fn handle_slash_command(input: &str, modelname: &str) -> SlashCommand { 213 + if let Some(cmd) = input.strip_prefix('/') { 214 + match cmd { 215 + "help" | "?" => { 216 + show_help(modelname); 217 + SlashCommand::Continue 218 + } 219 + "bye" => SlashCommand::Exit, 220 + "" => { 221 + println!("Empty command. Type /help for available commands."); 222 + SlashCommand::Continue 223 + } 224 + _ => { 225 + println!( 226 + "Unknown command: /{}. Type /help for available commands.", 227 + cmd 228 + ); 229 + SlashCommand::Continue 230 + } 231 + } 232 + } else { 233 + SlashCommand::NotACommand 234 + } 235 + } 236 + 237 + fn show_help(model_name: &str) { 238 + println!("\n=== Tiles REPL Help ===\n"); 239 + 240 + println!("Available Commands:"); 241 + println!(" /? Show this help message"); 242 + println!(" /help Show this help message"); 243 + println!(" /bye Exit the REPL"); 244 + println!(); 245 + 246 + println!("Current Model:"); 247 + println!(" {}", model_name); 248 + println!(); 249 + 250 + println!("Usage Tips:"); 251 + println!(" - Type your questions or prompts directly"); 252 + println!(" - Model outputs <think>, <python>, and <reply> tags"); 253 + println!(" - Only <reply> content is shown as final output"); 254 + println!(); 255 + } 256 + 173 257 async fn run_model_with_server( 174 258 mlx_runtime: &MLXRuntime, 175 259 modelfile: Modelfile, ··· 192 276 } 193 277 194 278 async fn start_repl(mlx_runtime: &MLXRuntime, modelname: &str, run_args: &RunArgs) { 195 - let stdin = io::stdin(); 196 - let mut stdout = io::stdout(); 197 279 println!("Running in interactive mode"); 280 + 281 + // Setup rustyline editor with hint support 282 + let config = Config::builder().auto_add_history(true).build(); 283 + let mut editor = Editor::<TilesHinter, DefaultHistory>::with_config(config).unwrap(); 284 + editor.set_helper(Some(TilesHinter)); 285 + 198 286 // TODO: Handle "enter" key press or any key press when repl is processing an input 199 287 loop { 200 - print!(">> "); 201 - stdout.flush().unwrap(); 202 - let mut input = String::new(); 203 - stdin.read_line(&mut input).unwrap(); 204 - let input = input.trim(); 205 - match input { 206 - "exit" => { 288 + let readline = editor.readline(">>> "); 289 + let input = match readline { 290 + Ok(line) => line.trim().to_string(), 291 + Err(_) => { 292 + // User pressed Ctrl+C or Ctrl+D 207 293 println!("Exiting interactive mode"); 208 294 if !cfg!(debug_assertions) { 209 295 let _res = mlx_runtime.stop_server_daemon().await; 210 296 } 211 297 break; 212 298 } 213 - _ => { 214 - let mut remaining_count = run_args.relay_count; 215 - let mut g_reply: String = "".to_owned(); 216 - let mut python_code: String = "".to_owned(); 217 - loop { 218 - if remaining_count > 0 { 219 - let chat_start = remaining_count == run_args.relay_count; 220 - if let Ok(response) = chat(input, modelname, chat_start, &python_code).await 221 - { 222 - if response.reply.is_empty() { 223 - if !response.code.is_empty() { 224 - python_code = response.code; 225 - } 226 - remaining_count -= 1; 227 - } else { 228 - g_reply = response.reply.clone(); 229 - println!("\n>> {}", response.reply.trim()); 230 - break; 231 - } 232 - } else { 233 - println!("\n>> failed to respond"); 234 - break; 299 + }; 300 + 301 + // Handle slash commands 302 + match handle_slash_command(&input, modelname) { 303 + SlashCommand::Continue => continue, 304 + SlashCommand::Exit => { 305 + println!("Exiting interactive mode"); 306 + if !cfg!(debug_assertions) { 307 + let _res = mlx_runtime.stop_server_daemon().await; 308 + } 309 + break; 310 + } 311 + SlashCommand::NotACommand => {} 312 + } 313 + 314 + // Skip empty input 315 + if input.is_empty() { 316 + continue; 317 + } 318 + 319 + // Send to model 320 + let mut remaining_count = run_args.relay_count; 321 + let mut g_reply: String = "".to_owned(); 322 + let mut python_code: String = "".to_owned(); 323 + loop { 324 + if remaining_count > 0 { 325 + let chat_start = remaining_count == run_args.relay_count; 326 + if let Ok(response) = chat(&input, modelname, chat_start, &python_code).await { 327 + if response.reply.is_empty() { 328 + if !response.code.is_empty() { 329 + python_code = response.code; 235 330 } 331 + remaining_count -= 1; 332 + } else { 333 + g_reply = response.reply.clone(); 334 + println!("\n{}", response.reply.trim()); 335 + break; 236 336 } 337 + } else { 338 + println!("\nFailed to respond"); 339 + break; 237 340 } 238 - if g_reply.is_empty() { 239 - println!(">> No reply") 240 - } 341 + } else { 342 + break; 241 343 } 344 + } 345 + if g_reply.is_empty() { 346 + println!("\nNo reply, try another prompt"); 242 347 } 243 348 } 244 349 }