pomera-ai-commander 1.2.5 → 1.2.8
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/bin/pomera-create-shortcut.js +51 -0
- package/bin/pomera.js +68 -0
- 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 +497 -0
- package/create_shortcut.py +12 -4
- package/mcp.json +1 -1
- package/package.json +4 -2
- package/pomera.py +408 -72
- package/tools/base64_tools.py +4 -4
- package/tools/curl_history.py +45 -5
- package/tools/curl_tool.py +42 -12
- package/tools/diff_viewer.py +2 -9
- package/tools/notes_widget.py +8 -1
- package/tools/tool_loader.py +265 -24
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Collapsible Panel Widget - Reusable collapsible container for UI sections.
|
|
3
|
+
|
|
4
|
+
This module provides a collapsible panel widget that can be used to hide/show
|
|
5
|
+
sections of the UI, saving screen space while keeping functionality accessible.
|
|
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
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CollapsiblePanel(ttk.Frame):
|
|
20
|
+
"""
|
|
21
|
+
A reusable collapsible panel widget.
|
|
22
|
+
|
|
23
|
+
Features:
|
|
24
|
+
- Toggle button with chevron icon (▼/▲)
|
|
25
|
+
- Smooth collapse/expand animation
|
|
26
|
+
- State persistence via callback
|
|
27
|
+
- Keyboard shortcut support
|
|
28
|
+
- Optional title display
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
panel = CollapsiblePanel(
|
|
32
|
+
parent,
|
|
33
|
+
title="Options",
|
|
34
|
+
collapsed=False,
|
|
35
|
+
on_state_change=lambda collapsed: save_state(collapsed)
|
|
36
|
+
)
|
|
37
|
+
# Add content to panel.content_frame
|
|
38
|
+
ttk.Label(panel.content_frame, text="Panel content").pack()
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# Animation settings
|
|
42
|
+
ANIMATION_DURATION_MS = 200
|
|
43
|
+
ANIMATION_STEPS = 10
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
parent: tk.Widget,
|
|
48
|
+
title: str = "",
|
|
49
|
+
collapsed: bool = False,
|
|
50
|
+
on_state_change: Optional[Callable[[bool], None]] = None,
|
|
51
|
+
show_title: bool = True,
|
|
52
|
+
**kwargs
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the collapsible panel.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
parent: Parent widget
|
|
59
|
+
title: Panel title text
|
|
60
|
+
collapsed: Initial collapsed state
|
|
61
|
+
on_state_change: Callback when collapse state changes, receives bool
|
|
62
|
+
show_title: Whether to show the title text
|
|
63
|
+
**kwargs: Additional keyword arguments for ttk.Frame
|
|
64
|
+
"""
|
|
65
|
+
super().__init__(parent, **kwargs)
|
|
66
|
+
|
|
67
|
+
self.title = title
|
|
68
|
+
self._collapsed = collapsed
|
|
69
|
+
self._on_state_change = on_state_change
|
|
70
|
+
self._show_title = show_title
|
|
71
|
+
self._animation_id: Optional[str] = None
|
|
72
|
+
self._content_height: int = 0
|
|
73
|
+
|
|
74
|
+
self._create_widgets()
|
|
75
|
+
self._apply_initial_state()
|
|
76
|
+
|
|
77
|
+
def _create_widgets(self) -> None:
|
|
78
|
+
"""Create the panel widgets."""
|
|
79
|
+
# Header frame with toggle button
|
|
80
|
+
self.header_frame = ttk.Frame(self)
|
|
81
|
+
self.header_frame.pack(fill=tk.X)
|
|
82
|
+
|
|
83
|
+
# Toggle button with chevron
|
|
84
|
+
self.toggle_btn = ttk.Button(
|
|
85
|
+
self.header_frame,
|
|
86
|
+
text=self._get_toggle_text(),
|
|
87
|
+
command=self.toggle,
|
|
88
|
+
width=3
|
|
89
|
+
)
|
|
90
|
+
self.toggle_btn.pack(side=tk.LEFT, padx=(0, 5))
|
|
91
|
+
|
|
92
|
+
# Title label with inline shortcut hint
|
|
93
|
+
if self._show_title and self.title:
|
|
94
|
+
self.title_label = ttk.Label(
|
|
95
|
+
self.header_frame,
|
|
96
|
+
text=f"{self.title} (Ctrl+Shift+H)",
|
|
97
|
+
font=("TkDefaultFont", 9, "bold")
|
|
98
|
+
)
|
|
99
|
+
self.title_label.pack(side=tk.LEFT)
|
|
100
|
+
# Make title clickable too
|
|
101
|
+
self.title_label.bind("<Button-1>", lambda e: self.toggle())
|
|
102
|
+
|
|
103
|
+
# Content frame (what gets collapsed)
|
|
104
|
+
self.content_frame = ttk.Frame(self)
|
|
105
|
+
if not self._collapsed:
|
|
106
|
+
self.content_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
|
|
107
|
+
|
|
108
|
+
def _get_toggle_text(self) -> str:
|
|
109
|
+
"""Get the toggle button text based on collapsed state."""
|
|
110
|
+
return "▲" if self._collapsed else "▼"
|
|
111
|
+
|
|
112
|
+
def _apply_initial_state(self) -> None:
|
|
113
|
+
"""Apply the initial collapsed state."""
|
|
114
|
+
if self._collapsed:
|
|
115
|
+
self.content_frame.pack_forget()
|
|
116
|
+
self.toggle_btn.configure(text=self._get_toggle_text())
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def collapsed(self) -> bool:
|
|
120
|
+
"""Get the current collapsed state."""
|
|
121
|
+
return self._collapsed
|
|
122
|
+
|
|
123
|
+
@collapsed.setter
|
|
124
|
+
def collapsed(self, value: bool) -> None:
|
|
125
|
+
"""Set the collapsed state."""
|
|
126
|
+
if value != self._collapsed:
|
|
127
|
+
self._collapsed = value
|
|
128
|
+
self._update_state()
|
|
129
|
+
|
|
130
|
+
def toggle(self) -> None:
|
|
131
|
+
"""Toggle the collapsed state."""
|
|
132
|
+
self._collapsed = not self._collapsed
|
|
133
|
+
self._update_state()
|
|
134
|
+
|
|
135
|
+
# Notify callback
|
|
136
|
+
if self._on_state_change:
|
|
137
|
+
try:
|
|
138
|
+
self._on_state_change(self._collapsed)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.warning(f"Error in on_state_change callback: {e}")
|
|
141
|
+
|
|
142
|
+
def _update_state(self) -> None:
|
|
143
|
+
"""Update the visual state based on collapsed flag."""
|
|
144
|
+
# Cancel any running animation
|
|
145
|
+
if self._animation_id:
|
|
146
|
+
self.after_cancel(self._animation_id)
|
|
147
|
+
self._animation_id = None
|
|
148
|
+
|
|
149
|
+
# Update toggle button
|
|
150
|
+
self.toggle_btn.configure(text=self._get_toggle_text())
|
|
151
|
+
|
|
152
|
+
# Show/hide content
|
|
153
|
+
if self._collapsed:
|
|
154
|
+
self.content_frame.pack_forget()
|
|
155
|
+
else:
|
|
156
|
+
self.content_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
|
|
157
|
+
|
|
158
|
+
logger.debug(f"Panel '{self.title}' collapsed={self._collapsed}")
|
|
159
|
+
|
|
160
|
+
def expand(self) -> None:
|
|
161
|
+
"""Expand the panel if collapsed."""
|
|
162
|
+
if self._collapsed:
|
|
163
|
+
self.toggle()
|
|
164
|
+
|
|
165
|
+
def collapse(self) -> None:
|
|
166
|
+
"""Collapse the panel if expanded."""
|
|
167
|
+
if not self._collapsed:
|
|
168
|
+
self.toggle()
|
|
169
|
+
|
|
170
|
+
def set_content_widget(self, widget: tk.Widget) -> None:
|
|
171
|
+
"""
|
|
172
|
+
Set a widget as the panel content.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
widget: Widget to place inside the content frame
|
|
176
|
+
"""
|
|
177
|
+
# Clear existing content
|
|
178
|
+
for child in self.content_frame.winfo_children():
|
|
179
|
+
child.destroy()
|
|
180
|
+
|
|
181
|
+
# Add new content
|
|
182
|
+
widget.pack(in_=self.content_frame, fill=tk.BOTH, expand=True)
|
|
183
|
+
|
|
184
|
+
def bind_shortcut(self, root: tk.Tk, shortcut: str = "<Control-Shift-H>") -> None:
|
|
185
|
+
"""
|
|
186
|
+
Bind a keyboard shortcut to toggle the panel.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
root: Root window to bind the shortcut to
|
|
190
|
+
shortcut: Key sequence (default: Ctrl+Shift+H)
|
|
191
|
+
"""
|
|
192
|
+
root.bind_all(shortcut, lambda e: self.toggle())
|
|
193
|
+
logger.debug(f"Bound shortcut {shortcut} to panel '{self.title}'")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Module availability flag for import checking
|
|
197
|
+
COLLAPSIBLE_PANEL_AVAILABLE = True
|
|
@@ -45,12 +45,17 @@ class NestedSettingsProxy:
|
|
|
45
45
|
def __getitem__(self, key: str) -> Any:
|
|
46
46
|
"""Handle nested access like settings["tool_settings"]["Tool Name"]."""
|
|
47
47
|
if key not in self._data:
|
|
48
|
-
# For tool_settings,
|
|
48
|
+
# For tool_settings, first try to load existing settings from database
|
|
49
49
|
if self.parent_key == "tool_settings":
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
existing_settings = self.settings_manager.get_tool_settings(key)
|
|
51
|
+
if existing_settings and not (len(existing_settings) == 1 and 'initialized' in existing_settings):
|
|
52
|
+
# Found real settings in database - use them
|
|
53
|
+
self._data[key] = existing_settings
|
|
54
|
+
else:
|
|
55
|
+
# No existing settings - create empty tool settings
|
|
56
|
+
self._data[key] = {}
|
|
57
|
+
# Save initialized marker to database
|
|
58
|
+
self.settings_manager.set_tool_setting(key, "initialized", True)
|
|
54
59
|
else:
|
|
55
60
|
raise KeyError(f"Key '{key}' not found in {self.parent_key}")
|
|
56
61
|
|
|
@@ -91,10 +96,16 @@ class NestedSettingsProxy:
|
|
|
91
96
|
def get(self, key: str, default: Any = None) -> Any:
|
|
92
97
|
"""Handle nested_settings.get("key", default) calls."""
|
|
93
98
|
if key not in self._data and self.parent_key == "tool_settings":
|
|
94
|
-
# For tool_settings,
|
|
95
|
-
self.
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
# For tool_settings, first try to load existing settings from database
|
|
100
|
+
existing_settings = self.settings_manager.get_tool_settings(key)
|
|
101
|
+
if existing_settings and not (len(existing_settings) == 1 and 'initialized' in existing_settings):
|
|
102
|
+
# Found real settings in database - use them
|
|
103
|
+
self._data[key] = existing_settings
|
|
104
|
+
else:
|
|
105
|
+
# No existing settings - create empty tool settings
|
|
106
|
+
self._data[key] = {}
|
|
107
|
+
# Save initialized marker to database
|
|
108
|
+
self.settings_manager.set_tool_setting(key, "initialized", True)
|
|
98
109
|
|
|
99
110
|
value = self._data.get(key, default)
|
|
100
111
|
|
|
@@ -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
|
|