conductor(checkpoint): Checkpoint end of Phase 2 - Text Input & Control Undo/Redo
This commit is contained in:
+130
@@ -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
|
||||
|
||||
@@ -2,6 +2,55 @@ import typing
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass
|
||||
class UISnapshot:
|
||||
"""Capture of restorable UI state."""
|
||||
ai_input: str
|
||||
project_system_prompt: str
|
||||
global_system_prompt: str
|
||||
base_system_prompt: str
|
||||
use_default_base_prompt: bool
|
||||
temperature: float
|
||||
top_p: float
|
||||
max_tokens: int
|
||||
auto_add_history: bool
|
||||
disc_entries: list[dict]
|
||||
files: list[dict]
|
||||
screenshots: list[str]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"ai_input": self.ai_input,
|
||||
"project_system_prompt": self.project_system_prompt,
|
||||
"global_system_prompt": self.global_system_prompt,
|
||||
"base_system_prompt": self.base_system_prompt,
|
||||
"use_default_base_prompt": self.use_default_base_prompt,
|
||||
"temperature": self.temperature,
|
||||
"top_p": self.top_p,
|
||||
"max_tokens": self.max_tokens,
|
||||
"auto_add_history": self.auto_add_history,
|
||||
"disc_entries": self.disc_entries,
|
||||
"files": self.files,
|
||||
"screenshots": self.screenshots
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "UISnapshot":
|
||||
return cls(
|
||||
ai_input=data.get("ai_input", ""),
|
||||
project_system_prompt=data.get("project_system_prompt", ""),
|
||||
global_system_prompt=data.get("global_system_prompt", ""),
|
||||
base_system_prompt=data.get("base_system_prompt", ""),
|
||||
use_default_base_prompt=data.get("use_default_base_prompt", True),
|
||||
temperature=data.get("temperature", 0.0),
|
||||
top_p=data.get("top_p", 1.0),
|
||||
max_tokens=data.get("max_tokens", 4096),
|
||||
auto_add_history=data.get("auto_add_history", False),
|
||||
disc_entries=data.get("disc_entries", []),
|
||||
files=data.get("files", []),
|
||||
screenshots=data.get("screenshots", [])
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class HistoryEntry:
|
||||
state: typing.Any
|
||||
|
||||
Reference in New Issue
Block a user