conductor(checkpoint): Checkpoint end of Phase 3 - Discussion & Context Structure Mutation
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def verify_phase_3():
|
||||||
|
print("Verifying Phase 3: Discussion & Context Structure Mutation...")
|
||||||
|
|
||||||
|
# Run the comprehensive simulation test
|
||||||
|
result = subprocess.run(
|
||||||
|
["uv", "run", "pytest", "tests/test_undo_redo_sim.py"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("Phase 3 verification PASSED.")
|
||||||
|
else:
|
||||||
|
print("Phase 3 verification FAILED.")
|
||||||
|
print(result.stdout)
|
||||||
|
print(result.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
verify_phase_3()
|
||||||
+21
-1
@@ -403,7 +403,9 @@ class AppController:
|
|||||||
'ui_separate_tier4': 'ui_separate_tier4',
|
'ui_separate_tier4': 'ui_separate_tier4',
|
||||||
'show_text_viewer': 'show_text_viewer',
|
'show_text_viewer': 'show_text_viewer',
|
||||||
'text_viewer_title': 'text_viewer_title',
|
'text_viewer_title': 'text_viewer_title',
|
||||||
'text_viewer_type': 'text_viewer_type'
|
'text_viewer_type': 'text_viewer_type',
|
||||||
|
'disc_entries': 'disc_entries',
|
||||||
|
'ui_file_paths': 'ui_file_paths'
|
||||||
}
|
}
|
||||||
self._gettable_fields = dict(self._settable_fields)
|
self._gettable_fields = dict(self._settable_fields)
|
||||||
self._gettable_fields.update({
|
self._gettable_fields.update({
|
||||||
@@ -575,6 +577,24 @@ class AppController:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ui_file_paths(self) -> list[str]:
|
||||||
|
return [f.path if hasattr(f, 'path') else str(f) for f in self.files]
|
||||||
|
|
||||||
|
@ui_file_paths.setter
|
||||||
|
def ui_file_paths(self, value: list[str]) -> None:
|
||||||
|
old_files = {f.path: f for f in self.files if hasattr(f, 'path')}
|
||||||
|
new_files = []
|
||||||
|
import time
|
||||||
|
now = time.time()
|
||||||
|
for p in value:
|
||||||
|
if p in old_files:
|
||||||
|
new_files.append(old_files[p])
|
||||||
|
else:
|
||||||
|
from src import models
|
||||||
|
new_files.append(models.FileItem(path=p, injected_at=now))
|
||||||
|
self.files = new_files
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def operations_live_indicator(self) -> bool:
|
def operations_live_indicator(self) -> bool:
|
||||||
return not self.is_viewing_prior_session
|
return not self.is_viewing_prior_session
|
||||||
|
|||||||
+25
-6
@@ -346,19 +346,27 @@ class App:
|
|||||||
self._is_applying_snapshot = False
|
self._is_applying_snapshot = False
|
||||||
|
|
||||||
def _handle_undo(self) -> None:
|
def _handle_undo(self) -> None:
|
||||||
|
sys.stderr.write(f"[DEBUG History] _handle_undo called. can_undo={self.history.can_undo}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
if not self.history.can_undo:
|
if not self.history.can_undo:
|
||||||
return
|
return
|
||||||
current = self._take_snapshot()
|
current = self._take_snapshot()
|
||||||
entry = self.history.undo(current, "Undo Action")
|
entry = self.history.undo(current, "Undo Action")
|
||||||
if entry:
|
if entry:
|
||||||
|
sys.stderr.write(f"[DEBUG History] Undoing to: {entry.description}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
self._apply_snapshot(entry.state)
|
self._apply_snapshot(entry.state)
|
||||||
|
|
||||||
def _handle_redo(self) -> None:
|
def _handle_redo(self) -> None:
|
||||||
|
sys.stderr.write(f"[DEBUG History] _handle_redo called. can_redo={self.history.can_redo}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
if not self.history.can_redo:
|
if not self.history.can_redo:
|
||||||
return
|
return
|
||||||
current = self._take_snapshot()
|
current = self._take_snapshot()
|
||||||
entry = self.history.redo(current, "Redo Action")
|
entry = self.history.redo(current, "Redo Action")
|
||||||
if entry:
|
if entry:
|
||||||
|
sys.stderr.write(f"[DEBUG History] Redoing to: {entry.description}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
self._apply_snapshot(entry.state)
|
self._apply_snapshot(entry.state)
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
@@ -400,6 +408,8 @@ class App:
|
|||||||
|
|
||||||
@ui_file_paths.setter
|
@ui_file_paths.setter
|
||||||
def ui_file_paths(self, paths: list[str]) -> None:
|
def ui_file_paths(self, paths: list[str]) -> None:
|
||||||
|
sys.stderr.write(f"[DEBUG] Setting ui_file_paths to: {paths}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
old_files = {f.path: f for f in self.files if hasattr(f, 'path')}
|
old_files = {f.path: f for f in self.files if hasattr(f, 'path')}
|
||||||
new_files = []
|
new_files = []
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -407,6 +417,7 @@ class App:
|
|||||||
if p in old_files:
|
if p in old_files:
|
||||||
new_files.append(old_files[p])
|
new_files.append(old_files[p])
|
||||||
else:
|
else:
|
||||||
|
from src import models
|
||||||
new_files.append(models.FileItem(path=p, injected_at=now))
|
new_files.append(models.FileItem(path=p, injected_at=now))
|
||||||
self.files = new_files
|
self.files = new_files
|
||||||
|
|
||||||
@@ -1210,9 +1221,10 @@ class App:
|
|||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func")
|
||||||
|
|
||||||
def _handle_history_logic(self) -> None:
|
def _handle_history_logic(self) -> None:
|
||||||
if self._is_applying_snapshot:
|
# Skip history tracking only during active AI thinking/tool execution
|
||||||
|
is_thinking = self.ai_status in ["sending...", "streaming...", "running powershell...", "fetching url...", "searching web..."]
|
||||||
|
if self._is_applying_snapshot or is_thinking:
|
||||||
return
|
return
|
||||||
|
|
||||||
io = imgui.get_io()
|
io = imgui.get_io()
|
||||||
|
|
||||||
# 1. Hotkey handling (Undo: Ctrl+Z, Redo: Ctrl+Y or Ctrl+Shift+Z)
|
# 1. Hotkey handling (Undo: Ctrl+Z, Redo: Ctrl+Y or Ctrl+Shift+Z)
|
||||||
@@ -1241,19 +1253,27 @@ class App:
|
|||||||
current.global_system_prompt != self._last_ui_snapshot.global_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.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.use_default_base_prompt != self._last_ui_snapshot.use_default_base_prompt or
|
||||||
current.temperature != self._last_ui_snapshot.temperature or
|
abs(current.temperature - self._last_ui_snapshot.temperature) > 1e-5 or
|
||||||
current.top_p != self._last_ui_snapshot.top_p or
|
abs(current.top_p - self._last_ui_snapshot.top_p) > 1e-5 or
|
||||||
current.max_tokens != self._last_ui_snapshot.max_tokens or
|
current.max_tokens != self._last_ui_snapshot.max_tokens or
|
||||||
current.auto_add_history != self._last_ui_snapshot.auto_add_history or
|
current.auto_add_history != self._last_ui_snapshot.auto_add_history or
|
||||||
len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries)
|
len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or
|
||||||
|
len(current.files) != len(self._last_ui_snapshot.files) or
|
||||||
|
len(current.screenshots) != len(self._last_ui_snapshot.screenshots)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not changed and len(current.disc_entries) > 0:
|
||||||
|
if current.disc_entries[-1].get('content') != self._last_ui_snapshot.disc_entries[-1].get('content'):
|
||||||
|
changed = True
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
if not self._pending_snapshot:
|
if not self._pending_snapshot:
|
||||||
self._pending_snapshot = True
|
self._pending_snapshot = True
|
||||||
self._snapshot_timer = time.time()
|
self._snapshot_timer = time.time()
|
||||||
|
# Capture state BEFORE current change
|
||||||
self._state_to_push = self._last_ui_snapshot
|
self._state_to_push = self._last_ui_snapshot
|
||||||
else:
|
else:
|
||||||
|
# Reset timer for settle debounce
|
||||||
self._snapshot_timer = time.time()
|
self._snapshot_timer = time.time()
|
||||||
|
|
||||||
self._last_ui_snapshot = current
|
self._last_ui_snapshot = current
|
||||||
@@ -1266,7 +1286,6 @@ class App:
|
|||||||
def _render_base_prompt_diff_modal(self) -> None:
|
def _render_base_prompt_diff_modal(self) -> None:
|
||||||
if not getattr(self.controller, "_show_base_prompt_diff_modal", False):
|
if not getattr(self.controller, "_show_base_prompt_diff_modal", False):
|
||||||
return
|
return
|
||||||
|
|
||||||
imgui.open_popup("Base Prompt Diff")
|
imgui.open_popup("Base Prompt Diff")
|
||||||
if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||||
imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt")
|
imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt")
|
||||||
|
|||||||
+90
-19
@@ -3,6 +3,7 @@ import time
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||||
@@ -15,46 +16,116 @@ def test_undo_redo_lifecycle(live_gui):
|
|||||||
assert client.wait_for_server(timeout=15), "Hook server did not start"
|
assert client.wait_for_server(timeout=15), "Hook server did not start"
|
||||||
|
|
||||||
# 1. Set initial state
|
# 1. Set initial state
|
||||||
|
print("Setting initial state...")
|
||||||
client.set_value('temperature', 0.5)
|
client.set_value('temperature', 0.5)
|
||||||
client.set_value('ai_input', "Initial Input")
|
client.set_value('ai_input', "Initial Input")
|
||||||
time.sleep(0.5) # Wait for debounce timer to start if it was triggered
|
|
||||||
|
|
||||||
# Trigger a snapshot by waiting for debounce
|
# Wait for settle and first push (S_init -> S0)
|
||||||
time.sleep(2.0)
|
time.sleep(3.0)
|
||||||
|
|
||||||
# 2. Change state
|
# 2. Change state
|
||||||
|
print("Modifying state...")
|
||||||
client.set_value('temperature', 1.5)
|
client.set_value('temperature', 1.5)
|
||||||
client.set_value('ai_input', "Modified Input")
|
client.set_value('ai_input', "Modified Input")
|
||||||
|
|
||||||
# Wait for debounce to push "Modified" state to history
|
# Wait for settle and second push (S0 -> S1)
|
||||||
time.sleep(2.0)
|
time.sleep(3.0)
|
||||||
|
|
||||||
# Verify current state
|
# Verify current state
|
||||||
assert client.get_value('temperature') == 1.5
|
temp = client.get_value('temperature')
|
||||||
assert client.get_value('ai_input') == "Modified Input"
|
ai_in = client.get_value('ai_input')
|
||||||
|
print(f"Current state: temp={temp}, ai_input={ai_in}")
|
||||||
|
assert temp == 1.5
|
||||||
|
assert ai_in == "Modified Input"
|
||||||
|
|
||||||
# 3. Undo
|
# 3. Undo
|
||||||
print("Sending Undo...")
|
print("Sending Undo...")
|
||||||
# Since we don't have a direct 'undo' hook, we can use 'click' if we added a button,
|
|
||||||
# or we can simulate the hotkey if the hook API supports it.
|
|
||||||
# Actually, I'll add a 'btn_undo' and 'btn_redo' to clickable actions for testing.
|
|
||||||
client.click('btn_undo')
|
client.click('btn_undo')
|
||||||
|
|
||||||
# Wait for state to revert
|
# Wait for state to revert
|
||||||
time.sleep(0.5)
|
time.sleep(1.0)
|
||||||
|
|
||||||
# Should be back to initial state
|
ai_in_undo = client.get_value('ai_input')
|
||||||
# Note: Undo moves 'current' to redo stack and pops from undo.
|
temp_undo = client.get_value('temperature')
|
||||||
# If we were at 'Modified', and we undo, we should get 'Initial'.
|
print(f"After undo: ai_input={ai_in_undo}, temp={temp_undo}")
|
||||||
assert client.get_value('ai_input') == "Initial Input"
|
|
||||||
assert client.get_value('temperature') == 0.5
|
assert ai_in_undo == "Initial Input"
|
||||||
|
assert temp_undo == 0.5
|
||||||
|
|
||||||
# 4. Redo
|
# 4. Redo
|
||||||
print("Sending Redo...")
|
print("Sending Redo...")
|
||||||
client.click('btn_redo')
|
client.click('btn_redo')
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
ai_in_redo = client.get_value('ai_input')
|
||||||
|
temp_redo = client.get_value('temperature')
|
||||||
|
print(f"After redo: ai_input={ai_in_redo}, temp={temp_redo}")
|
||||||
|
|
||||||
|
assert ai_in_redo == "Modified Input"
|
||||||
|
assert temp_redo == 1.5
|
||||||
|
|
||||||
|
print("Undo/Redo basic lifecycle PASSED.")
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_undo_redo_discussion_mutation(live_gui):
|
||||||
|
client = api_hook_client.ApiHookClient()
|
||||||
|
assert client.wait_for_server(timeout=15)
|
||||||
|
|
||||||
|
# Get initial entries count
|
||||||
|
initial_entries = client.get_value('disc_entries')
|
||||||
|
initial_count = len(initial_entries)
|
||||||
|
print(f"Initial entries: {initial_count}")
|
||||||
|
|
||||||
|
# 1. Add an entry (we simulate this by appending to disc_entries)
|
||||||
|
# Wait for settle
|
||||||
|
time.sleep(2.0)
|
||||||
|
|
||||||
|
new_entries = initial_entries + [{"role": "User", "content": "New Entry", "collapsed": False, "ts": "2026-03-11 12:00:00"}]
|
||||||
|
client.set_value('disc_entries', new_entries)
|
||||||
|
|
||||||
|
# Wait for debounce
|
||||||
|
time.sleep(2.0)
|
||||||
|
assert len(client.get_value('disc_entries')) == initial_count + 1
|
||||||
|
|
||||||
|
# 2. Undo addition
|
||||||
|
print("Undoing entry addition...")
|
||||||
|
client.click('btn_undo')
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
assert len(client.get_value('disc_entries')) == initial_count
|
||||||
|
|
||||||
assert client.get_value('ai_input') == "Modified Input"
|
# 3. Redo addition
|
||||||
assert client.get_value('temperature') == 1.5
|
print("Redoing entry addition...")
|
||||||
|
client.click('btn_redo')
|
||||||
|
time.sleep(0.5)
|
||||||
|
assert len(client.get_value('disc_entries')) == initial_count + 1
|
||||||
|
|
||||||
print("Undo/Redo lifecycle test PASSED.")
|
print("Undo/Redo discussion mutation PASSED.")
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_undo_redo_context_mutation(live_gui):
|
||||||
|
client = api_hook_client.ApiHookClient()
|
||||||
|
assert client.wait_for_server(timeout=15)
|
||||||
|
|
||||||
|
# Wait for settle
|
||||||
|
time.sleep(2.0)
|
||||||
|
|
||||||
|
# 1. Add a file
|
||||||
|
client.set_value('ui_file_paths', ['test_undo.py'])
|
||||||
|
|
||||||
|
# Wait for debounce
|
||||||
|
time.sleep(2.0)
|
||||||
|
assert 'test_undo.py' in client.get_value('ui_file_paths')
|
||||||
|
|
||||||
|
# 2. Undo addition
|
||||||
|
print("Undoing file addition...")
|
||||||
|
client.click('btn_undo')
|
||||||
|
time.sleep(0.5)
|
||||||
|
assert 'test_undo.py' not in client.get_value('ui_file_paths')
|
||||||
|
|
||||||
|
# 3. Redo addition
|
||||||
|
print("Redoing file addition...")
|
||||||
|
client.click('btn_redo')
|
||||||
|
time.sleep(0.5)
|
||||||
|
assert 'test_undo.py' in client.get_value('ui_file_paths')
|
||||||
|
|
||||||
|
print("Undo/Redo context mutation PASSED.")
|
||||||
|
|||||||
Reference in New Issue
Block a user