supervertaler 1.9.153__py3-none-any.whl

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.

Potentially problematic release.


This version of supervertaler might be problematic. Click here for more details.

Files changed (85) hide show
  1. Supervertaler.py +47886 -0
  2. modules/__init__.py +10 -0
  3. modules/ai_actions.py +964 -0
  4. modules/ai_attachment_manager.py +343 -0
  5. modules/ai_file_viewer_dialog.py +210 -0
  6. modules/autofingers_engine.py +466 -0
  7. modules/cafetran_docx_handler.py +379 -0
  8. modules/config_manager.py +469 -0
  9. modules/database_manager.py +1878 -0
  10. modules/database_migrations.py +417 -0
  11. modules/dejavurtf_handler.py +779 -0
  12. modules/document_analyzer.py +427 -0
  13. modules/docx_handler.py +689 -0
  14. modules/encoding_repair.py +319 -0
  15. modules/encoding_repair_Qt.py +393 -0
  16. modules/encoding_repair_ui.py +481 -0
  17. modules/feature_manager.py +350 -0
  18. modules/figure_context_manager.py +340 -0
  19. modules/file_dialog_helper.py +148 -0
  20. modules/find_replace.py +164 -0
  21. modules/find_replace_qt.py +457 -0
  22. modules/glossary_manager.py +433 -0
  23. modules/image_extractor.py +188 -0
  24. modules/keyboard_shortcuts_widget.py +571 -0
  25. modules/llm_clients.py +1211 -0
  26. modules/llm_leaderboard.py +737 -0
  27. modules/llm_superbench_ui.py +1401 -0
  28. modules/local_llm_setup.py +1104 -0
  29. modules/model_update_dialog.py +381 -0
  30. modules/model_version_checker.py +373 -0
  31. modules/mqxliff_handler.py +638 -0
  32. modules/non_translatables_manager.py +743 -0
  33. modules/pdf_rescue_Qt.py +1822 -0
  34. modules/pdf_rescue_tkinter.py +909 -0
  35. modules/phrase_docx_handler.py +516 -0
  36. modules/project_home_panel.py +209 -0
  37. modules/prompt_assistant.py +357 -0
  38. modules/prompt_library.py +689 -0
  39. modules/prompt_library_migration.py +447 -0
  40. modules/quick_access_sidebar.py +282 -0
  41. modules/ribbon_widget.py +597 -0
  42. modules/sdlppx_handler.py +874 -0
  43. modules/setup_wizard.py +353 -0
  44. modules/shortcut_manager.py +932 -0
  45. modules/simple_segmenter.py +128 -0
  46. modules/spellcheck_manager.py +727 -0
  47. modules/statuses.py +207 -0
  48. modules/style_guide_manager.py +315 -0
  49. modules/superbench_ui.py +1319 -0
  50. modules/superbrowser.py +329 -0
  51. modules/supercleaner.py +600 -0
  52. modules/supercleaner_ui.py +444 -0
  53. modules/superdocs.py +19 -0
  54. modules/superdocs_viewer_qt.py +382 -0
  55. modules/superlookup.py +252 -0
  56. modules/tag_cleaner.py +260 -0
  57. modules/tag_manager.py +333 -0
  58. modules/term_extractor.py +270 -0
  59. modules/termbase_entry_editor.py +842 -0
  60. modules/termbase_import_export.py +488 -0
  61. modules/termbase_manager.py +1060 -0
  62. modules/termview_widget.py +1172 -0
  63. modules/theme_manager.py +499 -0
  64. modules/tm_editor_dialog.py +99 -0
  65. modules/tm_manager_qt.py +1280 -0
  66. modules/tm_metadata_manager.py +545 -0
  67. modules/tmx_editor.py +1461 -0
  68. modules/tmx_editor_qt.py +2784 -0
  69. modules/tmx_generator.py +284 -0
  70. modules/tracked_changes.py +900 -0
  71. modules/trados_docx_handler.py +430 -0
  72. modules/translation_memory.py +715 -0
  73. modules/translation_results_panel.py +2134 -0
  74. modules/translation_services.py +282 -0
  75. modules/unified_prompt_library.py +659 -0
  76. modules/unified_prompt_manager_qt.py +3951 -0
  77. modules/voice_commands.py +920 -0
  78. modules/voice_dictation.py +477 -0
  79. modules/voice_dictation_lite.py +249 -0
  80. supervertaler-1.9.153.dist-info/METADATA +896 -0
  81. supervertaler-1.9.153.dist-info/RECORD +85 -0
  82. supervertaler-1.9.153.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.153.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.153.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.153.dist-info/top_level.txt +2 -0
