thinning message highlighted
This commit is contained in:
@@ -169,7 +169,7 @@ use tracing::{error, info};
|
|||||||
use g3_core::error_handling::{classify_error, ErrorType, RecoverableError};
|
use g3_core::error_handling::{classify_error, ErrorType, RecoverableError};
|
||||||
mod retro_tui;
|
mod retro_tui;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod tui;
|
pub mod tui;
|
||||||
mod ui_writer_impl;
|
mod ui_writer_impl;
|
||||||
use retro_tui::RetroTui;
|
use retro_tui::RetroTui;
|
||||||
use theme::ColorTheme;
|
use theme::ColorTheme;
|
||||||
@@ -1099,9 +1099,8 @@ async fn run_interactive<W: UiWriter>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
"/thinnify" => {
|
"/thinnify" => {
|
||||||
output.print("🔧 Triggering manual context thinning...");
|
|
||||||
let summary = agent.force_thin();
|
let summary = agent.force_thin();
|
||||||
output.print(&summary);
|
output.print_context_thinning(&summary);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
"/readme" => {
|
"/readme" => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crossterm::style::Color;
|
use crossterm::style::Color;
|
||||||
use crossterm::style::{SetForegroundColor, ResetColor};
|
use crossterm::style::{SetForegroundColor, ResetColor};
|
||||||
|
use std::io::{self, Write};
|
||||||
use termimad::MadSkin;
|
use termimad::MadSkin;
|
||||||
|
|
||||||
/// Simple output handler with markdown support
|
/// Simple output handler with markdown support
|
||||||
@@ -93,6 +94,37 @@ impl SimpleOutput {
|
|||||||
print!("{}", ResetColor);
|
print!("{}", ResetColor);
|
||||||
println!(" {:.1}% | {}/{} tokens", percentage, used, total);
|
println!(" {:.1}% | {}/{} tokens", percentage, used, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn print_context_thinning(&self, message: &str) {
|
||||||
|
// Animated highlight for context thinning
|
||||||
|
// Use bright cyan/green with a quick flash animation
|
||||||
|
|
||||||
|
// Flash animation: print with bright background, then normal
|
||||||
|
let frames = vec![
|
||||||
|
"\x1b[1;97;46m", // Frame 1: Bold white on cyan background
|
||||||
|
"\x1b[1;97;42m", // Frame 2: Bold white on green background
|
||||||
|
"\x1b[1;96;40m", // Frame 3: Bold cyan on black background
|
||||||
|
];
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Quick flash animation
|
||||||
|
for frame in &frames {
|
||||||
|
print!("\r{} ✨ {} ✨\x1b[0m", frame, message);
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(80));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final display with bright cyan and sparkle emojis
|
||||||
|
print!("\r\x1b[1;96m✨ {} ✨\x1b[0m", message);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Add a subtle "success" indicator line
|
||||||
|
println!("\x1b[2;36m └─ Context optimized successfully\x1b[0m");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -104,6 +104,37 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
println!("{}", message);
|
println!("{}", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_context_thinning(&self, message: &str) {
|
||||||
|
// Animated highlight for context thinning
|
||||||
|
// Use bright cyan/green with a quick flash animation
|
||||||
|
|
||||||
|
// Flash animation: print with bright background, then normal
|
||||||
|
let frames = vec![
|
||||||
|
"\x1b[1;97;46m", // Frame 1: Bold white on cyan background
|
||||||
|
"\x1b[1;97;42m", // Frame 2: Bold white on green background
|
||||||
|
"\x1b[1;96;40m", // Frame 3: Bold cyan on black background
|
||||||
|
];
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Quick flash animation
|
||||||
|
for frame in &frames {
|
||||||
|
print!("\r{} ✨ {} ✨\x1b[0m", frame, message);
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(80));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final display with bright cyan and sparkle emojis
|
||||||
|
print!("\r\x1b[1;96m✨ {} ✨\x1b[0m", message);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Add a subtle "success" indicator line
|
||||||
|
println!("\x1b[2;36m └─ Context optimized successfully\x1b[0m");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
}
|
||||||
|
|
||||||
fn print_tool_header(&self, tool_name: &str) {
|
fn print_tool_header(&self, tool_name: &str) {
|
||||||
// Store the tool name and clear args for collection
|
// Store the tool name and clear args for collection
|
||||||
*self.current_tool_name.lock().unwrap() = Some(tool_name.to_string());
|
*self.current_tool_name.lock().unwrap() = Some(tool_name.to_string());
|
||||||
@@ -360,6 +391,19 @@ impl UiWriter for RetroTuiWriter {
|
|||||||
self.tui.output(message);
|
self.tui.output(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_context_thinning(&self, message: &str) {
|
||||||
|
// For TUI, we'll use a highlighted output with special formatting
|
||||||
|
// The TUI will handle the visual presentation
|
||||||
|
|
||||||
|
// Add visual separators and emphasis
|
||||||
|
self.tui.output("");
|
||||||
|
self.tui.output("═══════════════════════════════════════════════════════════");
|
||||||
|
self.tui.output(&format!("✨ {} ✨", message));
|
||||||
|
self.tui.output(" └─ Context optimized successfully");
|
||||||
|
self.tui.output("═══════════════════════════════════════════════════════════");
|
||||||
|
self.tui.output("");
|
||||||
|
}
|
||||||
|
|
||||||
fn print_tool_header(&self, tool_name: &str) {
|
fn print_tool_header(&self, tool_name: &str) {
|
||||||
// Start collecting tool output
|
// Start collecting tool output
|
||||||
*self.current_tool_start.lock().unwrap() = Some(Instant::now());
|
*self.current_tool_start.lock().unwrap() = Some(Instant::now());
|
||||||
|
|||||||
@@ -1,425 +0,0 @@
|
|||||||
use crate::{ComputerController, types::*};
|
|
||||||
use anyhow::Result;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use core_graphics::display::CGPoint;
|
|
||||||
use core_graphics::event::{CGEvent, CGEventType, CGMouseButton, CGEventTapLocation};
|
|
||||||
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
|
|
||||||
use std::path::Path;
|
|
||||||
use tesseract::Tesseract;
|
|
||||||
|
|
||||||
// MacOSController doesn't store CGEventSource to avoid Send/Sync issues
|
|
||||||
// We create it fresh for each operation
|
|
||||||
pub struct MacOSController {
|
|
||||||
// Empty struct - event source created per operation
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MacOSController {
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
// Test that we can create an event source
|
|
||||||
let _event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to create event source. Make sure Accessibility permissions are granted."))?;
|
|
||||||
Ok(Self {})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key_to_keycode(&self, key: &str) -> Result<u16> {
|
|
||||||
// Map key names to macOS keycodes
|
|
||||||
let keycode = match key.to_lowercase().as_str() {
|
|
||||||
"return" | "enter" => 36,
|
|
||||||
"tab" => 48,
|
|
||||||
"space" => 49,
|
|
||||||
"delete" | "backspace" => 51,
|
|
||||||
"escape" | "esc" => 53,
|
|
||||||
"command" | "cmd" => 55,
|
|
||||||
"shift" => 56,
|
|
||||||
"capslock" => 57,
|
|
||||||
"option" | "alt" => 58,
|
|
||||||
"control" | "ctrl" => 59,
|
|
||||||
"left" => 123,
|
|
||||||
"right" => 124,
|
|
||||||
"down" => 125,
|
|
||||||
"up" => 126,
|
|
||||||
_ => anyhow::bail!("Unknown key: {}", key),
|
|
||||||
};
|
|
||||||
Ok(keycode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ComputerController for MacOSController {
|
|
||||||
async fn move_mouse(&self, x: i32, y: i32) -> Result<()> {
|
|
||||||
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to create event source"))?;
|
|
||||||
let point = CGPoint::new(x as f64, y as f64);
|
|
||||||
let event = CGEvent::new_mouse_event(
|
|
||||||
event_source,
|
|
||||||
CGEventType::MouseMoved,
|
|
||||||
point,
|
|
||||||
CGMouseButton::Left,
|
|
||||||
).map_err(|_| anyhow::anyhow!("Failed to create mouse move event"))?;
|
|
||||||
|
|
||||||
event.post(CGEventTapLocation::HID);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn click(&self, button: MouseButton) -> Result<()> {
|
|
||||||
let (cg_button, down_type, up_type) = match button {
|
|
||||||
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, CGEventType::LeftMouseUp),
|
|
||||||
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown, CGEventType::RightMouseUp),
|
|
||||||
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, CGEventType::OtherMouseUp),
|
|
||||||
};
|
|
||||||
|
|
||||||
let point = {
|
|
||||||
// Get current mouse position
|
|
||||||
let temp_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to create event source"))?;
|
|
||||||
let event = CGEvent::new(temp_source)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to get mouse position"))?;
|
|
||||||
let p = event.location();
|
|
||||||
p
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to create event source"))?;
|
|
||||||
|
|
||||||
// Mouse down
|
|
||||||
let down_event = CGEvent::new_mouse_event(
|
|
||||||
event_source,
|
|
||||||
down_type,
|
|
||||||
point,
|
|
||||||
cg_button,
|
|
||||||
).map_err(|_| anyhow::anyhow!("Failed to create mouse down event"))?;
|
|
||||||
down_event.post(CGEventTapLocation::HID);
|
|
||||||
} // event_source and down_event dropped here
|
|
||||||
|
|
||||||
// Small delay
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
|
||||||
|
|
||||||
{
|
|
||||||
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to create event source"))?;
|
|
||||||
|
|
||||||
let up_event = CGEvent::new_mouse_event(
|
|
||||||
event_source,
|
|
||||||
up_type,
|
|
||||||
point,
|
|
||||||
cg_button,
|
|
||||||
).map_err(|_| anyhow::anyhow!("Failed to create mouse up event"))?;
|
|
||||||
up_event.post(CGEventTapLocation::HID);
|
|
||||||
} // event_source and up_event dropped here
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn double_click(&self, button: MouseButton) -> Result<()> {
|
|
||||||
self.click(button).await?;
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
||||||
self.click(button).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn type_text(&self, text: &str) -> Result<()> {
|
|
||||||
for ch in text.chars() {
|
|
||||||
{
|
|
||||||
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to create event source"))?;
|
|
||||||
|
|
||||||
// Create keyboard event for character
|
|
||||||
let event = CGEvent::new_keyboard_event(
|
|
||||||
event_source,
|
|
||||||
0, // keycode (0 for unicode)
|
|
||||||
true,
|
|
||||||
).map_err(|_| anyhow::anyhow!("Failed to create keyboard event"))?;
|
|
||||||
|
|
||||||
// Set unicode string
|
|
||||||
let mut utf16_buf = [0u16; 2];
|
|
||||||
let utf16_slice = ch.encode_utf16(&mut utf16_buf);
|
|
||||||
let utf16_chars: Vec<u16> = utf16_slice.iter().copied().collect();
|
|
||||||
|
|
||||||
event.set_string_from_utf16_unchecked(utf16_chars.as_slice());
|
|
||||||
event.post(CGEventTapLocation::HID);
|
|
||||||
} // event_source and event dropped here
|
|
||||||
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn press_key(&self, key: &str) -> Result<()> {
|
|
||||||
let keycode = self.key_to_keycode(key)?;
|
|
||||||
|
|
||||||
{
|
|
||||||
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to create event source"))?;
|
|
||||||
|
|
||||||
// Key down
|
|
||||||
let down_event = CGEvent::new_keyboard_event(
|
|
||||||
event_source,
|
|
||||||
keycode,
|
|
||||||
true,
|
|
||||||
).map_err(|_| anyhow::anyhow!("Failed to create key down event"))?;
|
|
||||||
down_event.post(CGEventTapLocation::HID);
|
|
||||||
} // event_source and down_event dropped here
|
|
||||||
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
|
||||||
|
|
||||||
{
|
|
||||||
let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to create event source"))?;
|
|
||||||
|
|
||||||
// Key up
|
|
||||||
let up_event = CGEvent::new_keyboard_event(
|
|
||||||
event_source,
|
|
||||||
keycode,
|
|
||||||
false,
|
|
||||||
).map_err(|_| anyhow::anyhow!("Failed to create key up event"))?;
|
|
||||||
up_event.post(CGEventTapLocation::HID);
|
|
||||||
} // event_source and up_event dropped here
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_windows(&self) -> Result<Vec<Window>> {
|
|
||||||
// Note: Full implementation would use CGWindowListCopyWindowInfo
|
|
||||||
// For now, return empty list as this requires more complex FFI
|
|
||||||
tracing::warn!("list_windows not fully implemented on macOS");
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn focus_window(&self, _window_id: &str) -> Result<()> {
|
|
||||||
// Note: Full implementation would use NSWorkspace to activate application
|
|
||||||
tracing::warn!("focus_window not fully implemented on macOS");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_window_bounds(&self, _window_id: &str) -> Result<Rect> {
|
|
||||||
// Note: Full implementation would use Accessibility API
|
|
||||||
tracing::warn!("get_window_bounds not fully implemented on macOS");
|
|
||||||
Ok(Rect { x: 0, y: 0, width: 800, height: 600 })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_element(&self, _selector: &ElementSelector) -> Result<Option<UIElement>> {
|
|
||||||
// Note: Full implementation would use macOS Accessibility API
|
|
||||||
tracing::warn!("find_element not fully implemented on macOS");
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_element_text(&self, _element_id: &str) -> Result<String> {
|
|
||||||
// Note: Full implementation would use Accessibility API
|
|
||||||
tracing::warn!("get_element_text not fully implemented on macOS");
|
|
||||||
Ok(String::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_element_bounds(&self, _element_id: &str) -> Result<Rect> {
|
|
||||||
// Note: Full implementation would use Accessibility API
|
|
||||||
tracing::warn!("get_element_bounds not fully implemented on macOS");
|
|
||||||
Ok(Rect { x: 0, y: 0, width: 100, height: 30 })
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn take_screenshot(&self, path: &str, _region: Option<Rect>, window_id: Option<&str>) -> Result<()> {
|
|
||||||
// Use native macOS screencapture command which handles all the format complexities
|
|
||||||
|
|
||||||
// Check if we have Screen Recording permission by attempting a test capture
|
|
||||||
// If we only get wallpaper/menubar but no windows, we need permission
|
|
||||||
let needs_permission_check = std::env::var("G3_SKIP_PERMISSION_CHECK").is_err();
|
|
||||||
|
|
||||||
if needs_permission_check {
|
|
||||||
// Try to open Screen Recording settings if this is the first screenshot
|
|
||||||
static PERMISSION_PROMPTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
|
||||||
|
|
||||||
if !PERMISSION_PROMPTED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
|
||||||
tracing::warn!("\n=== Screen Recording Permission Required ===\n\
|
|
||||||
macOS requires explicit permission to capture window content.\n\
|
|
||||||
If screenshots only show wallpaper/menubar (no windows):\n\n\
|
|
||||||
1. Open System Settings > Privacy & Security > Screen Recording\n\
|
|
||||||
2. Enable permission for your terminal (iTerm/Terminal) or g3\n\
|
|
||||||
3. Restart your terminal if needed\n\n\
|
|
||||||
Opening Screen Recording settings now...\n");
|
|
||||||
|
|
||||||
// Try to open the settings (non-blocking)
|
|
||||||
let _ = std::process::Command::new("open")
|
|
||||||
.arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")
|
|
||||||
.spawn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_obj = Path::new(path);
|
|
||||||
if let Some(parent) = path_obj.parent() {
|
|
||||||
std::fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cmd = std::process::Command::new("screencapture");
|
|
||||||
|
|
||||||
// Add flags
|
|
||||||
cmd.arg("-x"); // No sound
|
|
||||||
|
|
||||||
if let Some(window_id) = window_id {
|
|
||||||
// Capture specific window by getting its bounds and using region capture
|
|
||||||
// window_id format: "AppName" or "AppName:WindowTitle"
|
|
||||||
let app_name = window_id.split(':').next().unwrap_or(window_id);
|
|
||||||
|
|
||||||
// Use AppleScript to get window bounds
|
|
||||||
let script = format!(
|
|
||||||
r#"tell application "{}"
|
|
||||||
tell current window
|
|
||||||
get bounds
|
|
||||||
end tell
|
|
||||||
end tell"#,
|
|
||||||
app_name
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = std::process::Command::new("osascript")
|
|
||||||
.arg("-e")
|
|
||||||
.arg(&script)
|
|
||||||
.output()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to get window bounds: {}", e))?;
|
|
||||||
|
|
||||||
if output.status.success() {
|
|
||||||
let bounds_str = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let bounds: Vec<i32> = bounds_str
|
|
||||||
.trim()
|
|
||||||
.split(',')
|
|
||||||
.filter_map(|s| s.trim().parse().ok())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if bounds.len() == 4 {
|
|
||||||
let (left, top, right, bottom) = (bounds[0], bounds[1], bounds[2], bounds[3]);
|
|
||||||
let width = right - left;
|
|
||||||
let height = bottom - top;
|
|
||||||
|
|
||||||
cmd.arg("-R");
|
|
||||||
cmd.arg(format!("{},{},{},{}", left, top, width, height));
|
|
||||||
|
|
||||||
tracing::debug!("Capturing window '{}' at region: {},{} {}x{}", app_name, left, top, width, height);
|
|
||||||
} else {
|
|
||||||
tracing::warn!("Failed to parse window bounds, capturing full screen");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tracing::warn!("Failed to get window bounds for '{}', capturing full screen", app_name);
|
|
||||||
}
|
|
||||||
} else if let Some(region) = _region {
|
|
||||||
// Capture specific region: -R x,y,width,height
|
|
||||||
cmd.arg("-R");
|
|
||||||
cmd.arg(format!("{},{},{},{}", region.x, region.y, region.width, region.height));
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.arg(path);
|
|
||||||
|
|
||||||
let output = cmd.output()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to execute screencapture: {}", e))?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
anyhow::bail!("screencapture failed: {}", stderr);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!("Screenshot saved using screencapture: {}", path);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn extract_text_from_screen(&self, region: Rect) -> Result<OCRResult> {
|
|
||||||
// Take screenshot of region first
|
|
||||||
let temp_path = format!("/tmp/g3_ocr_{}.png", uuid::Uuid::new_v4());
|
|
||||||
self.take_screenshot(&temp_path, Some(region), None).await?;
|
|
||||||
|
|
||||||
// Extract text from the screenshot
|
|
||||||
let result = self.extract_text_from_image(&temp_path).await?;
|
|
||||||
|
|
||||||
// Clean up temp file
|
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn extract_text_from_image(&self, _path: &str) -> Result<OCRResult> {
|
|
||||||
// Check if tesseract is available on the system
|
|
||||||
let tesseract_check = std::process::Command::new("which")
|
|
||||||
.arg("tesseract")
|
|
||||||
.output();
|
|
||||||
|
|
||||||
if tesseract_check.is_err() || !tesseract_check.as_ref().unwrap().status.success() {
|
|
||||||
anyhow::bail!("Tesseract OCR is not installed on your system.\n\n\
|
|
||||||
To install tesseract:\n macOS: brew install tesseract\n \
|
|
||||||
Linux: sudo apt-get install tesseract-ocr (Ubuntu/Debian)\n \
|
|
||||||
sudo yum install tesseract (RHEL/CentOS)\n \
|
|
||||||
Windows: Download from https://github.com/UB-Mannheim/tesseract/wiki\n\n\
|
|
||||||
After installation, restart your terminal and try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Tesseract
|
|
||||||
let tess = Tesseract::new(None, Some("eng"))
|
|
||||||
.map_err(|e| {
|
|
||||||
anyhow::anyhow!("Failed to initialize Tesseract: {}\n\n\
|
|
||||||
This usually means:\n1. Tesseract is not properly installed\n\
|
|
||||||
2. Language data files are missing\n\nTo fix:\n \
|
|
||||||
macOS: brew reinstall tesseract\n \
|
|
||||||
Linux: sudo apt-get install tesseract-ocr-eng\n \
|
|
||||||
Windows: Reinstall tesseract and ensure language files are included", e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let text = tess.set_image(_path)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to load image '{}': {}", _path, e))?
|
|
||||||
.get_text()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to extract text from image: {}", e))?;
|
|
||||||
|
|
||||||
// Get confidence (simplified - would need more complex API calls for per-word confidence)
|
|
||||||
let confidence = 0.85; // Placeholder
|
|
||||||
|
|
||||||
Ok(OCRResult {
|
|
||||||
text,
|
|
||||||
confidence,
|
|
||||||
bounds: Rect { x: 0, y: 0, width: 0, height: 0 }, // Would need image dimensions
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn find_text_on_screen(&self, _text: &str) -> Result<Option<Point>> {
|
|
||||||
// Check if tesseract is available on the system
|
|
||||||
let tesseract_check = std::process::Command::new("which")
|
|
||||||
.arg("tesseract")
|
|
||||||
.output();
|
|
||||||
|
|
||||||
if tesseract_check.is_err() || !tesseract_check.as_ref().unwrap().status.success() {
|
|
||||||
anyhow::bail!("Tesseract OCR is not installed on your system.\n\n\
|
|
||||||
To install tesseract:\n macOS: brew install tesseract\n \
|
|
||||||
Linux: sudo apt-get install tesseract-ocr (Ubuntu/Debian)\n \
|
|
||||||
sudo yum install tesseract (RHEL/CentOS)\n \
|
|
||||||
Windows: Download from https://github.com/UB-Mannheim/tesseract/wiki\n\n\
|
|
||||||
After installation, restart your terminal and try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take full screen screenshot
|
|
||||||
let temp_path = format!("/tmp/g3_ocr_search_{}.png", uuid::Uuid::new_v4());
|
|
||||||
self.take_screenshot(&temp_path, None, None).await?;
|
|
||||||
|
|
||||||
// Use Tesseract to find text with bounding boxes
|
|
||||||
let tess = Tesseract::new(None, Some("eng"))
|
|
||||||
.map_err(|e| {
|
|
||||||
anyhow::anyhow!("Failed to initialize Tesseract: {}\n\n\
|
|
||||||
This usually means:\n1. Tesseract is not properly installed\n\
|
|
||||||
2. Language data files are missing\n\nTo fix:\n \
|
|
||||||
macOS: brew reinstall tesseract\n \
|
|
||||||
Linux: sudo apt-get install tesseract-ocr-eng\n \
|
|
||||||
Windows: Reinstall tesseract and ensure language files are included", e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let full_text = tess.set_image(temp_path.as_str())
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to load screenshot: {}", e))?
|
|
||||||
.get_text()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to extract text from screen: {}", e))?;
|
|
||||||
|
|
||||||
// Clean up temp file
|
|
||||||
let _ = std::fs::remove_file(&temp_path);
|
|
||||||
|
|
||||||
// Simple text search - full implementation would use get_component_images
|
|
||||||
// to get bounding boxes for each word
|
|
||||||
if full_text.contains(_text) {
|
|
||||||
tracing::warn!("Text found but precise coordinates not available in simplified implementation");
|
|
||||||
Ok(Some(Point { x: 0, y: 0 }))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2473,8 +2473,7 @@ Template:
|
|||||||
let (thin_summary, chars_saved) = self.context_window.thin_context();
|
let (thin_summary, chars_saved) = self.context_window.thin_context();
|
||||||
self.thinning_events.push(chars_saved);
|
self.thinning_events.push(chars_saved);
|
||||||
// Print the thinning summary to the user
|
// Print the thinning summary to the user
|
||||||
self.ui_writer.println("");
|
self.ui_writer.print_context_thinning(&thin_summary);
|
||||||
self.ui_writer.print_context_status(&format!("{}\n", thin_summary));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track what we've already displayed before getting new text
|
// Track what we've already displayed before getting new text
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ pub trait UiWriter: Send + Sync {
|
|||||||
/// Print a context window status message
|
/// Print a context window status message
|
||||||
fn print_context_status(&self, message: &str);
|
fn print_context_status(&self, message: &str);
|
||||||
|
|
||||||
|
/// Print a context thinning success message with highlight and animation
|
||||||
|
fn print_context_thinning(&self, message: &str);
|
||||||
|
|
||||||
/// Print a tool execution header
|
/// Print a tool execution header
|
||||||
fn print_tool_header(&self, tool_name: &str);
|
fn print_tool_header(&self, tool_name: &str);
|
||||||
|
|
||||||
@@ -60,6 +63,7 @@ impl UiWriter for NullUiWriter {
|
|||||||
fn print_inline(&self, _message: &str) {}
|
fn print_inline(&self, _message: &str) {}
|
||||||
fn print_system_prompt(&self, _prompt: &str) {}
|
fn print_system_prompt(&self, _prompt: &str) {}
|
||||||
fn print_context_status(&self, _message: &str) {}
|
fn print_context_status(&self, _message: &str) {}
|
||||||
|
fn print_context_thinning(&self, _message: &str) {}
|
||||||
fn print_tool_header(&self, _tool_name: &str) {}
|
fn print_tool_header(&self, _tool_name: &str) {}
|
||||||
fn print_tool_arg(&self, _key: &str, _value: &str) {}
|
fn print_tool_arg(&self, _key: &str, _value: &str) {}
|
||||||
fn print_tool_output_header(&self) {}
|
fn print_tool_output_header(&self) {}
|
||||||
|
|||||||
Reference in New Issue
Block a user