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.
@@ -168,60 +168,144 @@ class BackupRecoveryManager:
168
168
  self.logger.error(f"Failed to create JSON backup: {e}")
169
169
  return None
170
170
 
171
+ def _validate_database_for_backup(self, connection_manager) -> tuple[bool, str]:
172
+ """
173
+ Validate that database has meaningful content before creating backup.
174
+
175
+ Args:
176
+ connection_manager: Database connection manager
177
+
178
+ Returns:
179
+ Tuple of (is_valid, message)
180
+ """
181
+ try:
182
+ conn = connection_manager.get_connection()
183
+
184
+ # Check that key tables exist and have content
185
+ critical_tables = ['core_settings', 'tool_settings']
186
+ total_records = 0
187
+
188
+ for table in critical_tables:
189
+ try:
190
+ cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
191
+ count = cursor.fetchone()[0]
192
+ total_records += count
193
+ except sqlite3.Error:
194
+ pass # Table doesn't exist
195
+
196
+ if total_records == 0:
197
+ return False, "Database appears empty - no records in core settings tables"
198
+
199
+ # Check database file size
200
+ try:
201
+ from core.data_directory import get_database_path
202
+ db_path = get_database_path('settings.db')
203
+ if os.path.exists(db_path):
204
+ file_size = os.path.getsize(db_path)
205
+ if file_size < 100: # Less than 100 bytes is essentially empty
206
+ return False, f"Database file too small ({file_size} bytes) - appears corrupted or empty"
207
+ except (ImportError, OSError):
208
+ pass # Can't check file size, continue anyway
209
+
210
+ return True, f"Database validated: {total_records} records in core tables"
211
+
212
+ except Exception as e:
213
+ self.logger.warning(f"Database validation error: {e}")
214
+ return False, f"Validation error: {e}"
215
+
171
216
  def create_database_backup(self, connection_manager,
172
217
  backup_type: BackupType = BackupType.MANUAL,
173
- description: Optional[str] = None) -> Optional[BackupInfo]:
218
+ description: Optional[str] = None,
219
+ skip_validation: bool = False) -> Optional[BackupInfo]:
174
220
  """
175
- Create a database backup.
221
+ Create a database backup including both settings.db and notes.db.
222
+
223
+ Creates a ZIP archive containing both database files for complete backup.
176
224
 
177
225
  Args:
178
- connection_manager: Database connection manager
226
+ connection_manager: Database connection manager for settings.db
179
227
  backup_type: Type of backup being created
180
228
  description: Optional description for the backup
229
+ skip_validation: If True, skip database validation (use for emergency backups)
181
230
 
182
231
  Returns:
183
232
  BackupInfo if successful, None otherwise
184
233
  """
234
+ import zipfile
235
+
185
236
  try:
237
+ # Validate database content before backup (unless skipped for emergency)
238
+ if not skip_validation:
239
+ is_valid, validation_msg = self._validate_database_for_backup(connection_manager)
240
+ if not is_valid:
241
+ self.logger.warning(f"Database validation failed: {validation_msg}")
242
+ # For manual backups, refuse to create empty backup
243
+ if backup_type == BackupType.MANUAL:
244
+ self.logger.error("Refusing to create backup of empty/corrupted database. Use emergency backup if needed.")
245
+ return None
246
+ else:
247
+ self.logger.debug(validation_msg)
248
+
186
249
  timestamp = datetime.now()
250
+ # Use .zip extension for the combined database archive
187
251
  filename = self._generate_backup_filename("db", backup_type, timestamp)
188
- filepath = self.backup_dir / filename
252
+ archive_filename = f"{filename}.zip"
253
+ archive_path = self.backup_dir / archive_filename
254
+
255
+ # Get database paths
256
+ try:
257
+ from core.data_directory import get_database_path
258
+ settings_db_path = get_database_path('settings.db')
259
+ notes_db_path = get_database_path('notes.db')
260
+ except ImportError:
261
+ settings_db_path = str(self.backup_dir.parent / 'settings.db')
262
+ notes_db_path = str(self.backup_dir.parent / 'notes.db')
189
263
 
