diff --git a/docs/widgets/(Widget)-Ai-Chat.md b/docs/widgets/(Widget)-Ai-Chat.md
index 54f5baa3..d6cee009 100644
--- a/docs/widgets/(Widget)-Ai-Chat.md
+++ b/docs/widgets/(Widget)-Ai-Chat.md
@@ -6,6 +6,7 @@ The AI Chat widget provides a flexible, interactive chat interface that works wi
| Option | Type | Default | Description |
|---------------------|---------|-----------------|-------------|
| `label` | string | `"AI Chat"` | The label displayed for the widget. |
+| `auto_focus_input` | boolean | `false` | Automatically focus the input field when the chat window is opened. |
| `chat` | dict | See example | Popup menu configuration (blur, corners, etc). |
| `icons` | dict | See example | Icons for send, stop, clear, assistant. |
| `notification_dot` | dict | `{'enabled': false, 'corner': 'bottom_left', 'color': 'red', 'margin': [1, 1]}` | A dictionary specifying the notification dot settings for the widget. |
@@ -22,6 +23,7 @@ ai_chat:
type: "yasb.ai_chat.AiChatWidget"
options:
label: "\uf086"
+ auto_focus_input: true
chat:
blur: true
round_corners: true
@@ -58,6 +60,7 @@ ai_chat:
label: "GPT3.5 Turbo"
- name: "gpt-4"
label: "GPT4"
+ default: true
temperature: 0.3
top_p: 0.95
max_tokens: 4096
@@ -91,6 +94,7 @@ This widget is ideal for integrating any LLM service that follows the OpenAI API
## Description of Options
- **label:** The label displayed for the widget.
+- **auto_focus_input:** Automatically focus the input field when the chat window is opened.
- **chat:** Dictionary for popup menu appearance.
- **blur**: Enable blur effect
- **round_corners**: Enable system rounded corners
@@ -125,6 +129,7 @@ This widget is ideal for integrating any LLM service that follows the OpenAI API
- **models**: List of models, each with:
- **name**: Model name
- **label**: Display label
+ - **default**: Optionally mark this provider+model as the default selection (only one model per widget should have this set to `true`)
- **max_tokens**: Max tokens per response
- **temperature**: Sampling temperature
- **top_p**: Nucleus sampling
@@ -361,5 +366,11 @@ If you want to use different styles for the context menu, you can target the `.a
- If instructions are a file path, it must end with `_chatmode.md` and be accessible.
- If streaming fails, check network/API credentials and error messages.
+> [!NOTE]
+> AI Chat widget supports toggle visibility using the `toggle-widget ai_chat` command in the CLI. More information about the CLI commands can be found in the [CLI documentation](https://github.com/amnweb/yasb/wiki/CLI#toggle-widget-visibility).
+> Additionally, you can toggle visibility of specific widget instances by their configuration name — e.g. `toggle-widget ai_chat1` or `toggle-widget `.
+
+
+
## Preview of the Widget

\ No newline at end of file
diff --git a/src/core/utils/widget_builder.py b/src/core/utils/widget_builder.py
index 5d2e4c6e..64d3861c 100644
--- a/src/core/utils/widget_builder.py
+++ b/src/core/utils/widget_builder.py
@@ -68,7 +68,9 @@ def _build_widget(self, widget_name: str) -> Optional[QWidget]:
self._collect_nested_listeners(child_names)
except Exception:
logging.debug("WidgetBuilder failed to collect nested listeners for Grouper")
- return widget_cls(**normalized_options)
+ widget = widget_cls(**normalized_options)
+ widget.widget_config_name = widget_name
+ return widget
except (AttributeError, ValueError, ModuleNotFoundError):
logging.exception(f"Failed to import widget with type {widget_config['type']}")
self._invalid_widget_types[widget_name] = widget_config["type"]
diff --git a/src/core/validation/widgets/yasb/ai_chat.py b/src/core/validation/widgets/yasb/ai_chat.py
index a027c4e3..a56d6b9b 100644
--- a/src/core/validation/widgets/yasb/ai_chat.py
+++ b/src/core/validation/widgets/yasb/ai_chat.py
@@ -1,5 +1,6 @@
DEFAULTS = {
"label": "AI Chat",
+ "auto_focus_input": False,
"chat": {
"blur": True,
"round_corners": True,
@@ -29,6 +30,7 @@
VALIDATION_SCHEMA = {
"label": {"type": "string", "default": DEFAULTS["label"]},
+ "auto_focus_input": {"type": "boolean", "default": DEFAULTS["auto_focus_input"]},
"container_padding": {
"type": "dict",
"required": False,
@@ -164,6 +166,7 @@
"schema": {
"name": {"type": "string", "required": True},
"label": {"type": "string", "required": True},
+ "default": {"type": "boolean", "required": False, "default": False},
"max_tokens": {"type": "integer", "required": False, "default": 0},
"temperature": {"type": "number", "required": False, "default": 0.7},
"top_p": {"type": "number", "required": False, "default": 0.95},
diff --git a/src/core/widgets/base.py b/src/core/widgets/base.py
index 38849863..8d5b17d7 100644
--- a/src/core/widgets/base.py
+++ b/src/core/widgets/base.py
@@ -23,6 +23,7 @@ def __init__(self, timer_interval: int = None, class_name: str = ""):
self.bar = None
self.bar_id = None
self.monitor_hwnd = None
+ self.widget_config_name = None
if class_name:
self._widget_frame.setProperty("class", f"widget {class_name}")
diff --git a/src/core/widgets/yasb/ai_chat.py b/src/core/widgets/yasb/ai_chat.py
index e21a73ce..ca5a9dfc 100644
--- a/src/core/widgets/yasb/ai_chat.py
+++ b/src/core/widgets/yasb/ai_chat.py
@@ -22,11 +22,13 @@
QWidget,
)
+from core.event_service import EventService
from core.utils.utilities import PopupWidget, add_shadow
from core.utils.widgets.ai_chat.client import AiChatClient
from core.utils.widgets.ai_chat.client_helper import format_chat_text
from core.utils.widgets.animation_manager import AnimationManager
from core.utils.win32.utilities import apply_qmenu_style
+from core.utils.win32.window_actions import force_foreground_focus
from core.validation.widgets.yasb.ai_chat import VALIDATION_SCHEMA
from core.widgets.base import BaseWidget
@@ -226,10 +228,12 @@ def paintEvent(self, a0: QPaintEvent | None):
class AiChatWidget(BaseWidget):
validation_schema = VALIDATION_SCHEMA
_persistent_chat_history = {}
+ handle_widget_cli = pyqtSignal(str, str)
def __init__(
self,
label: str,
+ auto_focus_input: bool,
chat: dict,
icons: dict,
notification_dot: dict[str, Any],
@@ -242,12 +246,14 @@ def __init__(
):
super().__init__(class_name="ai-chat-widget")
self._label_content = label
+ self._auto_focus_input = auto_focus_input
self._icons = icons
self._notification_dot: dict[str, Any] = notification_dot
self._providers = providers or []
self._provider = None
self._provider_config = None
self._model = None
+ self._initialize_provider_and_model()
self._popup_chat = None
self._animation = animation
self._padding = container_padding
@@ -278,6 +284,38 @@ def __init__(
self._thinking_label = None
self._new_notification = False
+ self._event_service = EventService()
+ self.handle_widget_cli.connect(self._handle_widget_cli)
+ self._event_service.register_event("handle_widget_cli", self.handle_widget_cli)
+
+ def _initialize_provider_and_model(self):
+ """Initialize provider and model by finding the model with default: true flag.
+
+ Validates that only one model has the default flag set.
+ """
+ default_models = []
+
+ # Find all models with default flag set to true
+ for provider_cfg in self._providers:
+ for model_cfg in provider_cfg.get("models", []):
+ if model_cfg.get("default", False):
+ default_models.append((provider_cfg["provider"], model_cfg["name"]))
+
+ # Logs warning if more than one model has default flag set
+ if len(default_models) > 1:
+ logging.warning(
+ f"Multiple models have default flag set: {default_models}. Using first model: {default_models[0]}"
+ )
+
+ # Set the default provider and model if found
+ if default_models:
+ self._provider = default_models[0][0]
+ self._model = default_models[0][1]
+
+ # Set provider config
+ if self._provider:
+ self._provider_config = next((p for p in self._providers if p["provider"] == self._provider), None)
+
def _create_dynamically_label(self, content: str):
label_parts = re.split("(.*?)", content)
label_parts = [part for part in label_parts if part]
@@ -440,6 +478,19 @@ def _reconnect_streaming_if_needed(self):
if not self._streaming_state.get("partial_text", ""):
self._start_thinking_animation(msg_label)
+ def _focus_input(self):
+ """Activate the popup window and set focus to input field"""
+ if not self._is_popup_valid():
+ return
+ try:
+ # Use Win32 API to force the popup window to foreground
+ hwnd = int(self._popup_chat.winId())
+ force_foreground_focus(hwnd)
+ # Then set focus to the input field
+ self.input_edit.setFocus()
+ except RuntimeError as e:
+ logging.error(f"Error bringing ai_chat window to foreground: {e}")
+
def _toggle_chat(self):
# If popup is not visible or doesn't exist, open it
if self._popup_chat is None or not (self._popup_chat and self._popup_chat.isVisible()):
@@ -451,6 +502,16 @@ def _toggle_chat(self):
self._popup_chat.deleteLater()
self._popup_chat = None
+ def _handle_widget_cli(self, widget: str, screen: str):
+ """Handle widget CLI commands"""
+ # Match if widget is "ai_chat" (backward compatibility) or matches widget_config_name
+ if widget != "ai_chat" and widget != self.widget_config_name:
+ return
+ current_screen = self.window().screen() if self.window() else None
+ current_screen_name = current_screen.name() if current_screen else None
+ if not screen or (current_screen_name and screen.lower() == current_screen_name.lower()):
+ self._toggle_chat()
+
def _show_chat(self):
"""Show the AI chat popup with all components initialized."""
self._new_notification = False
@@ -610,6 +671,8 @@ def _show_chat(self):
self._popup_chat.show()
self._reconnect_streaming_if_needed()
self._update_send_button_state()
+ if self._auto_focus_input:
+ self._focus_input()
def _populate_provider_menu(self):
self.provider_menu.clear()