diff --git a/Cargo.toml b/Cargo.toml index d77e24fd..0f202d30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,6 @@ semver = "1.0" sha2 = "0.10" rand = "0.8" diff = "0.1" -arboard = "3.6" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp"] } tempfile = "3.13" url = "2.5" @@ -93,6 +92,9 @@ portable-pty = "0.9" libc = "0.2" vt100 = "0.15.2" +[target.'cfg(not(target_os = "android"))'.dependencies] +arboard = "3.6" + [dev-dependencies] tokio-test = "0.4" diff --git a/remote-client/package.json b/remote-client/package.json index a13f70b8..32576988 100644 --- a/remote-client/package.json +++ b/remote-client/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "dev": "bunx --bun vike dev --host --port 4271", - "build": "bunx --bun vike build", + "dev": "vike dev --host --port 4271", + "build": "vike build", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/src/app.rs b/src/app.rs index 1a1794f7..5fb822a1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5748,14 +5748,10 @@ impl App { self.input.attach_image(path); self.input.insert_str(" "); self.update_suggestions(); - push_toast(Toast::new( - "Attached image from clipboard", - ToastLevel::Info, - None, - )); + push_toast(Toast::new("Attached image", ToastLevel::Info, None)); } Err(err) => push_toast(Toast::new( - format!("Clipboard image paste failed: {}", err), + format!("Image attachment failed: {}", err), ToastLevel::Warning, None, )), @@ -9930,7 +9926,6 @@ impl App { home_animating || self.has_active_selection_edge_scroll() || self.is_streaming - || self.chat_state.chat.has_active_tool_messages() || self.has_active_retry_status() || self.compaction_receiver.is_some() || self.storage_receiver.is_some() @@ -9979,7 +9974,7 @@ impl App { } pub fn is_streaming_animation_only(&self) -> bool { - let streaming_only = (self.is_streaming || self.chat_state.chat.has_active_tool_messages()) + let streaming_only = self.is_streaming && self.base_focus != BaseFocus::Home && !self.has_active_selection_edge_scroll() && self.current_session_retry_status().is_none() @@ -14258,6 +14253,29 @@ mod tests { ); } + #[test] + fn stale_tool_messages_do_not_keep_the_event_loop_running() { + let mut app = test_app(); + app.base_focus = BaseFocus::Chat; + app.chat_state + .chat + .add_message(crate::session::types::Message::tool( + serde_json::json!({ + "name": "bash", + "status": "pending", + "args": { "command": "printf hello" }, + }) + .to_string(), + )); + + assert!(app.chat_state.chat.has_active_tool_messages()); + assert!(!app.is_streaming); + assert!( + !app.is_animation_running(), + "stale tool messages must not force continuous redraws after a stream finishes" + ); + } + #[test] fn messages_wait_until_streaming_finishes() { let input_type = parse_input("send another prompt"); diff --git a/src/utils/image_attachment.rs b/src/utils/image_attachment.rs index 0dd04906..a74988ec 100644 --- a/src/utils/image_attachment.rs +++ b/src/utils/image_attachment.rs @@ -353,6 +353,7 @@ pub fn image_paths_from_paste(text: &str) -> Vec { paths } +#[cfg(not(target_os = "android"))] pub fn paste_image_to_temp_png() -> Result { let mut clipboard = arboard::Clipboard::new().context("failed to access clipboard")?; @@ -384,6 +385,55 @@ pub fn paste_image_to_temp_png() -> Result { Ok(path) } +#[cfg(target_os = "android")] +pub fn paste_image_to_temp_png() -> Result { + let temp = tempfile::Builder::new() + .prefix("crabcode-attachment-") + .suffix(".tmp") + .tempfile() + .context("failed to create image picker output file")?; + let (_file, path) = temp + .keep() + .context("failed to persist image picker output file")?; + + let status = Command::new("termux-storage-get") + .arg(&path) + .status() + .context( + "failed to run termux-storage-get; install Termux:API and the termux-api package", + )?; + if !status.success() { + let _ = std::fs::remove_file(&path); + anyhow::bail!("Termux image picker was cancelled or failed") + } + + let Some(extension) = detected_image_extension(&path) else { + let _ = std::fs::remove_file(&path); + anyhow::bail!("Termux image picker did not return a supported image") + }; + + let image_path = path.with_extension(extension); + std::fs::rename(&path, &image_path).context("failed to save selected Termux image")?; + + Ok(image_path) +} + +#[cfg(target_os = "android")] +fn detected_image_extension(path: &Path) -> Option<&'static str> { + let bytes = std::fs::read(path).ok()?; + image_extension(image::guess_format(&bytes).ok()?) +} + +fn image_extension(format: ImageFormat) -> Option<&'static str> { + match format { + ImageFormat::Png => Some("png"), + ImageFormat::Jpeg => Some("jpg"), + ImageFormat::Gif => Some("gif"), + ImageFormat::WebP => Some("webp"), + _ => None, + } +} + pub fn open_path(path: &Path, config: &crate::config::ImagesConfig) -> Result<()> { if !path.exists() { return Err(anyhow!("image no longer exists: {}", path.display())); @@ -800,6 +850,15 @@ fn file_url_to_path(value: &str) -> Option { mod tests { use super::*; + #[test] + fn image_extension_matches_supported_image_formats() { + assert_eq!(image_extension(ImageFormat::Png), Some("png")); + assert_eq!(image_extension(ImageFormat::Jpeg), Some("jpg")); + assert_eq!(image_extension(ImageFormat::Gif), Some("gif")); + assert_eq!(image_extension(ImageFormat::WebP), Some("webp")); + assert_eq!(image_extension(ImageFormat::Bmp), None); + } + #[test] fn editor_location_args_use_zed_path_line_column_syntax() { let path = Path::new("/tmp/project/src/main.rs");