Skip to content

Commit bd0b60e

Browse files
feat: Add MCP and AI features documentation
- Introduced a comprehensive guide for the AI Panel, Auto Voiceover, and MCP Agent Mode in a new MCP.md file. - Documented the changes in the AI Panel, voiceover capabilities, and the new MCP Agent Mode for enhanced user experience. chore: Update README with installation and deployment instructions - Added detailed installation and deployment instructions in README_IMPROVEMENTS.md for version 2.0.2. - Highlighted new features, configuration steps, and troubleshooting tips. feat: Implement unified keybinding system - Created keybinding_registry.py to manage keybindings with a single source of truth. - Added dynamic rebinding, conflict detection, and persistence for user-defined shortcuts. feat: Develop unified keybindings panel - Introduced keybindings_panel.py for editing keybindings with conflict detection and live updates. - Enhanced user interface for better accessibility and usability of keybindings.
1 parent 2fced2c commit bd0b60e

11 files changed

Lines changed: 2467 additions & 191 deletions

CHANGES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
---
44

5-
## v2.0.1 — Structural Cleanup & Rendering Integrity Update
5+
## v2.0.2 — Structural Cleanup & Rendering Integrity Update
66

77
**Date:** February 28, 2026
88

@@ -99,7 +99,7 @@ All built-in snippet templates updated:
9999

100100
---
101101

102-
## v2.0.1 — Major Feature Release
102+
## v2.0.2 — Major Feature Release
103103

104104
Initial production release including:
105105