@@ -0,0 +1,659 @@
1
+ """
2
+ Unified Prompt Library Module
3
+
4
+ Simplified 2-layer architecture:
5
+ 1. System Prompts (in Settings) - mode-specific, auto-selected
6
+ 2. Prompt Library (main UI) - unified workspace with folders, favorites, multi-attach
7
+
8
+ Replaces the old 4-layer system (System/Domain/Project/Style Guides).
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import shutil
14
+ from pathlib import Path
15
+ from datetime import datetime
16
+ from typing import Dict, List, Optional, Tuple
17
+
18
+
19
+ class UnifiedPromptLibrary:
20
+ """
21
+ Manages prompts in a unified library structure with:
22
+ - Nested folder support (unlimited depth)
23
+ - Favorites and Quick Run menu
24
+ - Multi-attach capability
25
+ - Markdown files with YAML frontmatter
26
+ """
27
+
28
+ def __init__(self, library_dir=None, log_callback=None):
29
+ """
30
+ Initialize the Unified Prompt Library.
31
+
32
+ Args:
33
+ library_dir: Path to unified library directory (user_data/prompt_library)
34
+ log_callback: Function to call for logging messages
35
+ """
36
+ self.library_dir = Path(library_dir) if library_dir else None
37
+ self.log = log_callback if log_callback else print
38
+
39
+ # Create directory if it doesn't exist
40
+ if self.library_dir:
41
+ self.library_dir.mkdir(parents=True, exist_ok=True)
42
+
43
+ # Prompts storage: {relative_path: prompt_data}
44
+ self.prompts = {}
45
+
46
+ # Active prompt configuration
47
+ self.active_primary_prompt = None # Main prompt
48
+ self.active_primary_prompt_path = None
49
+ self.attached_prompts = [] # List of attached prompt data
50
+ self.attached_prompt_paths = [] # List of paths
51
+
52
+ # Cached lists for quick access
53
+ self._favorites = []
54
+ # Backward-compatible name; now represents QuickMenu (future app-level menu)
55
+ self._quick_run = []
56
+ self._quickmenu_grid = []
57
+
58
+ def set_directory(self, library_dir):
59
+ """Set the library directory after initialization"""
60
+ self.library_dir = Path(library_dir)
61
+ self.library_dir.mkdir(parents=True, exist_ok=True)
62
+
63
+ def load_all_prompts(self):
64
+ """Load all prompts from library directory (recursive)"""
65
+ self.prompts = {}
66
+
67
+ if not self.library_dir or not self.library_dir.exists():
68
+ self.log("⚠ Library directory not found")
69
+ return 0
70
+
71
+ count = self._load_from_directory_recursive(self.library_dir, "")
72
+ self.log(f"✓ Loaded {count} prompts from unified library")
73
+
74
+ # Update cached lists
75
+ self._update_favorites_list()
76
+ self._update_quick_run_list()
77
+ self._update_quickmenu_grid_list()
78
+
79
+ return count
80
+
81
+ def _load_from_directory_recursive(self, directory: Path, relative_path: str) -> int:
82
+ """
83
+ Recursively load prompts from directory and subdirectories.
84
+
85
+ Args:
86
+ directory: Absolute path to directory
87
+ relative_path: Relative path from library root (for organization)
88
+
89
+ Returns:
90
+ Number of prompts loaded
91
+ """
92
+ count = 0
93
+
94
+ if not directory.exists():
95
+ return count
96
+
97
+ for item in directory.iterdir():
98
+ # Skip hidden files and __pycache__
99
+ if item.name.startswith('.') or item.name == '__pycache__':
100
+ continue
101
+
102
+ # Recurse into subdirectories
103
+ if item.is_dir():
104
+ sub_relative = str(Path(relative_path) / item.name) if relative_path else item.name
105
+ count += self._load_from_directory_recursive(item, sub_relative)
106
+ continue
107
+
108
+ # Load prompt files (.svprompt is the new format, .md and .txt for legacy)
109
+ if item.suffix.lower() in ['.svprompt', '.md', '.txt']:
110
+ prompt_data = self._parse_markdown(item)
111
+
112
+ if prompt_data:
113
+ # Store with relative path as key
114
+ rel_path = str(Path(relative_path) / item.name) if relative_path else item.name
115
+ prompt_data['_filepath'] = str(item)
116
+ prompt_data['_relative_path'] = rel_path
117
+ prompt_data['_folder'] = relative_path
118
+
119
+ self.prompts[rel_path] = prompt_data
120
+ count += 1
121
+
122
+ return count
123
+
124
+ def _parse_markdown(self, filepath: Path) -> Optional[Dict]:
125
+ """
126
+ Parse Markdown file with YAML frontmatter.
127
+
128
+ Format:
129
+ ---
130
+ name: "Prompt Name"
131
+ description: "Description"
132
+ favorite: false
133
+ quick_run: false
134
+ folder: "Domain Expertise"
135
+ tags: ["medical", "technical"]
136
+ ---
137
+
138
+ # Content
139
+ Actual prompt content here...
140
+ """
141
+ try:
142
+ content = filepath.read_text(encoding='utf-8')
143
+
144
+ # Split frontmatter from content
145
+ if content.startswith('---'):
146
+ content = content[3:].lstrip('\n')
147
+
148
+ if '---' in content:
149
+ frontmatter_str, prompt_content = content.split('---', 1)
150
+ prompt_content = prompt_content.lstrip('\n')
151
+ else:
152
+ self.log(f"⚠ Invalid format in {filepath.name}: closing --- not found")
153
+ return None
154
+ else:
155
+ # No frontmatter - treat entire file as content
156
+ prompt_content = content
157
+ frontmatter_str = ""
158
+
159
+ # Parse YAML frontmatter
160
+ prompt_data = self._parse_yaml(frontmatter_str) if frontmatter_str else {}
161
+
162
+ # Use filename as name if not specified
163
+ if 'name' not in prompt_data:
164
+ prompt_data['name'] = filepath.stem
165
+
166
+ # Store content
167
+ prompt_data['content'] = prompt_content.strip()
168
+
169
+ # Ensure boolean fields exist
170
+ prompt_data.setdefault('favorite', False)
171
+ # Backward compatibility: quick_run is the legacy field; internally we
172
+ # treat it as the "QuickMenu (future app menu)" flag.
173
+ prompt_data.setdefault('quick_run', False)
174
+ # Support legacy quickmenu_quickmenu field (rename to sv_quickmenu)
175
+ if 'quickmenu_quickmenu' in prompt_data:
176
+ prompt_data['sv_quickmenu'] = prompt_data['quickmenu_quickmenu']
177
+ prompt_data['sv_quickmenu'] = bool(
178
+ prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False))
179
+ )
180
+ # Keep legacy field in sync so older code/versions still behave.
181
+ prompt_data['quick_run'] = bool(prompt_data['sv_quickmenu'])
182
+
183
+ # New QuickMenu fields
184
+ prompt_data.setdefault('quickmenu_grid', False)
185
+ prompt_data.setdefault('quickmenu_label', prompt_data.get('name', filepath.stem))
186
+ prompt_data.setdefault('tags', [])
187
+
188
+ return prompt_data
189
+
190
+ except Exception as e:
191
+ self.log(f"⚠ Failed to parse {filepath.name}: {e}")
192
+ return None
193
+
194
+ def _parse_yaml(self, yaml_str: str) -> Dict:
195
+ """
196
+ Simple YAML parser for frontmatter.
197
+
198
+ Supports:
199
+ - Simple strings: key: "value" or key: value
200
+ - Booleans: key: true/false
201
+ - Numbers: key: 1.0
202
+ - Arrays: tags: ["item1", "item2"] or tags: [item1, item2]
203
+ """
204
+ data = {}
205
+
206
+ for line in yaml_str.strip().split('\n'):
207
+ line = line.strip()
208
+ if not line or line.startswith('#'):
209
+ continue
210
+
211
+ if ':' not in line:
212
+ continue
213
+
214
+ key, value = line.split(':', 1)
215
+ key = key.strip()
216
+ value = value.strip()
217
+
218
+ # Handle arrays
219
+ if value.startswith('[') and value.endswith(']'):
220
+ # Remove brackets and split by comma
221
+ array_str = value[1:-1]
222
+ items = [item.strip().strip('"').strip("'") for item in array_str.split(',')]
223
+ data[key] = [item for item in items if item] # Filter empty
224
+ continue
225
+
226
+ # Handle booleans
227
+ if value.lower() in ['true', 'false']:
228
+ data[key] = value.lower() == 'true'
229
+ continue
230
+
231
+ # Remove quotes
232
+ if value.startswith('"') and value.endswith('"'):
233
+ value = value[1:-1]
234
+ elif value.startswith("'") and value.endswith("'"):
235
+ value = value[1:-1]
236
+
237
+ # Handle numbers
238
+ if value.replace('.', '', 1).replace('-', '', 1).isdigit():
239
+ try:
240
+ value = float(value) if '.' in value else int(value)
241
+ except:
242
+ pass
243
+
244
+ data[key] = value
245
+
246
+ return data
247
+
248
+ def save_prompt(self, relative_path: str, prompt_data: Dict) -> bool:
249
+ """
250
+ Save prompt as Markdown file with YAML frontmatter.
251
+
252
+ Args:
253
+ relative_path: Relative path within library (e.g., "Domain Expertise/Medical.md")
254
+ prompt_data: Dictionary with prompt info and content
255
+
256
+ Returns:
257
+ True if successful
258
+ """
259
+ try:
260
+ if not self.library_dir:
261
+ self.log("✗ Library directory not set")
262
+ return False
263
+
264
+ # Construct full path
265
+ filepath = self.library_dir / relative_path
266
+ filepath.parent.mkdir(parents=True, exist_ok=True)
267
+
268
+ # Build frontmatter
269
+ frontmatter = ['---']
270
+
271
+ # Fields to include in frontmatter (in order)
272
+ frontmatter_fields = [
273
+ 'name', 'description', 'domain', 'version', 'task_type',
274
+ 'favorite',
275
+ # QuickMenu
276
+ 'quickmenu_label', 'quickmenu_grid', 'sv_quickmenu',
277
+ # Legacy (kept for backward compatibility)
278
+ 'quick_run',
279
+ 'folder', 'tags',
280
+ 'created', 'modified'
281
+ ]
282
+
283
+ for field in frontmatter_fields:
284
+ if field in prompt_data:
285
+ value = prompt_data[field]
286
+
287
+ # Format based on type
288
+ if isinstance(value, bool):
289
+ frontmatter.append(f'{field}: {str(value).lower()}')
290
+ elif isinstance(value, list):
291
+ # Format arrays
292
+ items = ', '.join([f'"{item}"' for item in value])
293
+ frontmatter.append(f'{field}: [{items}]')
294
+ elif isinstance(value, str):
295
+ frontmatter.append(f'{field}: "{value}"')
296
+ else:
297
+ frontmatter.append(f'{field}: {value}')
298
+
299
+ frontmatter.append('---')
300
+
301
+ # Get content
302
+ content = prompt_data.get('content', '')
303
+
304
+ # Build final file content
305
+ file_content = '\n'.join(frontmatter) + '\n\n' + content.strip()
306
+
307
+ # Write file
308
+ filepath.write_text(file_content, encoding='utf-8')
309
+
310
+ # Update in-memory storage
311
+ prompt_data['_filepath'] = str(filepath)
312
+ prompt_data['_relative_path'] = relative_path
313
+
314
+ # Keep legacy field in sync
315
+ if 'sv_quickmenu' in prompt_data:
316
+ prompt_data['quick_run'] = bool(prompt_data.get('sv_quickmenu', False))
317
+ self.prompts[relative_path] = prompt_data
318
+
319
+ self.log(f"✓ Saved prompt: {prompt_data.get('name', relative_path)}")
320
+ return True
321
+
322
+ except Exception as e:
323
+ self.log(f"✗ Failed to save prompt: {e}")
324
+ return False
325
+
326
+ def get_folder_structure(self) -> Dict:
327
+ """
328
+ Get hierarchical folder structure with prompts.
329
+
330
+ Returns:
331
+ Nested dictionary representing folder tree
332
+ """
333
+ structure = {}
334
+
335
+ for rel_path, prompt_data in self.prompts.items():
336
+ parts = Path(rel_path).parts
337
+
338
+ # Build nested structure
339
+ current = structure
340
+ for i, part in enumerate(parts[:-1]): # Folders only
341
+ if part not in current:
342
+ current[part] = {'_folders': {}, '_prompts': []}
343
+ current = current[part]['_folders']
344
+
345
+ # Add prompt to final folder
346
+ folder_name = parts[-2] if len(parts) > 1 else '_root'
347
+ if folder_name not in current:
348
+ current[folder_name] = {'_folders': {}, '_prompts': []}
349
+
350
+ current[folder_name]['_prompts'].append({
351
+ 'path': rel_path,
352
+ 'name': prompt_data.get('name', Path(rel_path).stem),
353
+ 'favorite': prompt_data.get('favorite', False),
354
+ 'quick_run': prompt_data.get('quick_run', False),
355
+ 'quickmenu_grid': prompt_data.get('quickmenu_grid', False),
356
+ 'quickmenu_quickmenu': prompt_data.get('quickmenu_quickmenu', prompt_data.get('quick_run', False)),
357
+ 'quickmenu_label': prompt_data.get('quickmenu_label', prompt_data.get('name', Path(rel_path).stem)),
358
+ })
359
+
360
+ return structure
361
+
362
+ def set_primary_prompt(self, relative_path: str) -> bool:
363
+ """Set the primary (main) active prompt"""
364
+ if relative_path not in self.prompts:
365
+ self.log(f"✗ Prompt not found: {relative_path}")
366
+ return False
367
+
368
+ self.active_primary_prompt = self.prompts[relative_path]['content']
369
+ self.active_primary_prompt_path = relative_path
370
+ self.log(f"✓ Set primary prompt: {self.prompts[relative_path].get('name', relative_path)}")
371
+ return True
372
+
373
+ def set_external_primary_prompt(self, file_path: str) -> Tuple[bool, str]:
374
+ """
375
+ Set an external file (not in library) as the primary prompt.
376
+
377
+ Args:
378
+ file_path: Absolute path to the external prompt file
379
+
380
+ Returns:
381
+ Tuple of (success, display_name or error_message)
382
+ """
383
+ path = Path(file_path)
384
+
385
+ if not path.exists():
386
+ self.log(f"✗ File not found: {file_path}")
387
+ return False, "File not found"
388
+
389
+ try:
390
+ content = path.read_text(encoding='utf-8')
391
+ except Exception as e:
392
+ self.log(f"✗ Error reading file: {e}")
393
+ return False, f"Error reading file: {e}"
394
+
395
+ # Use filename (without extension) as display name
396
+ display_name = path.stem
397
+
398
+ # Mark as external with special prefix
399
+ self.active_primary_prompt = content
400
+ self.active_primary_prompt_path = f"[EXTERNAL] {file_path}"
401
+
402
+ self.log(f"✓ Set external primary prompt: {display_name}")
403
+ return True, display_name
404
+
405
+ def attach_prompt(self, relative_path: str) -> bool:
406
+ """Attach a prompt to the active configuration"""
407
+ if relative_path not in self.prompts:
408
+ self.log(f"✗ Prompt not found: {relative_path}")
409
+ return False
410
+
411
+ # Don't attach if already attached
412
+ if relative_path in self.attached_prompt_paths:
413
+ self.log(f"⚠ Already attached: {relative_path}")
414
+ return False
415
+
416
+ prompt_data = self.prompts[relative_path]
417
+ self.attached_prompts.append(prompt_data['content'])
418
+ self.attached_prompt_paths.append(relative_path)
419
+
420
+ self.log(f"✓ Attached: {prompt_data.get('name', relative_path)}")
421
+ return True
422
+
423
+ def detach_prompt(self, relative_path: str) -> bool:
424
+ """Remove an attached prompt"""
425
+ if relative_path not in self.attached_prompt_paths:
426
+ return False
427
+
428
+ idx = self.attached_prompt_paths.index(relative_path)
429
+ self.attached_prompts.pop(idx)
430
+ self.attached_prompt_paths.pop(idx)
431
+
432
+ self.log(f"✓ Detached: {relative_path}")
433
+ return True
434
+
435
+ def clear_attachments(self):
436
+ """Clear all attached prompts"""
437
+ self.attached_prompts = []
438
+ self.attached_prompt_paths = []
439
+ self.log("✓ Cleared all attachments")
440
+
441
+ def toggle_favorite(self, relative_path: str) -> bool:
442
+ """Toggle favorite status for a prompt"""
443
+ if relative_path not in self.prompts:
444
+ return False
445
+
446
+ prompt_data = self.prompts[relative_path]
447
+ prompt_data['favorite'] = not prompt_data.get('favorite', False)
448
+ prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
449
+
450
+ # Save updated prompt
451
+ self.save_prompt(relative_path, prompt_data)
452
+ self._update_favorites_list()
453
+
454
+ return True
455
+
456
+ def toggle_quick_run(self, relative_path: str) -> bool:
457
+ """Toggle QuickMenu (future app menu) status for a prompt (legacy name: quick_run)."""
458
+ if relative_path not in self.prompts:
459
+ return False
460
+
461
+ prompt_data = self.prompts[relative_path]
462
+ new_value = not bool(prompt_data.get('sv_quickmenu', prompt_data.get('quick_run', False)))
463
+ prompt_data['sv_quickmenu'] = new_value
464
+ prompt_data['quick_run'] = new_value # keep legacy in sync
465
+ prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
466
+
467
+ # Save updated prompt
468
+ self.save_prompt(relative_path, prompt_data)
469
+ self._update_quick_run_list()
470
+ self._update_quickmenu_grid_list()
471
+
472
+ return True
473
+
474
+ def toggle_quickmenu_grid(self, relative_path: str) -> bool:
475
+ """Toggle whether this prompt appears in the Grid right-click QuickMenu."""
476
+ if relative_path not in self.prompts:
477
+ return False
478
+
479
+ prompt_data = self.prompts[relative_path]
480
+ prompt_data['quickmenu_grid'] = not bool(prompt_data.get('quickmenu_grid', False))
481
+ prompt_data['modified'] = datetime.now().strftime("%Y-%m-%d")
482
+
483
+ self.save_prompt(relative_path, prompt_data)
484
+ self._update_quickmenu_grid_list()
485
+ return True
486
+
487
+ def _update_favorites_list(self):
488
+ """Update cached favorites list"""
489
+ self._favorites = [
490
+ (path, data.get('name', Path(path).stem))
491
+ for path, data in self.prompts.items()
492
+ if data.get('favorite', False)
493
+ ]
494
+
495
+ def _update_quick_run_list(self):
496
+ """Update cached QuickMenu (future app menu) list (legacy name: quick_run)."""
497
+ self._quick_run = []
498
+ for path, data in self.prompts.items():
499
+ is_enabled = bool(data.get('sv_quickmenu', data.get('quick_run', False)))
500
+ if not is_enabled:
501
+ continue
502
+ label = (data.get('quickmenu_label') or data.get('name') or Path(path).stem).strip()
503
+ self._quick_run.append((path, label))
504
+
505
+ def _update_quickmenu_grid_list(self):
506
+ """Update cached Grid QuickMenu list."""
507
+ self._quickmenu_grid = []
508
+ for path, data in self.prompts.items():
509
+ if not bool(data.get('quickmenu_grid', False)):
510
+ continue
511
+ label = (data.get('quickmenu_label') or data.get('name') or Path(path).stem).strip()
512
+ self._quickmenu_grid.append((path, label))
513
+
514
+ def get_favorites(self) -> List[Tuple[str, str]]:
515
+ """Get list of favorite prompts (path, name)"""
516
+ return self._favorites
517
+
518
+ def get_quick_run_prompts(self) -> List[Tuple[str, str]]:
519
+ """Get list of QuickMenu (future app menu) prompts (path, label)."""
520
+ return self._quick_run
521
+
522
+ def get_quickmenu_prompts(self) -> List[Tuple[str, str]]:
523
+ """Alias for get_quick_run_prompts(), using the new naming."""
524
+ return self.get_quick_run_prompts()
525
+
526
+ def get_quickmenu_grid_prompts(self) -> List[Tuple[str, str]]:
527
+ """Get list of prompts shown in the Grid right-click QuickMenu (path, label)."""
528
+ return self._quickmenu_grid
529
+
530
+ def create_folder(self, folder_path: str) -> bool:
531
+ """Create a new folder in the library"""
532
+ try:
533
+ if not self.library_dir:
534
+ return False
535
+
536
+ full_path = self.library_dir / folder_path
537
+ full_path.mkdir(parents=True, exist_ok=True)
538
+
539
+ self.log(f"✓ Created folder: {folder_path}")
540
+ return True
541
+
542
+ except Exception as e:
543
+ self.log(f"✗ Failed to create folder: {e}")
544
+ return False
545
+
546
+ def move_prompt(self, old_path: str, new_path: str) -> bool:
547
+ """Move a prompt to a different folder"""
548
+ try:
549
+ if old_path not in self.prompts:
550
+ return False
551
+
552
+ old_file = Path(self.prompts[old_path]['_filepath'])
553
+ new_file = self.library_dir / new_path
554
+
555
+ # Create destination folder
556
+ new_file.parent.mkdir(parents=True, exist_ok=True)
557
+
558
+ # Move file
559
+ shutil.move(str(old_file), str(new_file))
560
+
561
+ # Update in-memory storage
562
+ prompt_data = self.prompts.pop(old_path)
563
+ prompt_data['_filepath'] = str(new_file)
564
+ prompt_data['_relative_path'] = new_path
565
+ self.prompts[new_path] = prompt_data
566
+
567
+ # Update active references if needed
568
+ if self.active_primary_prompt_path == old_path:
569
+ self.active_primary_prompt_path = new_path
570
+
571
+ if old_path in self.attached_prompt_paths:
572
+ idx = self.attached_prompt_paths.index(old_path)
573
+ self.attached_prompt_paths[idx] = new_path
574
+
575
+ self.log(f"✓ Moved: {old_path} → {new_path}")
576
+ return True
577
+
578
+ except Exception as e:
579
+ self.log(f"✗ Failed to move prompt: {e}")
580
+ return False
581
+
582
+ def move_folder(self, old_folder: str, new_folder: str) -> bool:
583
+ """Move a folder (and all contained prompts/subfolders) within the library."""
584
+ try:
585
+ if not self.library_dir:
586
+ return False
587
+
588
+ old_folder = old_folder or ""
589
+ new_folder = new_folder or ""
590
+
591
+ old_dir = self.library_dir / old_folder
592
+ new_dir = self.library_dir / new_folder
593
+
594
+ if not old_dir.exists() or not old_dir.is_dir():
595
+ return False
596
+
597
+ # Prevent moving a folder into itself / a descendant
598
+ old_parts = Path(old_folder).parts
599
+ new_parts = Path(new_folder).parts
600
+ if old_parts and len(new_parts) >= len(old_parts) and new_parts[:len(old_parts)] == old_parts:
601
+ return False
602
+
603
+ new_dir.parent.mkdir(parents=True, exist_ok=True)
604
+ shutil.move(str(old_dir), str(new_dir))
605
+
606
+ old_prefix = f"{old_folder}/" if old_folder else ""
607
+ new_prefix = f"{new_folder}/" if new_folder else ""
608
+
609
+ def rewrite_path(path: Optional[str]) -> Optional[str]:
610
+ if not path:
611
+ return path
612
+ if old_folder and (path == old_folder or path.startswith(old_prefix)):
613
+ return new_folder + path[len(old_folder):]
614
+ if not old_folder and path:
615
+ # moving root is not supported
616
+ return path
617
+ return path
618
+
619
+ # Update active references (paths only). Caller should reload prompts.
620
+ self.active_primary_prompt_path = rewrite_path(self.active_primary_prompt_path)
621
+
622
+ new_attached = []
623
+ for p in self.attached_prompt_paths:
624
+ new_attached.append(rewrite_path(p))
625
+ self.attached_prompt_paths = new_attached
626
+
627
+ self.log(f"✓ Moved folder: {old_folder} → {new_folder}")
628
+ return True
629
+
630
+ except Exception as e:
631
+ self.log(f"✗ Failed to move folder: {e}")
632
+ return False
633
+
634
+ def delete_prompt(self, relative_path: str) -> bool:
635
+ """Delete a prompt"""
636
+ try:
637
+ if relative_path not in self.prompts:
638
+ return False
639
+
640
+ filepath = Path(self.prompts[relative_path]['_filepath'])
641
+ filepath.unlink()
642
+
643
+ # Remove from memory
644
+ del self.prompts[relative_path]
645
+
646
+ # Clear from active if needed
647
+ if self.active_primary_prompt_path == relative_path:
648
+ self.active_primary_prompt = None
649
+ self.active_primary_prompt_path = None
650
+
651
+ if relative_path in self.attached_prompt_paths:
652
+ self.detach_prompt(relative_path)
653
+
654
+ self.log(f"✓ Deleted: {relative_path}")
655
+ return True
656
+
657
+ except Exception as e:
658
+ self.log(f"✗ Failed to delete prompt: {e}")
659
+ return False