supervertaler 1.9.131__py3-none-any.whl → 1.9.173__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.
modules/superbrowser.py CHANGED
@@ -45,11 +45,12 @@ def _clear_corrupted_cache(storage_path: str):
45
45
  class ChatColumn(QWidget):
46
46
  """A column containing a chat interface with web browser"""
47
47
 
48
- def __init__(self, title, url, header_color, parent=None):
48
+ def __init__(self, title, url, header_color, parent=None, user_data_path=None):
49
49
  super().__init__(parent)
50
50
  self.title = title
51
51
  self.url = url
52
52
  self.header_color = header_color
53
+ self.user_data_path = user_data_path # Store user data path
53
54
  self.init_ui()
54
55
 
55
56
  def init_ui(self):
@@ -102,12 +103,14 @@ class ChatColumn(QWidget):
102
103
  profile_name = f"superbrowser_{self.title.lower()}"
103
104
  self.profile = QWebEngineProfile(profile_name, self)
104
105
 
105
- # Set persistent storage path (use same pattern as main app)
106
- # Check if running in dev mode (private data folder)
107
- dev_mode_marker = os.path.join(os.path.dirname(__file__), "..", ".supervertaler.local")
108
- base_folder = "user_data_private" if os.path.exists(dev_mode_marker) else "user_data"
109
-
110
- storage_path = os.path.join(os.path.dirname(__file__), "..", base_folder, "superbrowser_profiles", profile_name)
106
+ # Set persistent storage path using user_data_path from parent
107
+ if self.user_data_path:
108
+ storage_path = os.path.join(str(self.user_data_path), "superbrowser_profiles", profile_name)
109
+ else:
110
+ # Fallback to script directory if user_data_path not provided
111
+ dev_mode_marker = os.path.join(os.path.dirname(__file__), "..", ".supervertaler.local")
112
+ base_folder = "user_data_private" if os.path.exists(dev_mode_marker) else "user_data"
113
+ storage_path = os.path.join(os.path.dirname(__file__), "..", base_folder, "superbrowser_profiles", profile_name)
111
114
  os.makedirs(storage_path, exist_ok=True)
112
115
 
113
116
  # Clear potentially corrupted cache to prevent Chromium errors
@@ -166,9 +169,10 @@ class SuperbrowserWidget(QWidget):
166
169
  and concurrent interaction with different AI models.
167
170
  """
168
171
 
169
- def __init__(self, parent=None):
172
+ def __init__(self, parent=None, user_data_path=None):
170
173
  super().__init__(parent)
171
174
  self.parent_window = parent
175
+ self.user_data_path = user_data_path # Store user data path for profiles
172
176
 
173
177
  # Default URLs for AI chat interfaces
174
178
  self.chatgpt_url = "https://chatgpt.com/"
@@ -257,10 +261,10 @@ class SuperbrowserWidget(QWidget):
257
261
  splitter = QSplitter(Qt.Orientation.Horizontal)
258
262
  splitter.setHandleWidth(3)
259
263
 
260
- # Create chat columns
261
- self.chatgpt_column = ChatColumn("ChatGPT", self.chatgpt_url, "#10a37f", self)
262
- self.claude_column = ChatColumn("Claude", self.claude_url, "#c17c4f", self)
263
- self.gemini_column = ChatColumn("Gemini", self.gemini_url, "#4285f4", self)
264
+ # Create chat columns - pass user_data_path for profile storage
265
+ self.chatgpt_column = ChatColumn("ChatGPT", self.chatgpt_url, "#10a37f", self, user_data_path=self.user_data_path)
266
+ self.claude_column = ChatColumn("Claude", self.claude_url, "#c17c4f", self, user_data_path=self.user_data_path)
267
+ self.gemini_column = ChatColumn("Gemini", self.gemini_url, "#4285f4", self, user_data_path=self.user_data_path)
264
268
 
265
269
  # Add columns to splitter
266
270
  splitter.addWidget(self.chatgpt_column)
modules/superlookup.py CHANGED
@@ -88,14 +88,18 @@ class SuperlookupEngine:
88
88
  Captured text or None if failed
89
89
  """
