diff --git a/Cargo.lock b/Cargo.lock index ae5909e..7d45306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3495,7 +3495,7 @@ dependencies = [ [[package]] name = "routecode-cli" -version = "0.1.7" +version = "0.1.8" dependencies = [ "anyhow", "async-trait", @@ -3517,7 +3517,7 @@ dependencies = [ [[package]] name = "routecode-sdk" -version = "0.1.7" +version = "0.1.8" dependencies = [ "anyhow", "async-stream", diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index cf7106c..e5d950a 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "routecode-cli" -version = "0.1.8" +version = "0.1.9" edition = "2021" authors = ["SpeerX "] description = "CLI application for RouteCode" diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs index 4ed11ae..57e776e 100644 --- a/apps/cli/src/main.rs +++ b/apps/cli/src/main.rs @@ -187,14 +187,7 @@ async fn main() -> anyhow::Result<()> { let api_key = match api_key { Some(key) => key, - None => { - if cli.debug { - "your-api-key-here".to_string() - } else { - anyhow::bail!("API Key for {} not found. Set {}_API_KEY environment variable or configure it in ~/.routecode/config.json", - provider_name, provider_name.to_uppercase()); - } - } + None => "".to_string(), }; let provider = if provider_name == "vertex" { diff --git a/apps/cli/src/ui/logic.rs b/apps/cli/src/ui/logic.rs index 565ec75..476c802 100644 --- a/apps/cli/src/ui/logic.rs +++ b/apps/cli/src/ui/logic.rs @@ -135,10 +135,18 @@ pub async fn handle_command(app: &mut App, input: &str) { } app.history.push(Message::system(format!("Session resumed: {}", name))); app.screen = Screen::Session; + } else { + app.history.push(Message::system(format!("Error: Session '{}' not found", name))); } + } else { + app.history.push(Message::system("Usage: /resume ")); } } "/export" => { + if app.history.is_empty() { + app.history.push(Message::system("No messages to export in current session.")); + return; + } let name = args.first().map(|s| s.to_string()).unwrap_or_else(|| app.session_id.clone()); let session = routecode_sdk::utils::storage::Session { messages: app.history.clone(), diff --git a/apps/cli/src/ui/mod.rs b/apps/cli/src/ui/mod.rs index 7a1f57f..d1bf4b8 100644 --- a/apps/cli/src/ui/mod.rs +++ b/apps/cli/src/ui/mod.rs @@ -180,6 +180,7 @@ pub struct App { pub cached_thinking_hovered: bool, pub cached_total_height: usize, pub cached_text: Option>, + pub cached_layout: Vec<(usize, bool)>, pub pending_command_confirmation: Option<(String, String, ConfirmationSender)>, pub inputting_command_feedback: bool, pub show_user_msg_modal: Option, @@ -275,6 +276,7 @@ impl App { cached_thinking_hovered: false, cached_total_height: 0, cached_text: None, + cached_layout: Vec::new(), pending_command_confirmation: None, inputting_command_feedback: false, show_user_msg_modal: None, @@ -378,27 +380,8 @@ pub fn compute_thinking_hover(app: &App, size: ratatui::layout::Rect) -> bool { // The absolute visual row including scroll let target_visual_row = viewport_row as usize + app.history_scroll as usize; - // Build the history text and compute wrapping to find which logical line the target row maps to - let is_collapsed = app.collapse_thinking && !app.temp_expand_thinking; - let history_text = render_history(&app.history, is_collapsed, app.thinking_hover_rendered, None, 0); - let available_width = size.width.max(1) as usize; - let calc_width = (available_width as f32 * 0.95).floor().max(1.0) as usize; - - let mut cumulative_visual_row: usize = 0; - for line in &history_text.lines { - let line_width: usize = line.spans.iter().map(|s| unicode_width::UnicodeWidthStr::width(s.content.as_ref())).sum(); - let wrapped_height = if line_width == 0 { 1 } else { - line_width.div_ceil(calc_width) - }; - - // Check if target_visual_row falls within this logical line's visual rows - if target_visual_row >= cumulative_visual_row && target_visual_row < cumulative_visual_row + wrapped_height { - // Found the line - check if it's a thinking line - return line.spans.iter().any(|span| { - span.content.contains('\u{2502}') || span.content.contains('\u{2503}') || span.content.contains("Thinking...") - }); - } - cumulative_visual_row += wrapped_height; + if let Some(&(_, is_thinking)) = app.cached_layout.get(target_visual_row) { + return is_thinking; } false } @@ -421,29 +404,8 @@ pub fn compute_message_hover(app: &App, size: ratatui::layout::Rect) -> Option= cumulative_visual_row && target_visual_row < cumulative_visual_row + msg_height { - return Some(msg_idx); - } - cumulative_visual_row += msg_height; + if let Some(&(msg_idx, _)) = app.cached_layout.get(target_visual_row) { + return Some(msg_idx); } None @@ -889,6 +851,31 @@ async fn handle_key_event( app.history.push(Message::system(format!("Queued: {}", input_text))); app.input = TextArea::default(); } else { + let provider_id = &app.current_provider_id; + let env_key = format!("{}_API_KEY", provider_id.to_uppercase().replace("-", "_")); + let mut api_key = std::env::var(&env_key).ok(); + if api_key.is_none() && provider_id.starts_with("cloudflare") { + api_key = std::env::var("CLOUDFLARE_API_KEY").ok(); + } + if api_key.is_none() { + let config = app.orchestrator.config.lock().await; + api_key = config.api_keys.get(provider_id).cloned(); + } + + let has_valid_key = api_key.map_or(false, |k| !k.trim().is_empty()); + + if !has_valid_key && provider_id != "opencode-zen" && provider_id != "opencode-go" { + app.history.push(Message::system(format!("No API key found for {}. Please enter it to continue.", provider_id))); + app.show_provider_menu = true; + if let Some(pos) = PROVIDERS.iter().position(|p| p.id == *provider_id) { + app.menu_state.select(Some(pos)); + } else { + app.menu_state.select(Some(0)); + } + app.input = TextArea::default(); + return Ok(KeyEventResult::Continue); + } + app.history.push(Message::user(input_text.clone())); app.prompt_history.push(input_text.clone()); app.prompt_history.truncate(100); diff --git a/apps/cli/src/ui/session.rs b/apps/cli/src/ui/session.rs index 34e4f0c..abc9493 100644 --- a/apps/cli/src/ui/session.rs +++ b/apps/cli/src/ui/session.rs @@ -35,20 +35,37 @@ pub fn ui_session(f: &mut Frame, app: &mut App, area: Rect) -> Rect { if needs_rebuild && (throttle_ok || !app.is_generating) { app.render_dirty = false; app.last_cache_update = std::time::Instant::now(); - let history = render_history(&app.history, is_collapsed, thinking_hovered, hovered_msg_idx, 0); - - // 1. Auto-scroll logic + let mut lines = Vec::new(); let mut total_height: usize = 0; + let mut layout = Vec::new(); let available_width = chunks[0].width.max(1) as usize; - for line in &history.lines { - let line_width: usize = line.spans.iter().map(|s| s.content.width()).sum(); - let wrapped_height = if line_width == 0 { 1 } else { - // Use a slightly smaller width for calculation to account for word wrapping - let calc_width = (available_width as f32 * 0.95).floor() as usize; - (line_width + calc_width - 1) / calc_width.max(1) - }; - total_height += wrapped_height; + let calc_width = (available_width as f32 * 0.95).floor().max(1.0) as usize; + + for (msg_idx, m) in app.history.iter().enumerate() { + let msg_slice = std::slice::from_ref(m); + let msg_text = render_history(msg_slice, is_collapsed, thinking_hovered, hovered_msg_idx, msg_idx); + + for line in msg_text.lines { + let line_width: usize = line.spans.iter().map(|s| s.content.width()).sum(); + let wrapped_height = if line_width == 0 { 1 } else { + (line_width + calc_width - 1) / calc_width.max(1) + }; + + let is_thinking = line.spans.iter().any(|span| { + span.content.contains('\u{2502}') || span.content.contains('\u{2503}') || span.content.contains("Thinking...") + }); + + for _ in 0..wrapped_height { + layout.push((msg_idx, is_thinking)); + } + + total_height += wrapped_height; + lines.push(line); + } } + + let history = Text::from(lines); + app.cached_layout = layout; // Safety buffer total_height += 2; diff --git a/libs/sdk/Cargo.toml b/libs/sdk/Cargo.toml index ef8ab15..a51221d 100644 --- a/libs/sdk/Cargo.toml +++ b/libs/sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "routecode-sdk" -version = "0.1.8" +version = "0.1.9" edition = "2021" authors = ["SpeerX "] description = "Core logic for RouteCode"