refactor: eradicate deprecated tools (sticky_notes, pinned_files, context_workspaces, pr_checklist, preferences) and dead code
This commit is contained in:
1 parent
d80915635f
commit
79209da711
31 files changed
+1230
-3920
No files matched your search
+201
-213
@@ -1,6 +1,6 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::{ReadClipboardTool, WriteClipboardTool};
|
||||
use crate::tools::{ClipboardAction, ClipboardTool};
|
||||
use arboard::{Clipboard, ImageData};
|
||||
use async_trait::async_trait;
|
||||
use image::{ImageBuffer, imageops::FilterType};
|
||||
@@ -8,104 +8,6 @@ use serde_json::{Value, json};
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct WriteClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for WriteClipboardHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"write_clipboard"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<WriteClipboardTool>(
|
||||
"write_clipboard",
|
||||
"Write text, HTML, files, and/or images directly to the OS clipboard.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let tool_args: WriteClipboardTool = serde_json::from_value(args)
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Invalid args: {}", e)))?;
|
||||
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
let mut msgs = Vec::new();
|
||||
|
||||
if let Some(text) = &tool_args.text {
|
||||
let mut written = false;
|
||||
if let Ok(mut clipboard) = Clipboard::new() {
|
||||
for _ in 0..3 {
|
||||
if clipboard.set_text(text).is_ok() {
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
if written {
|
||||
msgs.push("Wrote text");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle arboard for image
|
||||
if let Some(image_path) = &tool_args.image_path {
|
||||
match image::open(image_path) {
|
||||
Ok(img) => {
|
||||
let img = if img.width() > 2048 || img.height() > 2048 {
|
||||
img.resize(2048, 2048, FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
};
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let raw_bytes = rgba.into_raw();
|
||||
let mut written = false;
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
for _ in 0..3 {
|
||||
let img_data = ImageData {
|
||||
width: w as usize,
|
||||
height: h as usize,
|
||||
bytes: Cow::Borrowed(&raw_bytes),
|
||||
};
|
||||
if clipboard.set_image(img_data).is_ok() {
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
if written {
|
||||
msgs.push("Wrote image");
|
||||
} else {
|
||||
return Err(crate::error::AppError::Internal(
|
||||
"Failed to write image to clipboard".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"Failed to open image {}: {}",
|
||||
image_path, e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if msgs.is_empty() {
|
||||
Ok("No valid clipboard data provided or failed to write.".to_string())
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Successfully populated clipboard: {}",
|
||||
msgs.join(", ")
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
state.record_activity("clipboard", &res, None);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_native_clipboard_text() -> Option<String> {
|
||||
for _ in 0..3 {
|
||||
@@ -121,7 +23,53 @@ pub fn get_native_clipboard_text() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_windows_png_clipboard_image() -> Option<image::DynamicImage> {
|
||||
use windows_sys::Win32::System::DataExchange::{
|
||||
CloseClipboard, GetClipboardData, IsClipboardFormatAvailable, OpenClipboard,
|
||||
RegisterClipboardFormatW,
|
||||
};
|
||||
use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock};
|
||||
let format_name: Vec<u16> = "PNG\0".encode_utf16().collect();
|
||||
unsafe {
|
||||
let format_id = RegisterClipboardFormatW(format_name.as_ptr());
|
||||
if format_id == 0 {
|
||||
return None;
|
||||
}
|
||||
for _ in 0..3 {
|
||||
if OpenClipboard(std::ptr::null_mut()) != 0 {
|
||||
let has_format = IsClipboardFormatAvailable(format_id) != 0;
|
||||
let mut img_opt = None;
|
||||
if has_format {
|
||||
let handle = GetClipboardData(format_id);
|
||||
if !handle.is_null() {
|
||||
let size = GlobalSize(handle as _);
|
||||
if size > 0 {
|
||||
let ptr = GlobalLock(handle as _);
|
||||
if !ptr.is_null() {
|
||||
let slice = std::slice::from_raw_parts(ptr as *const u8, size);
|
||||
img_opt = image::load_from_memory(slice).ok();
|
||||
GlobalUnlock(handle as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CloseClipboard();
|
||||
if img_opt.is_some() {
|
||||
return img_opt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(img) = get_windows_png_clipboard_image() {
|
||||
return Some(img);
|
||||
}
|
||||
for _ in 0..3 {
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
if let Ok(image_data) = clipboard.get_image() {
|
||||
@@ -139,105 +87,6 @@ pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
|
||||
None
|
||||
}
|
||||
|
||||
pub struct ReadClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadClipboardHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_clipboard"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadClipboardTool>(
|
||||
"read_clipboard",
|
||||
"Read the current OS clipboard. Extracts text, HTML, file paths, and/or an image if available.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let result =
|
||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
|
||||
let mut out = serde_json::Map::new();
|
||||
|
||||
if let Some(text) = get_native_clipboard_text() {
|
||||
out.insert("text".into(), json!(text));
|
||||
}
|
||||
|
||||
if let Some(dynamic_img) = get_native_clipboard_image() {
|
||||
let mut img = dynamic_img;
|
||||
let max_dim = 1024;
|
||||
if img.width() > max_dim || img.height() > max_dim {
|
||||
img = img.resize(max_dim, max_dim, FilterType::Lanczos3);
|
||||
}
|
||||
let rgb_img = img.into_rgb8();
|
||||
|
||||
let cache_dir = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".gemini/mcp_memory/clipboard");
|
||||
let _ = std::fs::create_dir_all(&cache_dir);
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
let file_path = cache_dir.join(format!("clipboard_{}.jpg", timestamp));
|
||||
|
||||
if rgb_img
|
||||
.save_with_format(&file_path, image::ImageFormat::Jpeg)
|
||||
.is_ok()
|
||||
{
|
||||
let path_str = file_path.to_string_lossy().to_string();
|
||||
out.insert("image_path".into(), json!(path_str));
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(rest) = path_str.strip_prefix("C:\\") {
|
||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||
} else if let Some(rest) = path_str.strip_prefix("c:\\") {
|
||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||
}
|
||||
|
||||
if let Ok(bytes) = std::fs::read(&file_path) {
|
||||
use base64::Engine;
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||
out.insert("image_base64".into(), json!(b64));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::Object(out))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
let mut final_obj = result;
|
||||
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
|
||||
let b64_str = b64.to_string();
|
||||
if let Some(obj) = final_obj.as_object_mut() {
|
||||
obj.remove("image_base64");
|
||||
}
|
||||
if state.ollama.is_available().await
|
||||
&& let Ok(analysis) = state
|
||||
.ollama
|
||||
.generate_vision(
|
||||
"Describe the content, text, or UI layout of this image concisely:",
|
||||
&b64_str,
|
||||
)
|
||||
.await
|
||||
&& let Some(obj) = final_obj.as_object_mut()
|
||||
{
|
||||
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
use crate::tools::{ClipboardAction, ClipboardTool};
|
||||
|
||||
pub struct ClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -257,23 +106,161 @@ impl McpTool for ClipboardHandler {
|
||||
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
match req.action {
|
||||
ClipboardAction::Read => {
|
||||
ReadClipboardHandler.execute(serde_json::json!({}), state).await
|
||||
let result =
|
||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
|
||||
let mut out = serde_json::Map::new();
|
||||
|
||||
if let Some(text) = get_native_clipboard_text() {
|
||||
out.insert("text".into(), json!(text));
|
||||
}
|
||||
|
||||
if let Some(dynamic_img) = get_native_clipboard_image() {
|
||||
let mut img = dynamic_img;
|
||||
let max_dim = 1024;
|
||||
if img.width() > max_dim || img.height() > max_dim {
|
||||
img = img.resize(max_dim, max_dim, FilterType::Lanczos3);
|
||||
}
|
||||
let rgb_img = img.into_rgb8();
|
||||
|
||||
let mut jpeg_bytes = std::io::Cursor::new(Vec::new());
|
||||
if rgb_img.write_to(&mut jpeg_bytes, image::ImageFormat::Jpeg).is_ok() {
|
||||
let bytes = jpeg_bytes.into_inner();
|
||||
use base64::Engine;
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||
out.insert("image_base64".into(), json!(b64));
|
||||
|
||||
let cache_dir = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".gemini/mcp_memory/clipboard");
|
||||
let _ = std::fs::create_dir_all(&cache_dir);
|
||||
let file_path = cache_dir.join("clipboard_latest.jpg");
|
||||
if std::fs::write(&file_path, &bytes).is_ok() {
|
||||
let path_str = file_path.to_string_lossy().to_string();
|
||||
out.insert("image_path".into(), json!(path_str));
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(rest) = path_str.strip_prefix("C:\\") {
|
||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||
} else if let Some(rest) = path_str.strip_prefix("c:\\") {
|
||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Value::Object(out))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
let mut final_obj = result;
|
||||
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
|
||||
let b64_str = b64.to_string();
|
||||
if let Some(obj) = final_obj.as_object_mut() {
|
||||
obj.remove("image_base64");
|
||||
}
|
||||
if state.ollama.is_available().await
|
||||
&& let Ok(analysis) = state
|
||||
.ollama
|
||||
.generate_vision(
|
||||
"Describe the content, text, or UI layout of this image concisely:",
|
||||
&b64_str,
|
||||
)
|
||||
.await
|
||||
&& let Some(obj) = final_obj.as_object_mut()
|
||||
{
|
||||
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
|
||||
}
|
||||
ClipboardAction::Write => {
|
||||
WriteClipboardHandler.execute(
|
||||
serde_json::json!({
|
||||
"text": req.text,
|
||||
"html": req.html,
|
||||
"files": req.files,
|
||||
"image_path": req.image_path
|
||||
}),
|
||||
state,
|
||||
).await
|
||||
let text_opt = req.text;
|
||||
let image_path_opt = req.image_path;
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
let mut msgs = Vec::new();
|
||||
|
||||
if let Some(text) = &text_opt {
|
||||
let mut written = false;
|
||||
if let Ok(mut clipboard) = Clipboard::new() {
|
||||
for _ in 0..3 {
|
||||
if clipboard.set_text(text).is_ok() {
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
if written {
|
||||
msgs.push("Wrote text");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(image_path) = &image_path_opt {
|
||||
match image::open(image_path) {
|
||||
Ok(img) => {
|
||||
let img = if img.width() > 2048 || img.height() > 2048 {
|
||||
img.resize(2048, 2048, FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
};
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let raw_bytes = rgba.into_raw();
|
||||
let mut written = false;
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
for _ in 0..3 {
|
||||
let img_data = ImageData {
|
||||
width: w as usize,
|
||||
height: h as usize,
|
||||
bytes: Cow::Borrowed(&raw_bytes),
|
||||
};
|
||||
if clipboard.set_image(img_data).is_ok() {
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
if written {
|
||||
msgs.push("Wrote image");
|
||||
} else {
|
||||
return Err(crate::error::AppError::Internal(
|
||||
"Failed to write image to clipboard".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"Failed to open image {}: {}",
|
||||
image_path, e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if msgs.is_empty() {
|
||||
Ok("No valid clipboard data provided or failed to write.".to_string())
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Successfully populated clipboard: {}",
|
||||
msgs.join(", ")
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
state.record_activity("clipboard", &res, None);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -286,9 +273,10 @@ mod tests {
|
||||
async fn test_write_clipboard() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = WriteClipboardHandler;
|
||||
let handler = ClipboardHandler;
|
||||
|
||||
let args = json!({
|
||||
"action": "write",
|
||||
"text": "test_text"
|
||||
});
|
||||
|
||||
@@ -310,10 +298,10 @@ mod tests {
|
||||
async fn test_read_clipboard() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = ReadClipboardHandler;
|
||||
let handler = ClipboardHandler;
|
||||
|
||||
let result = handler
|
||||
.execute(json!({}), state)
|
||||
.execute(json!({"action": "read"}), state)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
||||
.unwrap();
|
||||
@@ -327,10 +315,10 @@ mod tests {
|
||||
async fn test_read_clipboard_empty() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = ReadClipboardHandler;
|
||||
let handler = ClipboardHandler;
|
||||
|
||||
let result = handler
|
||||
.execute(serde_json::json!({}), state)
|
||||
.execute(serde_json::json!({"action": "read"}), state)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
||||
.unwrap();
|
||||
|
||||
Reference in new issue
Block a user