diff --git a/src/cthulhu/plugins/AIAssistant/plugin.py b/src/cthulhu/plugins/AIAssistant/plugin.py index 1e93c2e..fbbc87f 100644 --- a/src/cthulhu/plugins/AIAssistant/plugin.py +++ b/src/cthulhu/plugins/AIAssistant/plugin.py @@ -56,10 +56,9 @@ class AIAssistant(Plugin): except: pass - # Keybinding storage - self._kb_binding_activate = None - self._kb_binding_question = None - self._kb_binding_describe = None + # Menu and keybinding storage + self._kb_binding_menu = None + self._menu_gui = None # AI provider and settings self._provider_type = None @@ -238,41 +237,144 @@ class AIAssistant(Plugin): return False def _register_keybindings(self): - """Register AI Assistant keybindings.""" + """Register AI Assistant menu keybinding.""" try: - # Main AI Assistant activation - avoid conflict with Actions - self._kb_binding_activate = self.registerGestureByString( - self._handle_ai_activate, - "Activate AI Assistant", + # Single keybinding to show AI Assistant menu + self._kb_binding_menu = self.registerGestureByString( + self._show_ai_menu, + "Show AI Assistant menu", 'kb:cthulhu+control+shift+a' ) - # Ask question about current focus - self._kb_binding_question = self.registerGestureByString( - self._handle_ai_question, - "Ask AI about current focus", - 'kb:cthulhu+control+shift+q' - ) - - # Describe current screen - self._kb_binding_describe = self.registerGestureByString( - self._handle_ai_describe, - "AI describe current screen", - 'kb:cthulhu+control+shift+d' - ) - - logger.info("AI Assistant keybindings registered") - print(f"DEBUG: AI Assistant keybindings registered - activate: {self._kb_binding_activate}, question: {self._kb_binding_question}, describe: {self._kb_binding_describe}") + logger.info("AI Assistant menu keybinding registered") + print(f"DEBUG: AI Assistant menu keybinding registered: {self._kb_binding_menu}") except Exception as e: - logger.error(f"Error registering AI keybindings: {e}") + logger.error(f"Error registering AI menu keybinding: {e}") def _unregister_keybindings(self): """Unregister AI Assistant keybindings.""" # Keybindings are automatically cleaned up when plugin deactivates - self._kb_binding_activate = None - self._kb_binding_question = None - self._kb_binding_describe = None + self._kb_binding_menu = None + self._menu_gui = None + + def _show_ai_menu(self, script=None, inputEvent=None): + """Show the AI Assistant menu.""" + try: + logger.info("Showing AI Assistant menu") + + # IMPORTANT: Capture screen data BEFORE showing menu + # This ensures we get the actual screen content, not the menu itself + self._pre_menu_screen_data = self._collect_ai_data() + logger.info("Pre-captured screen data for menu actions") + + # Now show the menu + self._menu_gui = AIAssistantMenu(self._handle_menu_selection) + self._menu_gui.show_gui() + return True + except Exception as e: + logger.error(f"Error showing AI menu: {e}") + import traceback + traceback.print_exc() + return False + + def _handle_menu_selection(self, action_id): + """Handle AI Assistant menu selection.""" + try: + logger.info(f"AI menu selection: {action_id}") + + if action_id == "ask_question": + self._handle_ai_question_with_data(self._pre_menu_screen_data) + elif action_id == "describe_screen": + self._handle_ai_describe_with_data(self._pre_menu_screen_data) + elif action_id == "request_action": + self._handle_ai_activate_with_data(self._pre_menu_screen_data) + else: + logger.warning(f"Unknown AI menu action: {action_id}") + + except Exception as e: + logger.error(f"Error handling menu selection {action_id}: {e}") + + def _handle_ai_describe_with_data(self, data): + """Handle AI screen description request with pre-captured data.""" + try: + logger.info("AI screen description requested with pre-captured data") + + if not self._enabled: + self._present_message("AI Assistant is not enabled") + return True + + # Use AI to describe the current screen + if not self._ai_provider: + self._present_message("AI provider not available. Check configuration.") + return True + + self._present_message("AI Assistant analyzing screen...") + + # Use pre-captured data + if data: + try: + response = self._ai_provider.describe_screen( + data.get('screenshot'), + data.get('accessibility') + ) + self._show_description_dialog(response) + except Exception as e: + logger.error(f"Error getting AI screen description: {e}") + self._present_message(f"Error getting AI screen description: {e}") + else: + self._present_message("Could not collect screen data for analysis") + + return True + except Exception as e: + logger.error(f"Error in AI describe handler: {e}") + return False + + def _handle_ai_question_with_data(self, data): + """Handle AI question request with pre-captured data.""" + try: + logger.info("AI question requested with pre-captured data") + + if not self._enabled: + self._present_message("AI Assistant is not enabled") + return True + + if not self._ai_provider: + self._present_message("AI provider not available. Check configuration.") + return True + + # Store the pre-captured data for use by the question dialog + self._current_screen_data = data + + # Show question dialog + self._show_question_dialog() + return True + except Exception as e: + logger.error(f"Error in AI question handler: {e}") + return False + + def _handle_ai_activate_with_data(self, data): + """Handle AI action request with pre-captured data.""" + try: + logger.info("AI action requested with pre-captured data") + + if not self._enabled: + self._present_message("AI Assistant is not enabled") + return True + + if not self._ai_provider: + self._present_message("AI provider not available. Check configuration.") + return True + + # Store the pre-captured data for use by the action dialog + self._current_screen_data = data + + # Show action dialog + self._show_action_dialog() + return True + except Exception as e: + logger.error(f"Error in AI action handler: {e}") + return False def _handle_ai_activate(self, script=None, inputEvent=None): """Handle main AI Assistant activation - now shows action dialog.""" @@ -361,7 +463,7 @@ class AIAssistant(Plugin): data.get('screenshot'), data.get('accessibility') ) - self._present_message(response) + self._show_description_dialog(response) except Exception as e: logger.error(f"Error getting AI screen description: {e}") self._present_message(f"Error getting AI screen description: {e}") @@ -779,6 +881,51 @@ class AIAssistant(Plugin): self._present_message(f"Error processing question: {e}") # ============================================================================ + def _show_description_dialog(self, description): + """Show a read-only dialog with the screen description.""" + try: + dialog = Gtk.Dialog( + title="AI Screen Description", + parent=None, + flags=Gtk.DialogFlags.MODAL, + buttons=(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) + ) + + dialog.set_default_size(600, 400) + + content_area = dialog.get_content_area() + + # Create scrollable text view + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled_window.set_shadow_type(Gtk.ShadowType.IN) + + text_view = Gtk.TextView() + text_view.set_editable(False) # Read-only + text_view.set_cursor_visible(False) + text_view.set_wrap_mode(Gtk.WrapMode.WORD) + + # Set the description text + text_buffer = text_view.get_buffer() + text_buffer.set_text(description) + + scrolled_window.add(text_view) + content_area.pack_start(scrolled_window, True, True, 10) + + dialog.set_default_response(Gtk.ResponseType.CLOSE) + dialog.show_all() + + # Focus the text view so screen reader reads the content + text_view.grab_focus() + + # Show dialog and wait for user to close + response = dialog.run() + dialog.destroy() + + except Exception as e: + logger.error(f"Error showing description dialog: {e}") + self._present_message(f"Error showing description: {e}") + # NEW: Action System Methods for Phase 5 # ============================================================================ @@ -1545,3 +1692,56 @@ class AIAssistant(Plugin): except Exception as e: logger.error(f"Error extracting text from action: {e}") return None + + +class AIAssistantMenu(Gtk.Menu): + """A menu containing AI Assistant options.""" + + def __init__(self, handler): + super().__init__() + self.connect("popped-up", self._on_popped_up) + self.on_option_selected = handler + + # AI Assistant menu options + options = [ + ("ask_question", "Ask Question"), + ("describe_screen", "Describe Screen"), + ("request_action", "Request Action") + ] + + for action_id, label in options: + menu_item = Gtk.MenuItem(label=label) + menu_item.connect("activate", self._on_activate, action_id) + self.append(menu_item) + + def _on_activate(self, widget, action_id): + """Handler for menu item activation.""" + self.on_option_selected(action_id) + + def _on_popped_up(self, *args): + """Handler for menu popup.""" + logger.info("AI Assistant menu popped up") + + def show_gui(self): + """Shows the AI Assistant menu.""" + self.show_all() + display = Gdk.Display.get_default() + seat = display.get_default_seat() + device = seat.get_pointer() + screen, x, y = device.get_position() + + event = Gdk.Event.new(Gdk.EventType.BUTTON_PRESS) + event.set_screen(screen) + event.set_device(device) + event.time = Gtk.get_current_event_time() + event.x = x + event.y = y + + rect = Gdk.Rectangle() + rect.x = x + rect.y = y + rect.width = 1 + rect.height = 1 + + window = Gdk.get_default_root_window() + self.popup_at_rect(window, rect, Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, event)