90
90
  try:
91
- import keyboard
92
-
93
- # Wait for hotkey to release before sending Ctrl+C
94
- time.sleep(0.2)
95
-
96
- # Use keyboard library to send Ctrl+C
97
- keyboard.press_and_release('ctrl+c')
98
- time.sleep(0.2)
91
+ # keyboard module is Windows-only
92
+ try:
93
+ import keyboard
94
+ # Wait for hotkey to release before sending Ctrl+C
95
+ time.sleep(0.2)
96
+ # Use keyboard library to send Ctrl+C
97
+ keyboard.press_and_release('ctrl+c')
98
+ time.sleep(0.2)
99
+ except ImportError:
100
+ # On non-Windows, just try to get clipboard content directly
101
+ # (user needs to have copied text manually)
102
+ pass
99
103
 
100
104
  # Get clipboard
101
105
  text = pyperclip.paste()
@@ -157,9 +161,13 @@ class SuperlookupEngine:
157
161
 
158
162
  # Convert to LookupResult format (limit results)
159
163
  for match in matches[:max_results]:
164
+ # Use 'source' and 'target' keys (matches database column names)
165
+ source_text = match.get('source', '')
166
+ target_text = match.get('target', '')
167
+ print(f"[Superlookup] Extracted: source='{source_text[:50]}...', target='{target_text[:50]}...'")
160
168
  results.append(LookupResult(
161
- source=match.get('source', ''),
162
- target=match.get('target', ''),
169
+ source=source_text,
170
+ target=target_text,
163
171
  match_percent=100, # Concordance = contains the text
164
172
  source_type='tm',
165
173
  metadata={
modules/tag_manager.py CHANGED
@@ -77,15 +77,33 @@ class TagManager:
77
77
  runs = []
78
78
  current_pos = 0
79
79
 
80
+ # Check if paragraph style has bold/italic formatting
81
+ # This handles cases like "Subtitle" or "Title" styles that are bold
82
+ style_bold = False
83
+ style_italic = False
84
+ try:
85
+ if paragraph.style and paragraph.style.font:
86
+ if paragraph.style.font.bold:
87
+ style_bold = True
88
+ if paragraph.style.font.italic:
89
+ style_italic = True
90
+ except Exception:
91
+ pass # If we can't read style, just use run-level formatting
92
+
80
93
  for run in paragraph.runs:
81
94
  text = run.text
82
95
  if not text:
83
96
  continue
84
97
 
98
+ # Combine run-level formatting with style-level formatting
99
+ # run.bold can be True, False, or None (None means inherit from style)
100
+ is_bold = run.bold if run.bold is not None else style_bold
101
+ is_italic = run.italic if run.italic is not None else style_italic
102
+
85
103
  run_info = FormattingRun(
86
104
  text=text,
87
- bold=run.bold or False,
88
- italic=run.italic or False,
105
+ bold=is_bold or False,
106
+ italic=is_italic or False,
89
107
  underline=run.underline or False,
90
108
  subscript=run.font.subscript or False if run.font else False,
91
109
  superscript=run.font.superscript or False if run.font else False,
@@ -515,6 +515,9 @@ class TermviewWidget(QWidget):
515
515
  self.current_target_lang = None
516
516
  self.current_project_id = None # Store project ID for termbase priority lookup
517
517
 
518
+ # Debug mode - disable verbose tokenization logging by default (performance)
519
+ self.debug_tokenize = False
520
+
518
521
  # Default font settings (will be updated from main app settings)
519
522
  self.current_font_family = "Segoe UI"
520
523
  self.current_font_size = 10
@@ -750,7 +753,10 @@ class TermviewWidget(QWidget):
750
753
  if not source_term or not target_term:
751
754
  continue
752
755
 
753
- key = source_term.lower()
756
+ # Strip punctuation from key to match lookup normalization
757
+ # This ensures "ca." in glossary matches "ca." token stripped to "ca"
758
+ PUNCT_CHARS_FOR_KEY = '.,;:!?\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A()[]'
759
+ key = source_term.lower().strip(PUNCT_CHARS_FOR_KEY)
754
760
  if key not in matches_dict:
755
761
  matches_dict[key] = []
756
762
 
@@ -803,7 +809,8 @@ class TermviewWidget(QWidget):
803
809
 
804
810
  # Comprehensive set of quote and punctuation characters to strip
805
811
  # Using Unicode escapes to avoid encoding issues
806
- PUNCT_CHARS = '.,;:!?\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A'
812
+ # Include brackets for terms like "(typisch)" to match "typisch"
813
+ PUNCT_CHARS = '.,;:!?\"\'\u201C\u201D\u201E\u00AB\u00BB\u2018\u2019\u201A\u2039\u203A()[]'
807
814
 
808
815
  # Track which terms have already been assigned shortcuts (avoid duplicates)
809
816
  assigned_shortcuts = set()
@@ -816,7 +823,6 @@ class TermviewWidget(QWidget):
816
823
 
817
824
  # Check if this is a non-translatable
818
825
  if lookup_key in nt_dict:
819
- # Create NT block
820
826
  nt_block = NTBlock(token, nt_dict[lookup_key], self, theme_manager=self.theme_manager,
821
827
  font_size=self.current_font_size, font_family=self.current_font_family,
822
828
  font_bold=self.current_font_bold)
@@ -992,9 +998,9 @@ class TermviewWidget(QWidget):
992
998
  Returns:
993
999
  List of tokens (words/phrases/numbers), with multi-word terms kept together
994
1000
  """
995
- # DEBUG: Log multi-word terms we're looking for
1001
+ # DEBUG: Log multi-word terms we're looking for (only if debug_tokenize enabled)
996
1002
  multi_word_terms = [k for k in matches.keys() if ' ' in k]
997
- if multi_word_terms:
1003
+ if multi_word_terms and self.debug_tokenize:
998
1004
  self.log(f"🔍 Tokenize: Looking for {len(multi_word_terms)} multi-word terms:")
999
1005
  for term in sorted(multi_word_terms, key=len, reverse=True)[:3]:
1000
1006
  self.log(f" - '{term}'")
@@ -1019,11 +1025,12 @@ class TermviewWidget(QWidget):
1019
1025
  else:
1020
1026
  pattern = r'\b' + term_escaped + r'\b'
1021
1027
 
1022
- # DEBUG: Check if multi-word term is found
1028
+ # DEBUG: Check if multi-word term is found (only if debug_tokenize enabled)
1023
1029
  found = re.search(pattern, text_lower)
1024
- self.log(f"🔍 Tokenize: Pattern '{pattern}' for '{term}' → {'FOUND' if found else 'NOT FOUND'}")
1025
- if found:
1026
- self.log(f" Match at position {found.span()}: '{text[found.start():found.end()]}'")
1030
+ if self.debug_tokenize:
1031
+ self.log(f"🔍 Tokenize: Pattern '{pattern}' for '{term}' → {'FOUND' if found else 'NOT FOUND'}")
1032
+ if found:
1033
+ self.log(f" Match at position {found.span()}: '{text[found.start():found.end()]}'")
1027
1034
 
1028
1035
  # Find all matches using regex
1029
1036
  for match in re.finditer(pattern, text_lower):
@@ -1036,10 +1043,11 @@ class TermviewWidget(QWidget):
1036
1043
  original_term = text[pos:pos + len(term)]
1037
1044
  tokens_with_positions.append((pos, len(term), original_term))
1038
1045
  used_positions.update(term_positions)
1039
- self.log(f" ✅ Added multi-word token: '{original_term}' covering positions {pos}-{pos+len(term)}")
1046
+ if self.debug_tokenize:
1047
+ self.log(f" ✅ Added multi-word token: '{original_term}' covering positions {pos}-{pos+len(term)}")
1040
1048
 
1041
- # DEBUG: Log used_positions after first pass
1042
- if ' ' in sorted(matches.keys(), key=len, reverse=True)[0]:
1049
+ # DEBUG: Log used_positions after first pass (only if debug_tokenize enabled)
1050
+ if matches and ' ' in sorted(matches.keys(), key=len, reverse=True)[0] and self.debug_tokenize:
1043
1051
  self.log(f"🔍 After first pass: {len(used_positions)} positions marked as used")
1044
1052
  self.log(f" Used positions: {sorted(list(used_positions))[:20]}...")
1045
1053
 
@@ -396,6 +396,47 @@ class TMMetadataManager:
396
396
  self.log(f"✗ Error fetching active tm_ids: {e}")
397
397
  return []
398
398
 
399
+ def get_writable_tm_ids(self, project_id: Optional[int]) -> List[str]:
400
+ """
401
+ Get list of writable tm_id strings for a project.
402
+
403
+ Returns TMs where:
404
+ - The TM has an activation record for this project AND
405
+ - read_only = 0 (Write checkbox is enabled)
406
+
407
+ This is used for SAVING segments to TM, separate from get_active_tm_ids()
408
+ which is used for READING/matching from TM.
409
+
410
+ Returns:
411
+ List of tm_id strings that are writable for the project
412
+ """
413
+ if project_id is None:
414
+ # No project - return all writable TMs
415
+ try:
416
+ cursor = self.db_manager.cursor
417
+ cursor.execute("SELECT tm_id FROM translation_memories WHERE read_only = 0")
418
+ return [row[0] for row in cursor.fetchall()]
419
+ except Exception as e:
420
+ self.log(f"✗ Error fetching all writable tm_ids: {e}")
421
+ return []
422
+
423
+ try:
424
+ cursor = self.db_manager.cursor
425
+
426
+ # Return TMs where Write checkbox is enabled (read_only = 0)
427
+ # AND the TM has an activation record for this project
428
+ cursor.execute("""
429
+ SELECT tm.tm_id
430
+ FROM translation_memories tm
431
+ INNER JOIN tm_activation ta ON tm.id = ta.tm_id
432
+ WHERE ta.project_id = ? AND tm.read_only = 0
433
+ """, (project_id,))
434
+
435
+ return [row[0] for row in cursor.fetchall()]
436
+ except Exception as e:
437
+ self.log(f"✗ Error fetching writable tm_ids: {e}")
438
+ return []
439
+
399
440
  # ========================================================================
400
441
  # PROJECT TM MANAGEMENT (similar to termbases)
401
442
  # ========================================================================
modules/tmx_editor_qt.py CHANGED
@@ -2655,7 +2655,7 @@ if __name__ == "__main__":
2655
2655
  os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".supervertaler.local")
2656
2656
  )
2657
2657
  user_data_path = Path("user_data_private" if ENABLE_PRIVATE_FEATURES else "user_data")
2658
- db_path = user_data_path / "Translation_Resources" / "supervertaler.db"
2658
+ db_path = user_data_path / "resources" / "supervertaler.db"
2659
2659
 
2660
2660
  # Ensure database directory exists
2661
2661
  db_path.parent.mkdir(parents=True, exist_ok=True)
@@ -123,7 +123,7 @@ class TMDatabase:
123
123
  if source_lang and target_lang:
124
124
  self.set_tm_languages(source_lang, target_lang)
125
125
 
126
- # Global fuzzy threshold
126
+ # Global fuzzy threshold (75% minimum similarity for fuzzy matches)
127
127
  self.fuzzy_threshold = 0.75
128
128
 
129
129
  # TM metadata cache (populated from database as needed)
@@ -401,7 +401,7 @@ class TMDatabase:
401
401
 
402
402
  def load_tmx_file(self, filepath: str, src_lang: str, tgt_lang: str,
403
403
  tm_name: str = None, read_only: bool = False,
404
- strip_variants: bool = True) -> tuple[str, int]:
404
+ strip_variants: bool = True, progress_callback=None) -> tuple[str, int]:
405
405
  """
406
406
  Load TMX file into a new custom TM
407
407
 
@@ -412,6 +412,7 @@ class TMDatabase:
412
412
  tm_name: Custom name for TM (default: filename)
413
413
  read_only: Make TM read-only
414
414
  strip_variants: Match base languages ignoring regional variants (default: True)
415
+ progress_callback: Optional callback function(current, total, message) for progress updates
415
416
 
416
417
  Returns: (tm_id, entry_count)
417
418
  """
@@ -423,16 +424,18 @@ class TMDatabase:
423
424
  self.add_custom_tm(tm_name, tm_id, read_only=read_only)
424
425
 
425
426
  # Load TMX content
426
- loaded_count = self._load_tmx_into_db(filepath, src_lang, tgt_lang, tm_id, strip_variants=strip_variants)
427
+ loaded_count = self._load_tmx_into_db(filepath, src_lang, tgt_lang, tm_id,
428
+ strip_variants=strip_variants,
429
+ progress_callback=progress_callback)
427
430
 
428
431
  self.log(f"✓ Loaded {loaded_count} entries from {os.path.basename(filepath)}")
429
432
 
430
433
  return tm_id, loaded_count
431
434
 
432
435
  def _load_tmx_into_db(self, filepath: str, src_lang: str, tgt_lang: str, tm_id: str,
433
- strip_variants: bool = False) -> int:
436
+ strip_variants: bool = False, progress_callback=None) -> int:
434
437
  """
435
- Internal: Load TMX content into database
438
+ Internal: Load TMX content into database with chunked processing
436
439
 
437
440
  Args:
438
441
  filepath: Path to TMX file
@@ -440,12 +443,24 @@ class TMDatabase:
440
443
  tgt_lang: Target target language code
441
444
  tm_id: TM identifier
442
445
  strip_variants: If True, match base languages ignoring regional variants
446
+ progress_callback: Optional callback function(current, total, message) for progress updates
443
447
  """
444
448
  loaded_count = 0
449
+ chunk_size = 1000 # Process in chunks for responsiveness
450
+ chunk_buffer = []
445
451
 
446
452
  try:
453
+ # First pass: count total TUs for progress bar
454
+ if progress_callback:
455
+ progress_callback(0, 0, "Counting translation units...")
456
+
447
457
  tree = ET.parse(filepath)
448
458
  root = tree.getroot()
459
+ total_tus = len(root.findall('.//tu'))
460
+
461
+ if progress_callback:
462
+ progress_callback(0, total_tus, f"Processing 0 / {total_tus:,} entries...")
463
+
449
464
  xml_ns = "http://www.w3.org/XML/1998/namespace"
450
465
 
451
466
  # Normalize language codes
@@ -458,6 +473,7 @@ class TMDatabase:
458
473
  src_base = get_base_lang_code(src_lang_normalized)
459
474
  tgt_base = get_base_lang_code(tgt_lang_normalized)
460
475
 
476
+ processed = 0
461
477
  for tu in root.findall('.//tu'):
462
478
  src_text, tgt_text = None, None
463
479
 
@@ -488,14 +504,43 @@ class TMDatabase:
488
504
  tgt_text = text
489
505
 
490
506
  if src_text and tgt_text:
507
+ chunk_buffer.append((src_text, tgt_text))
508
+ loaded_count += 1
509
+
510
+ # Process chunk when buffer is full
511
+ if len(chunk_buffer) >= chunk_size:
512
+ for src, tgt in chunk_buffer:
513
+ self.db.add_translation_unit(
514
+ source=src,
515
+ target=tgt,
516
+ source_lang=src_lang_normalized,
517
+ target_lang=tgt_lang_normalized,
518
+ tm_id=tm_id
519
+ )
520
+ chunk_buffer.clear()
521
+
522
+ # Update progress
523
+ if progress_callback:
524
+ progress_callback(processed + 1, total_tus,
525
+ f"Processing {loaded_count:,} / {total_tus:,} entries...")
526
+
527
+ processed += 1
528
+
529
+ # Process remaining entries in buffer
530
+ if chunk_buffer:
531
+ for src, tgt in chunk_buffer:
491
532
  self.db.add_translation_unit(
492
- source=src_text,
493
- target=tgt_text,
533
+ source=src,
534
+ target=tgt,
494
535
  source_lang=src_lang_normalized,
495
536
  target_lang=tgt_lang_normalized,
496
537
  tm_id=tm_id
497
538
  )
498
- loaded_count += 1
539
+ chunk_buffer.clear()
540
+
541
+ # Final progress update
542
+ if progress_callback:
543
+ progress_callback(total_tus, total_tus, f"Completed: {loaded_count:,} entries imported")
499
544
 
500
545
  return loaded_count
501
546
  except Exception as e:
@@ -30,7 +30,7 @@ class UnifiedPromptLibrary:
30
30
  Initialize the Unified Prompt Library.
31
31
 
32
32
  Args:
33
- library_dir: Path to unified library directory (user_data/Prompt_Library/Library)
33
+ library_dir: Path to unified library directory (user_data/prompt_library)
34
34
  log_callback: Function to call for logging messages
35
35
  """
36
36
  self.library_dir = Path(library_dir) if library_dir else None
@@ -544,8 +544,8 @@ class UnifiedPromptManagerQt:
544
544
  self.log = parent_app.log if hasattr(parent_app, 'log') else print
545
545
 
546
546
  # Paths
547
- self.prompt_library_dir = self.user_data_path / "Prompt_Library"
548
- # Use Prompt_Library directly, not Prompt_Library/Library
547
+ self.prompt_library_dir = self.user_data_path / "prompt_library"
548
+ # Use prompt_library directly, not prompt_library/Library
549
549
  self.unified_library_dir = self.prompt_library_dir
550
550
 
551
551
  # Run migration if needed
@@ -579,7 +579,7 @@ class UnifiedPromptManagerQt:
579
579
  self._cached_document_markdown: Optional[str] = None # Cached markdown conversion of current document
580
580
 
581
581
  # Initialize Attachment Manager
582
- ai_assistant_dir = self.user_data_path / "AI_Assistant"
582
+ ai_assistant_dir = self.user_data_path / "ai_assistant"
583
583
  self.attachment_manager = AttachmentManager(
584
584
  base_dir=str(ai_assistant_dir),
585
585
  log_callback=self.log_message
@@ -694,7 +694,7 @@ class UnifiedPromptManagerQt:
694
694
  layout.setSpacing(5)
695
695
 
696
696
  # Title
697
- title = QLabel("🤖 Prompt Manager")
697
+ title = QLabel("📝 Prompt Manager")
698
698
  title.setStyleSheet("font-size: 16pt; font-weight: bold; color: #1976D2;")
699
699
  layout.addWidget(title, 0)
700
700
 
@@ -1546,7 +1546,7 @@ class UnifiedPromptManagerQt:
1546
1546
 
1547
1547
  def _create_active_config_panel(self) -> QGroupBox:
1548
1548
  """Create active prompt configuration panel"""
1549
- group = QGroupBox("Active Configuration")
1549
+ group = QGroupBox("Active Prompt")
1550
1550
  layout = QVBoxLayout()
1551
1551
 
1552
1552
  # Mode info (read-only, auto-selected)
@@ -1704,10 +1704,10 @@ class UnifiedPromptManagerQt:
1704
1704
  self.editor_quickmenu_label_input.setPlaceholderText("Label shown in QuickMenu")
1705
1705
  quickmenu_layout.addWidget(self.editor_quickmenu_label_input, 2)
1706
1706
 
1707
- self.editor_quickmenu_in_grid_cb = CheckmarkCheckBox("Show in Grid right-click QuickMenu")
1707
+ self.editor_quickmenu_in_grid_cb = CheckmarkCheckBox("Show in QuickMenu (in-app)")
1708
1708
  quickmenu_layout.addWidget(self.editor_quickmenu_in_grid_cb, 2)
1709
1709
 
1710
- self.editor_quickmenu_in_quickmenu_cb = CheckmarkCheckBox("Show in Supervertaler QuickMenu")
1710
+ self.editor_quickmenu_in_quickmenu_cb = CheckmarkCheckBox("Show in QuickMenu (global)")
1711
1711
  quickmenu_layout.addWidget(self.editor_quickmenu_in_quickmenu_cb, 1)
1712
1712
 
1713
1713
  layout.addLayout(quickmenu_layout)
@@ -1760,26 +1760,7 @@ class UnifiedPromptManagerQt:
1760
1760
  item.setFlags(item.flags() | Qt.ItemFlag.ItemIsDragEnabled)
1761
1761
  favorites_root.addChild(item)
1762
1762
 
1763
- # QuickMenu section (legacy kind name: quick_run)
1764
- quick_run_root = QTreeWidgetItem(["⚡ QuickMenu"])
1765
- # Special node: not draggable/droppable
1766
- quick_run_root.setData(0, Qt.ItemDataRole.UserRole, {'type': 'special', 'kind': 'quick_run'})
1767
- quick_run_root.setExpanded(False)
1768
- font = quick_run_root.font(0)
1769
- font.setBold(True)
1770
- quick_run_root.setFont(0, font)
1771
- self.tree_widget.addTopLevelItem(quick_run_root)
1772
-
1773
- quickmenu_items = self.library.get_quickmenu_prompts() if hasattr(self.library, 'get_quickmenu_prompts') else self.library.get_quick_run_prompts()
1774
- self.log_message(f"🔍 DEBUG: QuickMenu count: {len(quickmenu_items)}")
1775
- for path, label in quickmenu_items:
1776
- item = QTreeWidgetItem([label])
1777
- item.setData(0, Qt.ItemDataRole.UserRole, {'type': 'prompt', 'path': path})
1778
- # Quick Run entries are shortcuts, but allow dragging to move the actual prompt file.
1779
- item.setFlags(item.flags() | Qt.ItemFlag.ItemIsDragEnabled)
1780
- quick_run_root.addChild(item)
1781
-
1782
- # Library folders
1763
+ # Library folders (QuickMenu parent folder removed - folder hierarchy now defines menu structure)
1783
1764
  self.log_message(f"🔍 DEBUG: Building tree from {self.unified_library_dir}")
1784
1765
  self._build_tree_recursive(None, self.unified_library_dir, "")
1785
1766
 
@@ -3548,7 +3529,7 @@ Output complete ACTION."""
3548
3529
  """
3549
3530
  Save the markdown conversion of the current document.
3550
3531
 
3551
- Saves to: user_data_private/AI_Assistant/current_document/
3532
+ Saves to: user_data_private/ai_assistant/current_document/
3552
3533
 
3553
3534
  Args:
3554
3535
  original_path: Original document file path
@@ -3556,7 +3537,7 @@ Output complete ACTION."""
3556
3537
  """
3557
3538
  try:
3558
3539
  # Create directory for current document markdown
3559
- doc_dir = self.user_data_path / "AI_Assistant" / "current_document"
3540
+ doc_dir = self.user_data_path / "ai_assistant" / "current_document"
3560
3541
  doc_dir.mkdir(parents=True, exist_ok=True)
3561
3542
 
3562
3543
  # Create filename based on original