190
- # Create database backup
191
- success = connection_manager.backup_to_disk(str(filepath))
264
+ # First, create the settings.db backup using connection_manager
265
+ temp_settings_backup = self.backup_dir / f"temp_settings_{int(time.time())}.db"
266
+ success = connection_manager.backup_to_disk(str(temp_settings_backup))
192
267
  if not success:
193
- self.logger.error("Database backup failed")
268
+ self.logger.error("Settings database backup failed")
194
269
  return None
195
270
 
196
- # Compress if enabled
197
- if self.enable_compression:
198
- compressed_path = f"{filepath}.gz"
199
- with open(filepath, 'rb') as f_in:
200
- with gzip.open(compressed_path, 'wb') as f_out:
201
- shutil.copyfileobj(f_in, f_out)
271
+ try:
272
+ # Create ZIP archive containing both databases
273
+ compression = zipfile.ZIP_DEFLATED if self.enable_compression else zipfile.ZIP_STORED
274
+ with zipfile.ZipFile(archive_path, 'w', compression=compression) as zf:
275
+ # Add settings.db from the temp backup
276
+ zf.write(temp_settings_backup, 'settings.db')
277
+ self.logger.debug(f"Added settings.db to backup archive")
278
+
279
+ # Add notes.db if it exists
280
+ if os.path.exists(notes_db_path):
281
+ zf.write(notes_db_path, 'notes.db')
282
+ self.logger.debug(f"Added notes.db to backup archive")
283
+ else:
284
+ self.logger.debug("notes.db not found - skipping")
202
285
 
203
- # Remove uncompressed file
204
- os.remove(filepath)
205
- filepath = compressed_path
206
- format_type = BackupFormat.COMPRESSED
207
- else:
208
- format_type = BackupFormat.SQLITE
286
+ finally:
287
+ # Clean up temp file
288
+ if temp_settings_backup.exists():
289
+ os.remove(temp_settings_backup)
290
+
291
+ format_type = BackupFormat.COMPRESSED if self.enable_compression else BackupFormat.SQLITE
209
292
 
210
293
  # Get file size
211
- size_bytes = os.path.getsize(filepath)
294
+ size_bytes = os.path.getsize(archive_path)
212
295
 
213
296
  # Calculate checksum
214
- checksum = self._calculate_checksum(filepath)
297
+ checksum = self._calculate_checksum(str(archive_path))
215
298
 
216
- # Get database info
299
+ # Get database info (includes both databases)
217
300
  db_info = self._get_database_info(connection_manager)
301
+ db_info['includes_notes_db'] = os.path.exists(notes_db_path)
218
302
 
219
303
  # Create backup info