autosave_manager.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
autosave_manager.py — Production-grade autosave system
3+
4+
Implements debounced 3-second autosave with hash-based change detection.
5+
6+
Key Features:
7+
- Hash-based change detection (code, graph, assets, keybindings)
8+
- 3-second debounced timer (no disk thrashing)
9+
- Automatic save triggers for specific events
10+
- Silent operation (no dialogs)
11+
- Respects user's last saved version as fallback
12+
"""
13+
14+
import hashlib
15+
import logging
16+
from typing import Optional, Callable
17+
from pathlib import Path
18+
19+
try:
20+
from PySide6.QtCore import QTimer, QObject, Signal
21+
HAS_PYSIDE = True
22+
except ImportError:
23+
HAS_PYSIDE = False
24+
QObject = object
25+
Signal = None
26+
27+
28+
LOGGER = logging.getLogger("autosave")
29+
30+
# Autosave interval in milliseconds (3 seconds)
31+
AUTOSAVE_INTERVAL_MS = 3000
32+
33+
34+
class AutosaveManager(QObject if HAS_PYSIDE else object):
35+
"""
36+
Manages automatic saving of projects with intelligent change detection.
37+
38+
Never saves unnecessarily — only when state actually changes.
39+
Uses debouncing to prevent rapid successive saves.
40+
"""
41+
42+
if HAS_PYSIDE:
43+
autosave_triggered = Signal(str) # (reason)
44+
45+
def __init__(self):
46+
if HAS_PYSIDE:
47+
super().__init__()
48+
49+
self._save_callback: Optional[Callable[[], None]] = None
50+
self._timer: Optional[QTimer] = None
51+
52+
# Hash tracking
53+
self._last_code_hash = ""
54+
self._last_graph_hash = ""
55+
self._last_assets_hash = ""
56+
self._last_keybindings_hash = ""
57+
58+
# Compute functions (will be set by EfficientManimWindow)
59+
self._compute_code_hash: Optional[Callable[[], str]] = None
60+
self._compute_graph_hash: Optional[Callable[[], str]] = None
61+
self._compute_assets_hash: Optional[Callable[[], str]] = None
62+
self._compute_keybindings_hash: Optional[Callable[[], str]] = None
63+
64+
self._enabled = False
65+
66+
if HAS_PYSIDE:
67+
self._setup_timer()
68+
69+
def _setup_timer(self) -> None:
70+
"""Initialize the autosave timer."""
71+
self._timer = QTimer()
72+
self._timer.setSingleShot(True) # Debounce: only fire once per interval
73+
self._timer.setInterval(AUTOSAVE_INTERVAL_MS)
74+
self._timer.timeout.connect(self._on_timer_timeout)
75+
LOGGER.debug(f"Autosave timer configured: {AUTOSAVE_INTERVAL_MS}ms interval")
76+
77+
def set_save_callback(self, callback: Callable[[], None]) -> None:
78+
"""Register the save function to call on autosave."""
79+
self._save_callback = callback
80+
LOGGER.debug("Autosave callback registered")
81+
82+
def set_hash_computers(
83+
self,
84+
code_fn: Callable[[], str],
85+
graph_fn: Callable[[], str],
86+
assets_fn: Callable[[], str],
87+
keybindings_fn: Callable[[], str],
88+
) -> None:
89+
"""Register hash computation functions."""
90+
self._compute_code_hash = code_fn
91+
self._compute_graph_hash = graph_fn
92+
self._compute_assets_hash = assets_fn
93+
self._compute_keybindings_hash = keybindings_fn
94+
LOGGER.debug("Hash computer functions registered")
95+
96+
def enable(self) -> None:
97+
"""Enable autosave."""
98+
self._enabled = True
99+
LOGGER.info("Autosave enabled")
100+
101+
def disable(self) -> None:
102+
"""Disable autosave."""
103+
self._enabled = False
104+
if HAS_PYSIDE and self._timer:
105+
self._timer.stop()
106+
LOGGER.info("Autosave disabled")
107+
108+
def trigger_autosave(self, reason: str = "change detected") -> None:
109+
"""
110+
Request autosave with debouncing.
111+
112+
Won't save immediately — will wait for quiet period,
113+
then check if state actually changed before saving.
114+
"""
115+
if not self._enabled:
116+
return
117+
118+
if HAS_PYSIDE and self._timer:
119+
self._timer.stop()
120+
self._timer.start() # Reset debounce timer
121+
LOGGER.debug(f"Autosave scheduled ({reason})")
122+
123+
def _on_timer_timeout(self) -> None:
124+
"""Timer fired — check if state changed, then save."""
125+
if not self._enabled or not self._save_callback:
126+
return
127+
128+
try:
129+
# Compute current hashes
130+
code_hash = self._compute_code_hash() if self._compute_code_hash else ""
131+
graph_hash = self._compute_graph_hash() if self._compute_graph_hash else ""
132+
assets_hash = self._compute_assets_hash() if self._compute_assets_hash else ""
133+
keybindings_hash = self._compute_keybindings_hash() if self._compute_keybindings_hash else ""
134+
135+
# Check if anything changed
136+
code_changed = (code_hash != self._last_code_hash)
137+
graph_changed = (graph_hash != self._last_graph_hash)
138+
assets_changed = (assets_hash != self._last_assets_hash)
139+
keybindings_changed = (keybindings_hash != self._last_keybindings_hash)
140+
141+
if not (code_changed or graph_changed or assets_changed or keybindings_changed):
142+
LOGGER.debug("No changes detected, skipping autosave")
143+
return
144+
145+
# State changed — save now
146+
reason_parts = []
147+
if code_changed:
148+
reason_parts.append("code")
149+
if graph_changed:
150+
reason_parts.append("graph")
151+
if assets_changed:
152+
reason_parts.append("assets")
153+
if keybindings_changed:
154+
reason_parts.append("keybindings")
155+
156+
reason = f"autosave ({', '.join(reason_parts)})"
157+
158+
# Update hashes for next check
159+
self._last_code_hash = code_hash
160+
self._last_graph_hash = graph_hash
161+
self._last_assets_hash = assets_hash
162+
self._last_keybindings_hash = keybindings_hash
163+
164+
# Perform the save
165+
self._save_callback()
166+
167+
if HAS_PYSIDE:
168+
self.autosave_triggered.emit(reason)
169+
170+
LOGGER.info(f"✓ Autosave completed ({', '.join(reason_parts)})")
171+
172+
except Exception as e:
173+
LOGGER.error(f"Autosave failed: {e}")
174+
175+
def force_save(self) -> None:
176+
"""Force immediate save (bypass debounce)."""
177+
if HAS_PYSIDE and self._timer:
178+
self._timer.stop()
179+
self._on_timer_timeout()
180+
181+
182+
# Global autosave manager instance
183+
AUTOSAVE = AutosaveManager()

0 commit comments

Comments
 (0)