use crate::config::Config;
use crate::mcp::get_available_functions;
use crate::session::estimate_full_context_tokens;
use crate::session::history::{append_to_session_history_file, load_session_history_from_file};
use anyhow::Result;
use colored::*;
use reedline::{
default_emacs_keybindings, ColumnarMenu, EditCommand, Emacs, FileBackedHistory, History,
HistoryItem, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent,
ReedlineMenu, Signal,
};
use std::io::Write;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[derive(Debug)]
pub enum InputResult {
Text(String),
Cancelled,
Exit,
AddWithoutSending(String),
}
use crate::log_info;
fn display_shortcuts_help() {
println!();
println!(
"{}",
"╭─ Keyboard Shortcuts ─────────────────────────────────────╮".bright_cyan()
);
println!(
"{}",
"│ / - Commands (type /help for list) │".bright_black()
);
println!(
"{}",
"│ @ - Fuzzy file completion (e.g., @src/ma) │".bright_black()
);
println!(
"{}",
"│ Tab - Complete command/file │".bright_black()
);
println!(
"{}",
"│ Shift+Tab - Search history │".bright_black()
);
println!(
"{}",
"│ Ctrl+J - Insert newline (multi-line input) │".bright_black()
);
println!(
"{}",
"│ Ctrl+G - Add message without sending to API │".bright_black()
);
println!(
"{}",
"│ Ctrl+E - Accept hint / Exit reverse search │".bright_black()
);
println!(
"{}",
"│ Ctrl+R - Search command history │".bright_black()
);
println!(
"{}",
"│ Ctrl+C - Cancel current operation │".bright_black()
);
println!(
"{}",
"│ Ctrl+D - Exit session │".bright_black()
);
println!(
"{}",
"│ Ctrl+P/N - Navigate command history │".bright_black()
);
println!(
"{}",
"│ → - Accept hint (when at end of line) │".bright_black()
);
println!(
"{}",
"╰──────────────────────────────────────────────────────────╯".bright_cyan()
);
println!();
let _ = std::io::stdout().flush();
}
fn calculate_context_percentage(
current_context_tokens: u64,
max_session_tokens_threshold: usize,
) -> Option<f64> {
if max_session_tokens_threshold > 0 {
Some(
(current_context_tokens as f64 / max_session_tokens_threshold as f64 * 100.0)
.min(100.0),
)
} else {
None
}
}
fn add_completion_menu_keybindings(keybindings: &mut Keybindings) {
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Tab,
ReedlineEvent::UntilFound(vec![
ReedlineEvent::Menu("completion_menu".to_string()),
ReedlineEvent::MenuNext,
]),
);
}
pub async fn calculate_current_context_tokens(
messages: &[crate::session::Message],
config: &Config,
_role: &str,
) -> u64 {
let tools = get_available_functions(config).await;
estimate_full_context_tokens(messages, Some(&tools)) as u64
}
pub fn read_user_input(
estimated_cost: f64,
octomind_config: &Config,
role: &str,
current_context_tokens: u64,
max_session_tokens_threshold: usize,
session_id: &str,
inbox_pending: Arc<std::sync::Mutex<Option<String>>>,
) -> Result<InputResult> {
let mut history = FileBackedHistory::new(1000).expect("Error configuring history");
if let Ok(lines) = load_session_history_from_file(role) {
for line in lines {
let _ = history.save(HistoryItem {
id: None,
start_timestamp: None,
command_line: line,
session_id: None,
hostname: None,
cwd: None,
duration: None,
exit_status: None,
more_info: None,
});
}
}
let history = Box::new(history);
let mut keybindings = default_emacs_keybindings();
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('j'),
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('u'),
ReedlineEvent::Edit(vec![
EditCommand::CutFromLineStart,
EditCommand::CutToLineEnd,
]),
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('a'),
ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]),
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('e'),
ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd { select: false }]),
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('e'),
ReedlineEvent::HistoryHintComplete,
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('p'),
ReedlineEvent::UntilFound(vec![
ReedlineEvent::MenuPrevious,
ReedlineEvent::PreviousHistory,
]),
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('n'),
ReedlineEvent::UntilFound(vec![ReedlineEvent::MenuNext, ReedlineEvent::NextHistory]),
);
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Char('@'),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::InsertChar('@')]),
ReedlineEvent::Menu("completion_menu".to_string()),
]),
);
add_completion_menu_keybindings(&mut keybindings);
let config = Arc::new(octomind_config.clone());
let role_name = role.to_string();
let buffer_empty = Arc::new(AtomicBool::new(true));
let reverse_search_active = Arc::new(AtomicBool::new(false));
let hint_available = Arc::new(AtomicBool::new(false));
let line_state = Arc::new(std::sync::Mutex::new(
crate::session::chat::reedline_adapter::LineState::default(),
));
let edit_mode = Box::new(crate::session::chat::EmacsWithShortcutHelp::new(
Emacs::new(keybindings),
buffer_empty.clone(),
reverse_search_active.clone(),
hint_available.clone(),
line_state.clone(),
));
let completion_menu = Box::new(
ColumnarMenu::default()
.with_name("completion_menu")
.with_columns(4)
.with_column_padding(2),
);
let line_editor = Reedline::create()
.with_history(history)
.with_completer(Box::new(
crate::session::chat::reedline_adapter::ReedlineAdapter::new(
config.clone(),
role_name.clone(),
buffer_empty.clone(),
hint_available.clone(),
line_state.clone(),
),
))
.with_menu(ReedlineMenu::EngineCompleter(completion_menu))
.with_highlighter(Box::new(
crate::session::chat::reedline_adapter::ReedlineAdapter::new(
config.clone(),
role_name.clone(),
buffer_empty.clone(),
hint_available.clone(),
line_state.clone(),
),
))
.with_hinter(Box::new(
crate::session::chat::reedline_adapter::ReedlineAdapter::new(
config,
role_name.clone(),
buffer_empty,
hint_available,
line_state.clone(),
),
))
.with_quick_completions(true)
.use_bracketed_paste(true)
.with_edit_mode(edit_mode);
let printer = reedline::ExternalPrinter::<String>::new(5);
let sender = printer.sender();
let inbox_slot = inbox_pending;
std::thread::spawn(move || {
let mut notified = false;
loop {
std::thread::sleep(std::time::Duration::from_millis(100));
let preview = inbox_slot.lock().ok().and_then(|g| g.clone());
if let Some(preview) = preview {
if !notified {
let msg = format!(
"\x1b[33m📨 Inbox message received ({preview}) — press Enter to process\x1b[0m"
);
if sender.send(msg).is_err() {
break; }
notified = true;
}
} else {
notified = false;
}
}
});
let mut line_editor = line_editor
.with_external_printer(printer)
.with_poll_interval(std::time::Duration::from_millis(100));
let prompt_text = if estimated_cost > 0.0 {
let context_pct =
calculate_context_percentage(current_context_tokens, max_session_tokens_threshold);
if let Some(pct) = context_pct {
format!("[${:.2}|{:.1}%]", estimated_cost, pct)
} else {
format!("[${:.2}|∞]", estimated_cost)
}
} else if max_session_tokens_threshold > 0 {
let context_pct =
calculate_context_percentage(current_context_tokens, max_session_tokens_threshold);
if let Some(pct) = context_pct {
format!("[{:.1}%]", pct)
} else {
String::new()
}
} else {
String::new()
};
let prompt_left = if prompt_text.is_empty() {
String::new()
} else {
format!("{} ", prompt_text).bright_blue().to_string()
};
let prompt = crate::session::chat::ChatPrompt::new(
prompt_left,
"〉".bright_blue().to_string(),
reverse_search_active,
);
let line_state_for_check = line_state.clone();
loop {
match line_editor.read_line(&prompt) {
Ok(Signal::Success(line)) => {
if line == "__show_shortcuts__" {
display_shortcuts_help();
continue;
}
if line.trim() == "?" {
display_shortcuts_help();
continue;
}
let add_without_sending = if let Ok(mut state) = line_state_for_check.lock() {
let flag = state.add_without_sending;
state.add_without_sending = false; flag
} else {
false
};
let starts_with_whitespace = line.starts_with(char::is_whitespace);
if !starts_with_whitespace {
if let Err(e) = append_to_session_history_file(&role_name, &line) {
log_info!(
"Could not append to history file for role '{}': {}",
role,
e
);
}
}
return if add_without_sending {
Ok(InputResult::AddWithoutSending(line))
} else {
Ok(InputResult::Text(line))
};
}
Ok(Signal::CtrlC) => {
return Ok(InputResult::Cancelled);
}
Ok(Signal::CtrlD) => {
let resume_cmd = format!("octomind run --resume {}", session_id).bright_cyan();
println!("\nTo continue this session, run: {}", resume_cmd);
if let Ok(sessions_dir) = crate::session::get_sessions_dir() {
crate::log_debug!("Session files saved in: {}", sessions_dir.display());
}
crate::log_debug!("Session preserved for future reference.");
return Ok(InputResult::Exit);
}
Ok(_) => {
continue;
}
Err(err) => {
let msg = format!("{err:?}");
if msg.contains("cursor position could not be read")
|| msg.contains("not a terminal")
|| msg.contains("inappropriate ioctl")
{
return Ok(InputResult::Exit);
}
log_info!("Reedline error: {}", msg);
return Ok(InputResult::Text(String::new()));
}
}
}
}