conductor(checkpoint): Checkpoint end of Phase 2 - Text Input & Control Undo/Redo

This commit is contained in:
2026-05-05 00:23:55 -04:00
parent 8513604539
commit a02849b9a3
4 changed files with 263 additions and 0 deletions
+130
View File
@@ -103,6 +103,14 @@ class App:
def __init__(self) -> None:
# Initialize controller and delegate state
self.controller = app_controller.AppController()
from src import history
self.history = history.HistoryManager(max_capacity=100)
self._last_ui_snapshot: Optional[history.UISnapshot] = None
self._snapshot_timer: float = 0.0
self._snapshot_debounce: float = 1.5
self._pending_snapshot: bool = False
self._is_applying_snapshot: bool = False
# Restore legacy PROVIDERS to controller if needed (it already has it via delegation if set on class level, but let's be explicit)
if not hasattr(self.controller, 'PROVIDERS'):
self.controller.PROVIDERS = PROVIDERS
@@ -114,6 +122,10 @@ class App:
self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset
self.controller._predefined_callbacks['set_ui_file_paths'] = lambda p: setattr(self, 'ui_file_paths', p)
self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p)
self.controller._clickable_actions.update({
'btn_undo': self._handle_undo,
'btn_redo': self._handle_redo
})
def simulate_save_preset(name: str):
from src import models
self.files = [models.FileItem(path='test.py')]
@@ -287,6 +299,68 @@ class App:
def perf_profiling_enabled(self, value: bool) -> None:
self.controller.perf_profiling_enabled = value
def _take_snapshot(self) -> history.UISnapshot:
from src import history
import copy
return history.UISnapshot(
ai_input=self.ui_ai_input,
project_system_prompt=self.ui_project_system_prompt,
global_system_prompt=self.ui_global_system_prompt,
base_system_prompt=self.ui_base_system_prompt,
use_default_base_prompt=self.ui_use_default_base_prompt,
temperature=self.temperature,
top_p=self.top_p,
max_tokens=self.max_tokens,
auto_add_history=self.ui_auto_add_history,
disc_entries=copy.deepcopy(self.disc_entries),
files=[f.to_dict() if hasattr(f, 'to_dict') else f for f in self.files],
screenshots=list(self.screenshots)
)
def _apply_snapshot(self, snapshot: history.UISnapshot) -> None:
self._is_applying_snapshot = True
try:
self.ui_ai_input = snapshot.ai_input
self.ui_project_system_prompt = snapshot.project_system_prompt
self.ui_global_system_prompt = snapshot.global_system_prompt
self.ui_base_system_prompt = snapshot.base_system_prompt
self.ui_use_default_base_prompt = snapshot.use_default_base_prompt
self.temperature = snapshot.temperature
self.top_p = snapshot.top_p
self.max_tokens = snapshot.max_tokens
self.ui_auto_add_history = snapshot.auto_add_history
self.disc_entries = snapshot.disc_entries
# Restore files as FileItem objects
from src import models
self.files = []
for f in snapshot.files:
if isinstance(f, dict):
self.files.append(models.FileItem.from_dict(f))
else:
self.files.append(models.FileItem(path=str(f)))
self.screenshots = list(snapshot.screenshots)
self._last_ui_snapshot = snapshot # Update last snapshot to avoid immediate re-push
finally:
self._is_applying_snapshot = False
def _handle_undo(self) -> None:
if not self.history.can_undo:
return
current = self._take_snapshot()
entry = self.history.undo(current, "Undo Action")
if entry:
self._apply_snapshot(entry.state)
def _handle_redo(self) -> None:
if not self.history.can_redo:
return
current = self._take_snapshot()
entry = self.history.redo(current, "Redo Action")
if entry:
self._apply_snapshot(entry.state)
def shutdown(self) -> None:
"""Cleanly shuts down the app's background tasks and saves state."""
try:
@@ -1131,8 +1205,64 @@ class App:
if pushed_prior_tint:
imgui.pop_style_color()
self._handle_history_logic()
if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func")
def _handle_history_logic(self) -> None:
if self._is_applying_snapshot:
return
io = imgui.get_io()
# 1. Hotkey handling (Undo: Ctrl+Z, Redo: Ctrl+Y or Ctrl+Shift+Z)
ctrl = io.key_ctrl
shift = io.key_shift
if ctrl:
if imgui.is_key_pressed(imgui.Key.z):
if shift:
self._handle_redo()
else:
self._handle_undo()
elif imgui.is_key_pressed(imgui.Key.y):
self._handle_redo()
# 2. Debounced snapshotting
current = self._take_snapshot()
if self._last_ui_snapshot is None:
self._last_ui_snapshot = current
return
# Compare only core fields for performance
changed = (
current.ai_input != self._last_ui_snapshot.ai_input or
current.project_system_prompt != self._last_ui_snapshot.project_system_prompt or
current.global_system_prompt != self._last_ui_snapshot.global_system_prompt or
current.base_system_prompt != self._last_ui_snapshot.base_system_prompt or
current.use_default_base_prompt != self._last_ui_snapshot.use_default_base_prompt or
current.temperature != self._last_ui_snapshot.temperature or
current.top_p != self._last_ui_snapshot.top_p or
current.max_tokens != self._last_ui_snapshot.max_tokens or
current.auto_add_history != self._last_ui_snapshot.auto_add_history or
len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries)
)
if changed:
if not self._pending_snapshot:
self._pending_snapshot = True
self._snapshot_timer = time.time()
self._state_to_push = self._last_ui_snapshot
else:
self._snapshot_timer = time.time()
self._last_ui_snapshot = current
if self._pending_snapshot and (time.time() - self._snapshot_timer > self._snapshot_debounce):
if self._state_to_push:
self.history.push(self._state_to_push, "UI State Change")
self._pending_snapshot = False
def _render_base_prompt_diff_modal(self) -> None:
if not getattr(self.controller, "_show_base_prompt_diff_modal", False):
return