pomera-ai-commander 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/backup_recovery_manager.py +223 -60
- package/core/collapsible_panel.py +197 -0
- package/core/database_settings_manager.py +20 -9
- package/core/migration_manager.py +12 -11
- package/core/settings_defaults_registry.py +8 -0
- package/core/tool_search_widget.py +417 -0
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/pomera.py +403 -69
- package/tools/curl_history.py +45 -5
- package/tools/curl_tool.py +42 -12
- package/tools/diff_viewer.py +2 -9
- package/tools/tool_loader.py +263 -15
|
@@ -324,8 +324,9 @@ class MigrationManager:
|
|
|
324
324
|
"""
|
|
325
325
|
try:
|
|
326
326
|
with self.connection_manager.transaction() as conn:
|
|
327
|
-
#
|
|
328
|
-
|
|
327
|
+
# NOTE: Do NOT clear tables - use INSERT OR REPLACE for upsert semantics
|
|
328
|
+
# The _clear_all_tables() call was removed because it caused data loss
|
|
329
|
+
# when save_settings() was called with incomplete/empty data
|
|
329
330
|
|
|
330
331
|
# Migrate core settings
|
|
331
332
|
self._migrate_core_settings(conn, json_data)
|
|
@@ -432,7 +433,7 @@ class MigrationManager:
|
|
|
432
433
|
serialized_value = self.converter.serialize_value(value)
|
|
433
434
|
|
|
434
435
|
conn.execute(
|
|
435
|
-
"INSERT INTO core_settings (key, value, data_type) VALUES (?, ?, ?)",
|
|
436
|
+
"INSERT OR REPLACE INTO core_settings (key, value, data_type) VALUES (?, ?, ?)",
|
|
436
437
|
(key, serialized_value, data_type)
|
|
437
438
|
)
|
|
438
439
|
|
|
@@ -454,7 +455,7 @@ class MigrationManager:
|
|
|
454
455
|
serialized_value = self.converter.serialize_value(value)
|
|
455
456
|
|
|
456
457
|
conn.execute(
|
|
457
|
-
"INSERT INTO tool_settings (tool_name, setting_path, setting_value, data_type) VALUES (?, ?, ?, ?)",
|
|
458
|
+
"INSERT OR REPLACE INTO tool_settings (tool_name, setting_path, setting_value, data_type) VALUES (?, ?, ?, ?)",
|
|
458
459
|
(tool_name, setting_path, serialized_value, data_type)
|
|
459
460
|
)
|
|
460
461
|
else:
|
|
@@ -463,7 +464,7 @@ class MigrationManager:
|
|
|
463
464
|
serialized_value = self.converter.serialize_value(tool_config)
|
|
464
465
|
|
|
465
466
|
conn.execute(
|
|
466
|
-
"INSERT INTO tool_settings (tool_name, setting_path, setting_value, data_type) VALUES (?, ?, ?, ?)",
|
|
467
|
+
"INSERT OR REPLACE INTO tool_settings (tool_name, setting_path, setting_value, data_type) VALUES (?, ?, ?, ?)",
|
|
467
468
|
(tool_name, 'value', serialized_value, data_type)
|
|
468
469
|
)
|
|
469
470
|
|
|
@@ -480,7 +481,7 @@ class MigrationManager:
|
|
|
480
481
|
input_tabs = json_data['input_tabs']
|
|
481
482
|
for i, content in enumerate(input_tabs):
|
|
482
483
|
conn.execute(
|
|
483
|
-
"INSERT INTO tab_content (tab_type, tab_index, content) VALUES (?, ?, ?)",
|
|
484
|
+
"INSERT OR REPLACE INTO tab_content (tab_type, tab_index, content) VALUES (?, ?, ?)",
|
|
484
485
|
('input', i, content or '')
|
|
485
486
|
)
|
|
486
487
|
|
|
@@ -489,7 +490,7 @@ class MigrationManager:
|
|
|
489
490
|
output_tabs = json_data['output_tabs']
|
|
490
491
|
for i, content in enumerate(output_tabs):
|
|
491
492
|
conn.execute(
|
|
492
|
-
"INSERT INTO tab_content (tab_type, tab_index, content) VALUES (?, ?, ?)",
|
|
493
|
+
"INSERT OR REPLACE INTO tab_content (tab_type, tab_index, content) VALUES (?, ?, ?)",
|
|
493
494
|
('output', i, content or '')
|
|
494
495
|
)
|
|
495
496
|
|
|
@@ -511,7 +512,7 @@ class MigrationManager:
|
|
|
511
512
|
serialized_value = self.converter.serialize_value(value)
|
|
512
513
|
|
|
513
514
|
conn.execute(
|
|
514
|
-
"INSERT INTO performance_settings (category, setting_key, setting_value, data_type) VALUES (?, ?, ?, ?)",
|
|
515
|
+
"INSERT OR REPLACE INTO performance_settings (category, setting_key, setting_value, data_type) VALUES (?, ?, ?, ?)",
|
|
515
516
|
(category, setting_key, serialized_value, data_type)
|
|
516
517
|
)
|
|
517
518
|
else:
|
|
@@ -520,7 +521,7 @@ class MigrationManager:
|
|
|
520
521
|
serialized_value = self.converter.serialize_value(settings)
|
|
521
522
|
|
|
522
523
|
conn.execute(
|
|
523
|
-
"INSERT INTO performance_settings (category, setting_key, setting_value, data_type) VALUES (?, ?, ?, ?)",
|
|
524
|
+
"INSERT OR REPLACE INTO performance_settings (category, setting_key, setting_value, data_type) VALUES (?, ?, ?, ?)",
|
|
524
525
|
(category, 'value', serialized_value, data_type)
|
|
525
526
|
)
|
|
526
527
|
|
|
@@ -539,7 +540,7 @@ class MigrationManager:
|
|
|
539
540
|
serialized_value = self.converter.serialize_value(value)
|
|
540
541
|
|
|
541
542
|
conn.execute(
|
|
542
|
-
"INSERT INTO font_settings (font_type, property, value, data_type) VALUES (?, ?, ?, ?)",
|
|
543
|
+
"INSERT OR REPLACE INTO font_settings (font_type, property, value, data_type) VALUES (?, ?, ?, ?)",
|
|
543
544
|
(font_type, property_name, serialized_value, data_type)
|
|
544
545
|
)
|
|
545
546
|
|
|
@@ -560,7 +561,7 @@ class MigrationManager:
|
|
|
560
561
|
self.logger.debug(f"Inserting dialog setting: {category}.{property_name} = {value} (type: {data_type})")
|
|
561
562
|
|
|
562
563
|
conn.execute(
|
|
563
|
-
"INSERT INTO dialog_settings (category, property, value, data_type) VALUES (?, ?, ?, ?)",
|
|
564
|
+
"INSERT OR REPLACE INTO dialog_settings (category, property, value, data_type) VALUES (?, ?, ?, ?)",
|
|
564
565
|
(category, property_name, serialized_value, data_type)
|
|
565
566
|
)
|
|
566
567
|
|
|
@@ -833,6 +833,14 @@ class SettingsDefaultsRegistry:
|
|
|
833
833
|
"description": "Error messages for critical issues (cannot be disabled)",
|
|
834
834
|
"examples": ["File not found", "Network error", "Invalid configuration"]
|
|
835
835
|
}
|
|
836
|
+
},
|
|
837
|
+
# UI Layout Settings for tool search and collapsible panels
|
|
838
|
+
"ui_layout": {
|
|
839
|
+
"options_panel_collapsed": False,
|
|
840
|
+
"search_bar_collapsed": False,
|
|
841
|
+
"favorite_tools": [],
|
|
842
|
+
"recent_tools": [],
|
|
843
|
+
"recent_tools_max": 10
|
|
836
844
|
}
|
|
837
845
|
}
|
|
838
846
|
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Search Widget - Compact tool selector with popup dropdown.
|
|
3
|
+
|
|
4
|
+
This module provides a compact search bar that shows the current tool name
|
|
5
|
+
and opens a popup dropdown with search results when focused.
|
|
6
|
+
|
|
7
|
+
Author: Pomera AI Commander Team
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import tkinter as tk
|
|
11
|
+
from tkinter import ttk
|
|
12
|
+
from typing import Optional, Callable, List, Dict, Any
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
# Try to import rapidfuzz for fuzzy matching
|
|
16
|
+
try:
|
|
17
|
+
from rapidfuzz import fuzz, process
|
|
18
|
+
RAPIDFUZZ_AVAILABLE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
RAPIDFUZZ_AVAILABLE = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ToolSearchPalette(ttk.Frame):
|
|
27
|
+
"""
|
|
28
|
+
Compact tool selector with popup dropdown.
|
|
29
|
+
|
|
30
|
+
Features:
|
|
31
|
+
- Compact bar showing current tool name
|
|
32
|
+
- Popup dropdown appears on click/focus
|
|
33
|
+
- Fuzzy search with keyboard navigation
|
|
34
|
+
- Dropdown hides on selection or click outside
|
|
35
|
+
|
|
36
|
+
Layout:
|
|
37
|
+
[🔍 Current Tool Name (Ctrl+K)]
|
|
38
|
+
↓ (on focus)
|
|
39
|
+
┌────────────────────────────────────┐
|
|
40
|
+
│ Search: [_____________] │
|
|
41
|
+
├────────────────────────────────────┤
|
|
42
|
+
│ ⭐ Case Tool │
|
|
43
|
+
│ Email Extraction - Extract... │
|
|
44
|
+
│ Hash Generator - Generate... │
|
|
45
|
+
└────────────────────────────────────┘
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
parent: tk.Widget,
|
|
51
|
+
tool_loader: Any,
|
|
52
|
+
on_tool_selected: Callable[[str], None],
|
|
53
|
+
settings: Optional[Dict[str, Any]] = None,
|
|
54
|
+
on_settings_change: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
55
|
+
**kwargs
|
|
56
|
+
):
|
|
57
|
+
super().__init__(parent, **kwargs)
|
|
58
|
+
|
|
59
|
+
self._tool_loader = tool_loader
|
|
60
|
+
self._on_tool_selected = on_tool_selected
|
|
61
|
+
self._settings = settings or {}
|
|
62
|
+
self._on_settings_change = on_settings_change
|
|
63
|
+
|
|
64
|
+
# Get UI layout settings
|
|
65
|
+
ui_layout = self._settings.get("ui_layout", {})
|
|
66
|
+
self._favorites: List[str] = list(ui_layout.get("favorite_tools", []))
|
|
67
|
+
self._recent: List[str] = list(ui_layout.get("recent_tools", []))
|
|
68
|
+
self._recent_max = ui_layout.get("recent_tools_max", 10)
|
|
69
|
+
|
|
70
|
+
# Current state
|
|
71
|
+
self._current_tool: str = self._settings.get("selected_tool", "Case Tool")
|
|
72
|
+
self._filtered_tools: List[str] = []
|
|
73
|
+
self._selected_index: int = 0
|
|
74
|
+
self._popup: Optional[tk.Toplevel] = None
|
|
75
|
+
self._popup_listbox: Optional[tk.Listbox] = None
|
|
76
|
+
|
|
77
|
+
self._create_widgets()
|
|
78
|
+
|
|
79
|
+
def _create_widgets(self) -> None:
|
|
80
|
+
"""Create the compact search bar (centered, half-width like VS Code)."""
|
|
81
|
+
# Centering frame
|
|
82
|
+
self.center_frame = ttk.Frame(self)
|
|
83
|
+
self.center_frame.pack(expand=True) # Centers horizontally
|
|
84
|
+
|
|
85
|
+
# Main bar frame (fixed width, centered)
|
|
86
|
+
self.bar_frame = ttk.Frame(self.center_frame)
|
|
87
|
+
self.bar_frame.pack()
|
|
88
|
+
|
|
89
|
+
# "Search Tools" label instead of icon
|
|
90
|
+
self.icon_label = ttk.Label(self.bar_frame, text="Search Tools", cursor="hand2")
|
|
91
|
+
self.icon_label.pack(side=tk.LEFT, padx=(5, 0))
|
|
92
|
+
self.icon_label.bind("<Button-1>", self._on_bar_click)
|
|
93
|
+
|
|
94
|
+
# StringVar for reliable text change detection
|
|
95
|
+
self._search_var = tk.StringVar(value=self._current_tool)
|
|
96
|
+
self._search_var.trace_add("write", self._on_search_change)
|
|
97
|
+
|
|
98
|
+
# Current tool name (wider entry for dropdown width + Ctrl+K hint space)
|
|
99
|
+
self.tool_entry = ttk.Entry(self.bar_frame, textvariable=self._search_var, width=65)
|
|
100
|
+
self.tool_entry.pack(side=tk.LEFT, padx=5)
|
|
101
|
+
|
|
102
|
+
# Make entry clickable to show dropdown
|
|
103
|
+
self.tool_entry.bind("<FocusIn>", self._on_entry_focus)
|
|
104
|
+
self.tool_entry.bind("<Return>", self._on_enter)
|
|
105
|
+
self.tool_entry.bind("<Escape>", self._hide_popup)
|
|
106
|
+
self.tool_entry.bind("<Down>", self._on_key_down)
|
|
107
|
+
self.tool_entry.bind("<Up>", self._on_key_up)
|
|
108
|
+
|
|
109
|
+
# Shortcut button (clickable, focuses search)
|
|
110
|
+
self.hint_button = ttk.Button(self.bar_frame, text="(Ctrl+K)", width=8,
|
|
111
|
+
command=self.focus_search)
|
|
112
|
+
self.hint_button.pack(side=tk.RIGHT, padx=(0, 5))
|
|
113
|
+
|
|
114
|
+
# Bind to MAIN WINDOW Configure event for resize/move handling
|
|
115
|
+
self.winfo_toplevel().bind("<Configure>", self._on_main_window_configure, add="+")
|
|
116
|
+
|
|
117
|
+
def _on_search_change(self, *args) -> None:
|
|
118
|
+
"""Handle text changes in search entry (via StringVar trace)."""
|
|
119
|
+
# Show popup if not visible
|
|
120
|
+
if not (self._popup and self._popup.winfo_exists()):
|
|
121
|
+
self._show_popup()
|
|
122
|
+
else:
|
|
123
|
+
self._update_popup_list()
|
|
124
|
+
|
|
125
|
+
def _on_main_window_configure(self, event=None) -> None:
|
|
126
|
+
"""Handle main window resize/move - hide popup to prevent floating."""
|
|
127
|
+
if self._popup and self._popup.winfo_exists():
|
|
128
|
+
self._hide_popup()
|
|
129
|
+
|
|
130
|
+
def _on_bar_click(self, event=None) -> None:
|
|
131
|
+
"""Handle click on the bar - show dropdown."""
|
|
132
|
+
self.tool_entry.focus_set()
|
|
133
|
+
self._show_popup()
|
|
134
|
+
|
|
135
|
+
def _on_configure(self, event=None) -> None:
|
|
136
|
+
"""Handle window resize/move - hide popup to prevent floating."""
|
|
137
|
+
if self._popup and self._popup.winfo_exists():
|
|
138
|
+
self._hide_popup()
|
|
139
|
+
|
|
140
|
+
def _on_entry_focus(self, event=None) -> None:
|
|
141
|
+
"""Handle focus on entry - clear text and show dropdown."""
|
|
142
|
+
# Clear search field to allow fresh search
|
|
143
|
+
self._search_var.set("")
|
|
144
|
+
self._show_popup()
|
|
145
|
+
|
|
146
|
+
def _show_popup(self) -> None:
|
|
147
|
+
"""Show the dropdown popup below the search bar."""
|
|
148
|
+
if self._popup and self._popup.winfo_exists():
|
|
149
|
+
self._update_popup_list()
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Create popup window
|
|
153
|
+
self._popup = tk.Toplevel(self)
|
|
154
|
+
self._popup.wm_overrideredirect(True) # No window decorations
|
|
155
|
+
self._popup.wm_attributes("-topmost", True)
|
|
156
|
+
|
|
157
|
+
# Position below the entry
|
|
158
|
+
x = self.tool_entry.winfo_rootx()
|
|
159
|
+
y = self.tool_entry.winfo_rooty() + self.tool_entry.winfo_height()
|
|
160
|
+
width = max(400, self.tool_entry.winfo_width() + 50)
|
|
161
|
+
self._popup.geometry(f"{width}x250+{x}+{y}")
|
|
162
|
+
|
|
163
|
+
# Popup content frame
|
|
164
|
+
popup_frame = ttk.Frame(self._popup, relief="solid", borderwidth=1)
|
|
165
|
+
popup_frame.pack(fill=tk.BOTH, expand=True)
|
|
166
|
+
|
|
167
|
+
# Results listbox
|
|
168
|
+
self._popup_listbox = tk.Listbox(
|
|
169
|
+
popup_frame,
|
|
170
|
+
selectmode=tk.SINGLE,
|
|
171
|
+
font=("TkDefaultFont", 10),
|
|
172
|
+
activestyle="dotbox"
|
|
173
|
+
)
|
|
174
|
+
self._popup_listbox.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
|
175
|
+
|
|
176
|
+
# Scrollbar
|
|
177
|
+
scrollbar = ttk.Scrollbar(popup_frame, command=self._popup_listbox.yview)
|
|
178
|
+
scrollbar.place(relx=1.0, rely=0, relheight=1.0, anchor="ne")
|
|
179
|
+
self._popup_listbox.configure(yscrollcommand=scrollbar.set)
|
|
180
|
+
|
|
181
|
+
# Bind events
|
|
182
|
+
self._popup_listbox.bind("<Double-Button-1>", self._on_listbox_select)
|
|
183
|
+
self._popup_listbox.bind("<Return>", self._on_listbox_select)
|
|
184
|
+
|
|
185
|
+
# Close popup when clicking outside
|
|
186
|
+
self._popup.bind("<FocusOut>", self._on_popup_focus_out)
|
|
187
|
+
self.tool_entry.bind("<FocusOut>", self._on_entry_focus_out)
|
|
188
|
+
|
|
189
|
+
# Populate list
|
|
190
|
+
self._update_popup_list()
|
|
191
|
+
|
|
192
|
+
# Sub-tools that are nested within parent categories (for indentation)
|
|
193
|
+
SUB_TOOLS = {
|
|
194
|
+
"Slug Generator", "Hash Generator", "ASCII Art Generator",
|
|
195
|
+
"URL Link Extractor", "Regex Extractor", "Email Extraction",
|
|
196
|
+
"Word Frequency Counter", "HTML Tool",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
def _update_popup_list(self) -> None:
|
|
200
|
+
"""Update the popup listbox with filtered tools."""
|
|
201
|
+
if not self._popup_listbox:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Get search query
|
|
205
|
+
query = self._search_var.get().strip()
|
|
206
|
+
|
|
207
|
+
# Get grouped tools (processing tools only, with parent-child hierarchy)
|
|
208
|
+
if self._tool_loader:
|
|
209
|
+
grouped_tools = self._tool_loader.get_grouped_tools()
|
|
210
|
+
all_tools = [t[0] for t in grouped_tools]
|
|
211
|
+
else:
|
|
212
|
+
grouped_tools = []
|
|
213
|
+
all_tools = []
|
|
214
|
+
|
|
215
|
+
# Filter by search query or show all grouped
|
|
216
|
+
if query and query != self._current_tool:
|
|
217
|
+
# Search mode - fuzzy search all tools
|
|
218
|
+
matched = self._fuzzy_search(all_tools, query)
|
|
219
|
+
self._filtered_tools = matched
|
|
220
|
+
# Build display with is_sub_tool info
|
|
221
|
+
tool_info = {t[0]: t[1] for t in grouped_tools}
|
|
222
|
+
display_tools = [(t, tool_info.get(t, False)) for t in matched]
|
|
223
|
+
else:
|
|
224
|
+
# Default mode - show grouped tools in order
|
|
225
|
+
display_tools = grouped_tools
|
|
226
|
+
self._filtered_tools = [t[0] for t in grouped_tools]
|
|
227
|
+
|
|
228
|
+
# Update listbox
|
|
229
|
+
self._popup_listbox.delete(0, tk.END)
|
|
230
|
+
for tool, is_sub_tool in display_tools:
|
|
231
|
+
# Format sub-tools with arrow prefix
|
|
232
|
+
if is_sub_tool:
|
|
233
|
+
indent = " ↳ " # Arrow prefix for sub-tools
|
|
234
|
+
else:
|
|
235
|
+
indent = ""
|
|
236
|
+
|
|
237
|
+
prefix = "⭐ " if tool in self._favorites else ""
|
|
238
|
+
|
|
239
|
+
# Get description
|
|
240
|
+
desc = ""
|
|
241
|
+
if self._tool_loader:
|
|
242
|
+
spec = self._tool_loader.get_tool_spec(tool)
|
|
243
|
+
if spec and spec.description:
|
|
244
|
+
desc = f" - {spec.description[:35]}..."
|
|
245
|
+
|
|
246
|
+
self._popup_listbox.insert(tk.END, f"{indent}{prefix}{tool}{desc}")
|
|
247
|
+
|
|
248
|
+
# Select first item
|
|
249
|
+
if self._filtered_tools:
|
|
250
|
+
self._selected_index = 0
|
|
251
|
+
self._popup_listbox.selection_set(0)
|
|
252
|
+
|
|
253
|
+
def _fuzzy_search(self, tools: List[str], query: str) -> List[str]:
|
|
254
|
+
"""Perform fuzzy search on tool names."""
|
|
255
|
+
if not query:
|
|
256
|
+
return tools
|
|
257
|
+
|
|
258
|
+
if RAPIDFUZZ_AVAILABLE:
|
|
259
|
+
search_data = {}
|
|
260
|
+
for tool in tools:
|
|
261
|
+
search_text = tool
|
|
262
|
+
if self._tool_loader:
|
|
263
|
+
spec = self._tool_loader.get_tool_spec(tool)
|
|
264
|
+
if spec and spec.description:
|
|
265
|
+
search_text = f"{tool} {spec.description}"
|
|
266
|
+
# Use lowercase for case-insensitive matching
|
|
267
|
+
search_data[tool] = search_text.lower()
|
|
268
|
+
|
|
269
|
+
# Convert query to lowercase for case-insensitive search
|
|
270
|
+
results = process.extract(query.lower(), search_data, scorer=fuzz.WRatio, limit=15)
|
|
271
|
+
return [match[2] for match in results if match[1] >= 40]
|
|
272
|
+
else:
|
|
273
|
+
# Fallback substring matching
|
|
274
|
+
query_lower = query.lower()
|
|
275
|
+
matches = [(t, t.lower().find(query_lower)) for t in tools if query_lower in t.lower()]
|
|
276
|
+
matches.sort(key=lambda x: x[1])
|
|
277
|
+
return [m[0] for m in matches]
|
|
278
|
+
|
|
279
|
+
def _on_search_key(self, event=None) -> None:
|
|
280
|
+
"""Handle key press in search entry."""
|
|
281
|
+
if event and event.keysym in ("Up", "Down", "Return", "Escape"):
|
|
282
|
+
return # Handled separately
|
|
283
|
+
self._update_popup_list()
|
|
284
|
+
|
|
285
|
+
def _on_key_down(self, event=None) -> str:
|
|
286
|
+
"""Handle down arrow."""
|
|
287
|
+
if self._popup_listbox and self._filtered_tools:
|
|
288
|
+
if self._selected_index < len(self._filtered_tools) - 1:
|
|
289
|
+
self._selected_index += 1
|
|
290
|
+
self._popup_listbox.selection_clear(0, tk.END)
|
|
291
|
+
self._popup_listbox.selection_set(self._selected_index)
|
|
292
|
+
self._popup_listbox.see(self._selected_index)
|
|
293
|
+
return "break"
|
|
294
|
+
|
|
295
|
+
def _on_key_up(self, event=None) -> str:
|
|
296
|
+
"""Handle up arrow."""
|
|
297
|
+
if self._popup_listbox and self._filtered_tools:
|
|
298
|
+
if self._selected_index > 0:
|
|
299
|
+
self._selected_index -= 1
|
|
300
|
+
self._popup_listbox.selection_clear(0, tk.END)
|
|
301
|
+
self._popup_listbox.selection_set(self._selected_index)
|
|
302
|
+
self._popup_listbox.see(self._selected_index)
|
|
303
|
+
return "break"
|
|
304
|
+
|
|
305
|
+
def _on_enter(self, event=None) -> str:
|
|
306
|
+
"""Handle Enter key - select current tool."""
|
|
307
|
+
if self._filtered_tools and 0 <= self._selected_index < len(self._filtered_tools):
|
|
308
|
+
tool = self._filtered_tools[self._selected_index]
|
|
309
|
+
self._select_tool(tool)
|
|
310
|
+
return "break"
|
|
311
|
+
|
|
312
|
+
def _on_listbox_select(self, event=None) -> None:
|
|
313
|
+
"""Handle listbox selection."""
|
|
314
|
+
if not self._popup_listbox:
|
|
315
|
+
return
|
|
316
|
+
selection = self._popup_listbox.curselection()
|
|
317
|
+
if selection and selection[0] < len(self._filtered_tools):
|
|
318
|
+
tool = self._filtered_tools[selection[0]]
|
|
319
|
+
self._select_tool(tool)
|
|
320
|
+
|
|
321
|
+
def _select_tool(self, tool_name: str) -> None:
|
|
322
|
+
"""Select a tool and hide popup."""
|
|
323
|
+
self._current_tool = tool_name
|
|
324
|
+
|
|
325
|
+
# Update entry to show selected tool (use StringVar)
|
|
326
|
+
self._search_var.set(tool_name)
|
|
327
|
+
|
|
328
|
+
# Update recent tools
|
|
329
|
+
if tool_name in self._recent:
|
|
330
|
+
self._recent.remove(tool_name)
|
|
331
|
+
self._recent.insert(0, tool_name)
|
|
332
|
+
self._recent = self._recent[:self._recent_max]
|
|
333
|
+
|
|
334
|
+
# Hide popup
|
|
335
|
+
self._hide_popup()
|
|
336
|
+
|
|
337
|
+
# Remove focus from search entry so clicking again shows dropdown
|
|
338
|
+
self.winfo_toplevel().focus_set()
|
|
339
|
+
|
|
340
|
+
# Save settings
|
|
341
|
+
self._save_settings()
|
|
342
|
+
|
|
343
|
+
# Notify callback
|
|
344
|
+
if self._on_tool_selected:
|
|
345
|
+
try:
|
|
346
|
+
self._on_tool_selected(tool_name)
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logger.error(f"Error in on_tool_selected callback: {e}")
|
|
349
|
+
|
|
350
|
+
def _hide_popup(self, event=None) -> str:
|
|
351
|
+
"""Hide the popup dropdown and restore tool name."""
|
|
352
|
+
if self._popup and self._popup.winfo_exists():
|
|
353
|
+
self._popup.destroy()
|
|
354
|
+
self._popup = None
|
|
355
|
+
self._popup_listbox = None
|
|
356
|
+
|
|
357
|
+
# Restore current tool name if search field is empty or contains partial text
|
|
358
|
+
if not self._search_var.get().strip() or self._search_var.get() != self._current_tool:
|
|
359
|
+
self._search_var.set(self._current_tool)
|
|
360
|
+
|
|
361
|
+
return "break"
|
|
362
|
+
|
|
363
|
+
def _on_popup_focus_out(self, event=None) -> None:
|
|
364
|
+
"""Handle focus leaving popup."""
|
|
365
|
+
# Delay to check if focus went to entry
|
|
366
|
+
self.after(100, self._check_focus)
|
|
367
|
+
|
|
368
|
+
def _on_entry_focus_out(self, event=None) -> None:
|
|
369
|
+
"""Handle focus leaving entry."""
|
|
370
|
+
# Delay to check if focus went to popup
|
|
371
|
+
self.after(100, self._check_focus)
|
|
372
|
+
|
|
373
|
+
def _check_focus(self) -> None:
|
|
374
|
+
"""Check if focus is on entry or popup, hide if not."""
|
|
375
|
+
try:
|
|
376
|
+
focused = self.focus_get()
|
|
377
|
+
if focused not in (self.tool_entry, self._popup_listbox):
|
|
378
|
+
if self._popup and self._popup.winfo_exists():
|
|
379
|
+
# Check if focus is inside popup
|
|
380
|
+
try:
|
|
381
|
+
if not str(focused).startswith(str(self._popup)):
|
|
382
|
+
self._hide_popup()
|
|
383
|
+
except:
|
|
384
|
+
pass
|
|
385
|
+
except:
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
def _save_settings(self) -> None:
|
|
389
|
+
"""Save favorites and recent to settings."""
|
|
390
|
+
if self._on_settings_change:
|
|
391
|
+
try:
|
|
392
|
+
self._on_settings_change({
|
|
393
|
+
"ui_layout": {
|
|
394
|
+
"favorite_tools": self._favorites.copy(),
|
|
395
|
+
"recent_tools": self._recent.copy(),
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.error(f"Error saving settings: {e}")
|
|
400
|
+
|
|
401
|
+
def focus_search(self) -> None:
|
|
402
|
+
"""Focus the search entry (Ctrl+K shortcut)."""
|
|
403
|
+
self.tool_entry.focus_set()
|
|
404
|
+
self.tool_entry.selection_range(0, tk.END)
|
|
405
|
+
self._show_popup()
|
|
406
|
+
|
|
407
|
+
def get_selected_tool(self) -> Optional[str]:
|
|
408
|
+
"""Get the currently selected tool name."""
|
|
409
|
+
return self._current_tool
|
|
410
|
+
|
|
411
|
+
def set_tool_loader(self, loader: Any) -> None:
|
|
412
|
+
"""Update the tool loader."""
|
|
413
|
+
self._tool_loader = loader
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# Module availability flag
|
|
417
|
+
TOOL_SEARCH_WIDGET_AVAILABLE = True
|
package/mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pomera-ai-commander",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "Text processing toolkit with 22 MCP tools including case transformation, encoding, hashing, text analysis, and notes management for AI assistants.",
|
|
5
5
|
"homepage": "https://github.com/matbanik/Pomera-AI-Commander",
|
|
6
6
|
"repository": "https://github.com/matbanik/Pomera-AI-Commander",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pomera-ai-commander",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "Text processing toolkit with 22 MCP tools for AI assistants - case transformation, encoding, hashing, text analysis, and notes management",
|
|
5
5
|
"main": "pomera_mcp_server.py",
|
|
6
6
|
"bin": {
|