220
304
  backup_info = BackupInfo(
221
305
  timestamp=timestamp,
222
306
  backup_type=backup_type,
223
307
  format=format_type,
224
- filepath=str(filepath),
308
+ filepath=str(archive_path),
225
309
  size_bytes=size_bytes,
226
310
  checksum=checksum,
227
311
  description=description,
@@ -231,7 +315,7 @@ class BackupRecoveryManager:
231
315
  # Record backup
232
316
  self._record_backup(backup_info)
233
317
 
234
- self.logger.info(f"Database backup created: {filepath}")
318
+ self.logger.info(f"Database backup created: {archive_path}")
235
319
  return backup_info
236
320
 
237
321
  except Exception as e:
@@ -281,6 +365,9 @@ class BackupRecoveryManager:
281
365
  """
282
366
  Restore database from a backup.
283
367
 
368
+ Handles both new ZIP format (contains settings.db and notes.db) and
369
+ legacy single-file formats (.db or .db.gz).
370
+
284
371
  Args:
285
372
  backup_info: Information about the backup to restore
286
373
  connection_manager: Database connection manager
@@ -288,6 +375,8 @@ class BackupRecoveryManager:
288
375
  Returns:
289
376
  True if restore successful, False otherwise
290
377
  """
378
+ import zipfile
379
+
291
380
  try:
292
381
  filepath = backup_info.filepath
293
382
 
@@ -301,35 +390,82 @@ class BackupRecoveryManager:
301
390
  if current_checksum != backup_info.checksum:
302
391
  self.logger.warning(f"Backup checksum mismatch: {filepath}")
303
392
 
304
- # Prepare restore file
305
- restore_path = filepath
306
- if backup_info.format == BackupFormat.COMPRESSED:
307
- # Decompress to temporary file
308
- temp_path = self.backup_dir / f"temp_restore_{int(time.time())}.db"
309
- with gzip.open(filepath, 'rb') as f_in:
310
- with open(temp_path, 'wb') as f_out:
311
- shutil.copyfileobj(f_in, f_out)
312
- restore_path = str(temp_path)
393
+ # Determine backup format
394
+ is_zip_archive = filepath.endswith('.zip')
395
+ is_gzip = filepath.endswith('.gz') and not is_zip_archive
313
396
 
397
+ # Get database paths
314
398
  try:
315
- # Restore database
316
- success = connection_manager.restore_from_disk(restore_path)
317
-
318
- if success:
319
- self.logger.info(f"Database restored from backup: {filepath}")
399
+ from core.data_directory import get_database_path
400
+ notes_db_path = get_database_path('notes.db')
401
+ except ImportError:
402
+ notes_db_path = str(self.backup_dir.parent / 'notes.db')
403
+
404
+ temp_dir = self.backup_dir / f"temp_restore_{int(time.time())}"
405
+ temp_dir.mkdir(exist_ok=True)
406
+
407
+ try:
408
+ if is_zip_archive:
409
+ # New format: ZIP archive with both databases
410
+ with zipfile.ZipFile(filepath, 'r') as zf:
411
+ zf.extractall(temp_dir)
412
+
413
+ # Restore settings.db
414
+ temp_settings = temp_dir / 'settings.db'
415
+ if temp_settings.exists():
416
+ success = connection_manager.restore_from_disk(str(temp_settings))
417
+ if not success:
418
+ self.logger.error(f"Settings database restore failed: {filepath}")
419
+ return False
420
+ self.logger.info(f"Settings database restored from backup: {filepath}")
421
+ else:
422
+ self.logger.error(f"settings.db not found in backup archive: {filepath}")
423
+ return False
424
+
425
+ # Restore notes.db if present in backup
426
+ temp_notes = temp_dir / 'notes.db'
427
+ if temp_notes.exists():
428
+ try:
429
+ shutil.copy2(str(temp_notes), notes_db_path)
430
+ self.logger.info(f"Notes database restored from backup: {filepath}")
431
+ except Exception as e:
432
+ self.logger.warning(f"Failed to restore notes.db: {e}")
433
+ # Don't fail the whole restore if notes.db restore fails
434
+ else:
435
+ self.logger.debug("notes.db not present in backup archive")
436
+
437
+ elif is_gzip:
438
+ # Legacy format: gzipped single database file
439
+ temp_db = temp_dir / "settings.db"
440
+ with gzip.open(filepath, 'rb') as f_in:
441
+ with open(temp_db, 'wb') as f_out:
442
+ shutil.copyfileobj(f_in, f_out)
443
+
444
+ success = connection_manager.restore_from_disk(str(temp_db))
445
+ if not success:
446
+ self.logger.error(f"Database restore failed: {filepath}")
447
+ return False
448
+ self.logger.info(f"Database restored from legacy backup: {filepath}")
449
+
320
450
  else:
321
- self.logger.error(f"Database restore failed: {filepath}")
451
+ # Legacy format: plain database file
452
+ success = connection_manager.restore_from_disk(filepath)
453
+ if not success:
454
+ self.logger.error(f"Database restore failed: {filepath}")
455
+ return False
456
+ self.logger.info(f"Database restored from legacy backup: {filepath}")
322
457
 
323
- return success
458
+ return True
324
459
 
325
460
  finally:
326
- # Clean up temporary file
327
- if restore_path != filepath and os.path.exists(restore_path):
328
- os.remove(restore_path)
461
+ # Clean up temp directory
462
+ if temp_dir.exists():
463
+ shutil.rmtree(temp_dir, ignore_errors=True)
329
464
 
330
465
  except Exception as e:
331
466
  self.logger.error(f"Failed to restore from database backup: {e}")
332
467
  return False
468
+
333
469
 
334
470
  def create_migration_backup(self, json_filepath: str) -> Optional[BackupInfo]:
335
471
  """
@@ -375,11 +511,12 @@ class BackupRecoveryManager:
375
511
  try:
376
512
  self.logger.info("Starting database repair procedure")
377
513
 
378
- # Create emergency backup first
514
+ # Create emergency backup first (skip validation - we want to backup even if corrupted)
379
515
  emergency_backup = self.create_database_backup(
380
516
  connection_manager,
381
517
  BackupType.EMERGENCY,
382
- "Emergency backup before repair"
518
+ "Emergency backup before repair",
519
+ skip_validation=True
383
520
  )
384
521
 
385
522
  if not emergency_backup:
@@ -1023,17 +1160,18 @@ class BackupRecoveryManager:
1023
1160
  return hash_md5.hexdigest()
1024
1161
 
1025
1162
  def _get_database_info(self, connection_manager) -> Dict[str, Any]:
1026
- """Get database information for backup metadata."""
1163
+ """Get database information for backup metadata (includes both settings.db and notes.db)."""
1164
+ import sqlite3
1165
+
1166
+ table_counts = {}
1167
+
1027
1168
  try:
1169
+ # Get settings.db table counts
1028
1170
  conn = connection_manager.get_connection()
1171
+ settings_tables = ['core_settings', 'tool_settings', 'tab_content',
1172
+ 'performance_settings', 'font_settings', 'dialog_settings']
1029
1173
 
1030
- # Get table counts
1031
- table_counts = {}
1032
- tables = ['core_settings', 'tool_settings', 'tab_content',
1033
- 'performance_settings', 'font_settings', 'dialog_settings',
1034
- 'notes', 'notes_fts']
1035
-
1036
- for table in tables:
1174
+ for table in settings_tables:
1037
1175
  try:
1038
1176
  cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
1039
1177
  count = cursor.fetchone()[0]
@@ -1041,15 +1179,40 @@ class BackupRecoveryManager:
1041
1179
  except sqlite3.Error:
1042
1180
  table_counts[table] = 0
1043
1181
 
1044
- return {
1045
- 'data_type': 'sqlite_database',
1046
- 'table_counts': table_counts,
1047
- 'total_records': sum(table_counts.values())
1048
- }
1182
+ except Exception as e:
1183
+ self.logger.warning(f"Failed to get settings.db info: {e}")
1184
+
1185
+ # Get notes.db table counts (separate database file)
1186
+ try:
1187
+ from core.data_directory import get_database_path
1188
+ notes_db_path = get_database_path('notes.db')
1049
1189
 
1190
+ if os.path.exists(notes_db_path):
1191
+ notes_conn = sqlite3.connect(notes_db_path, timeout=5.0)
1192
+ try:
1193
+ for table in ['notes', 'notes_fts']:
1194
+ try:
1195
+ cursor = notes_conn.execute(f"SELECT COUNT(*) FROM {table}")
1196
+ count = cursor.fetchone()[0]
1197
+ table_counts[table] = count
1198
+ except sqlite3.Error:
1199
+ table_counts[table] = 0
1200
+ finally:
1201
+ notes_conn.close()
1202
+ else:
1203
+ table_counts['notes'] = 0
1204
+ table_counts['notes_fts'] = 0
1205
+
1050
1206
  except Exception as e:
1051
- self.logger.warning(f"Failed to get database info: {e}")
1052
- return {'data_type': 'sqlite_database', 'error': str(e)}
1207
+ self.logger.warning(f"Failed to get notes.db info: {e}")
1208
+ table_counts['notes'] = 0
1209
+ table_counts['notes_fts'] = 0
1210
+
1211
+ return {
1212
+ 'data_type': 'sqlite_database',
1213
+ 'table_counts': table_counts,
1214
+ 'total_records': sum(table_counts.values())
1215
+ }
1053
1216
 
1054
1217
  def _record_backup(self, backup_info: BackupInfo) -> None:
1055
1218
  """Record backup in history."""
@@ -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, create empty tool settings when accessed
48
+ # For tool_settings, first try to load existing settings from database
49
49
  if self.parent_key == "tool_settings":
50
- # Initialize empty tool settings
51
- self._data[key] = {}
52
- # Also save to database
53
- self.settings_manager.set_tool_setting(key, "initialized", True)
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, create empty tool settings when accessed via get()
95
- self._data[key] = {}
96
- # Also save to database
97
- self.settings_manager.set_tool_setting(key, "initialized", True)
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