diff --git a/src/aggregate.py b/src/aggregate.py index 3cccd46..97920c0 100644 --- a/src/aggregate.py +++ b/src/aggregate.py @@ -157,7 +157,7 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[ tier = entry_raw.get("tier") auto_aggregate = entry_raw.get("auto_aggregate", True) force_full = entry_raw.get("force_full", False) - view_mode = entry_raw.get("view_mode", "summary") + view_mode = entry_raw.get("view_mode", "full") if force_full: view_mode = "full" ast_signatures = entry_raw.get("ast_signatures", False) @@ -169,7 +169,7 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[ tier = getattr(entry_raw, "tier", None) auto_aggregate = getattr(entry_raw, "auto_aggregate", True) force_full = getattr(entry_raw, "force_full", False) - view_mode = getattr(entry_raw, "view_mode", "summary") + view_mode = getattr(entry_raw, "view_mode", "full") if force_full: view_mode = "full" ast_signatures = getattr(entry_raw, "ast_signatures", False) @@ -181,7 +181,7 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[ tier = None auto_aggregate = True force_full = False - view_mode = "summary" + view_mode = "full" ast_signatures = False ast_definitions = False ast_mask = {} diff --git a/src/app_controller.py b/src/app_controller.py index eeaff59..17296a5 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -2840,6 +2840,9 @@ class AppController: self._tool_stats.clear() self._comms_log.clear() self.disc_entries.clear() + self.files.clear() + self.context_files.clear() + self.tracks.clear() # Clear history in ALL discussions to be safe disc_sec = self.project.get("discussion", {}) discussions = disc_sec.get("discussions", {}) @@ -3205,14 +3208,15 @@ class AppController: try: # Use a local copy of files to avoid concurrent modification issues files_to_scan = list(self.files) - for i, file_path in enumerate(files_to_scan): + for i, f_item in enumerate(files_to_scan): try: self.ai_status = f"Phase 2: Scanning files ({i+1}/{len(files_to_scan)})..." - abs_path = Path(self.active_project_root) / file_path + f_path = f_item.path if hasattr(f_item, 'path') else str(f_item) + abs_path = Path(self.active_project_root) / f_path if abs_path.exists() and abs_path.suffix == ".py": with open(abs_path, "r", encoding="utf-8") as f: code = f.read() - generated_skeletons += f"\nFile: {file_path}\n{parser.get_skeleton(code)}\n" + generated_skeletons += f"\nFile: {f_path}\n{parser.get_skeleton(code)}\n" except Exception as e: pass except Exception as e: @@ -3220,10 +3224,12 @@ class AppController: return # Exit if skeleton generation fails # Now loop through tracks and call _start_track_logic with generated skeletons total_tracks = len(self.proposed_tracks) + print(f"[DEBUG] _cb_accept_tracks: Starting {total_tracks} tracks...") for i, track_data in enumerate(self.proposed_tracks): title = track_data.get("title") or track_data.get("goal", "Untitled Track") self.ai_status = f"Processing track {i+1} of {total_tracks}: '{title}'..." self._start_track_logic(track_data, skeletons_str=generated_skeletons) # Pass skeletons + print(f"[DEBUG] _cb_accept_tracks: All {total_tracks} tracks processed.") with self._pending_gui_tasks_lock: self._pending_gui_tasks.append({'action': 'refresh_from_project'}) # Ensure UI refresh after tracks are started self.ai_status = f"All {total_tracks} tracks accepted and execution started." @@ -3326,16 +3332,24 @@ class AppController: self.tracks.append({"id": track_id, "title": title, "status": "todo"}) with self._pending_gui_tasks_lock: self._pending_gui_tasks.append({'action': 'refresh_from_project'}) - # 4. Initialize ConductorEngine and run loop + # 4. Initialize ConductorEngine and run loop + sys.stderr.write(f"[DEBUG] _start_track_logic: Initializing engine for {track_id}...\n") + sys.stderr.flush() engine = multi_agent_conductor.ConductorEngine(track, self.event_queue, auto_queue=not self.mma_step_mode) self.engines[track.id] = engine # Use current full markdown context for the track execution track_id_param = track.id flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id_param) flat.setdefault("files", {})["paths"] = self.context_files + sys.stderr.write(f"[DEBUG] _start_track_logic: Aggregating context for {track_id}...\n") + sys.stderr.flush() full_md, _, _ = aggregate.run(flat) + sys.stderr.write(f"[DEBUG] _start_track_logic: Starting engine thread for {track_id}...\n") + sys.stderr.flush() # Start the engine in a separate thread threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start() + sys.stderr.write(f"[DEBUG] _start_track_logic: Engine thread spawned for {track_id}.\n") + sys.stderr.flush() except Exception as e: self.ai_status = f"Track start error: {e}" print(f"ERROR in _start_track_logic: {e}") diff --git a/src/gui_2.py b/src/gui_2.py index 8e94086..aa403ea 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -108,7 +108,8 @@ class App: """ self.controller = app_controller.AppController() self.controller._app = self - from src import history + from src import history, performance_monitor + self.perf_monitor = performance_monitor.PerformanceMonitor() self.history = history.HistoryManager(max_capacity=100) self._last_ui_snapshot: Optional[history.UISnapshot] = None self._snapshot_timer: float = 0.0 @@ -117,7 +118,7 @@ class App: self._is_applying_snapshot: bool = False self.controller.init_state() - self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.active_project_root) + self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.controller.active_project_root) self.workspace_profiles = self.workspace_manager.load_all_profiles() self.show_windows.setdefault("Diagnostics", False) self.controller.start_services(self) @@ -133,7 +134,9 @@ class App: }) def simulate_save_preset(name: str): from src import models - self.files = [models.FileItem(path='test.py')] + item = models.FileItem(path='test.py') + self.files = [item] + self.context_files = [item] self.screenshots = ['test.png'] self.save_context_preset(name) self.controller._predefined_callbacks['simulate_save_preset'] = simulate_save_preset @@ -328,6 +331,7 @@ class App: 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], + context_files=[f.to_dict() if hasattr(f, 'to_dict') else f for f in self.context_files], screenshots=list(self.screenshots) ) @@ -354,6 +358,13 @@ class App: else: self.files.append(models.FileItem(path=str(f))) + self.context_files = [] + for f in snapshot.context_files: + if isinstance(f, dict): + self.context_files.append(models.FileItem.from_dict(f)) + else: + self.context_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: @@ -1639,67 +1650,62 @@ class App: if self.perf_profiling_enabled: self.perf_monitor.end_component("_gui_func") def _handle_history_logic(self) -> None: - # 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 - 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 + """ + Logic for capturing UI state for undo/redo. + """ + if self._is_applying_snapshot: 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 - abs(current.temperature - self._last_ui_snapshot.temperature) > 1e-5 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.auto_add_history != self._last_ui_snapshot.auto_add_history or - 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 + try: + # 2. Debounced snapshotting + current = self._take_snapshot() + if self._last_ui_snapshot is None: + self._last_ui_snapshot = current + return - if changed: - if not self._pending_snapshot: - self._pending_snapshot = True - self._snapshot_timer = time.time() - # Capture state BEFORE current change - self._state_to_push = self._last_ui_snapshot - else: - # Reset timer for settle debounce - self._snapshot_timer = time.time() + # 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 + abs(current.temperature - self._last_ui_snapshot.temperature) > 1e-5 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.auto_add_history != self._last_ui_snapshot.auto_add_history or + len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or + len(current.files) != len(self._last_ui_snapshot.files) or + len(current.context_files) != len(self._last_ui_snapshot.context_files) or + len(current.screenshots) != len(self._last_ui_snapshot.screenshots) + ) - self._last_ui_snapshot = current + 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 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 + if changed: + if not self._pending_snapshot: + self._pending_snapshot = True + self._snapshot_timer = time.time() + # Capture state BEFORE current change + self._state_to_push = self._last_ui_snapshot + else: + # Reset timer for settle debounce + 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 Update") + self._state_to_push = None + self._pending_snapshot = False + except Exception as e: + import sys, traceback + sys.stderr.write(f"[DEBUG History] ERROR in _handle_history_logic: {e}\n") + traceback.print_exc(file=sys.stderr) + sys.stderr.flush() def _render_base_prompt_diff_modal(self) -> None: if not getattr(self.controller, "_show_base_prompt_diff_modal", False): @@ -2983,22 +2989,42 @@ class App: def _render_context_composition_panel(self) -> None: if not hasattr(self, '_file_stats_cache'): self._file_stats_cache = {} - - total_lines = 0 - total_ast = 0 - for f in self.context_files: - f_path = f.path if hasattr(f, "path") else str(f) - mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 - cache_key = f"{f_path}_{mtime}" - if cache_key not in self._file_stats_cache: - self._file_stats_cache[cache_key] = aggregate.compute_file_stats(f_path) - stats = self._file_stats_cache[cache_key] - total_lines += stats.get("lines", 0) - total_ast += stats.get("ast_elements", 0) + if not hasattr(self, '_file_stats_queue'): + self._file_stats_queue = [] + if not hasattr(self, '_file_stats_worker_active'): + self._file_stats_worker_active = False if imgui.collapsing_header("Context Composition"): - #region: Batch Action Bar - imgui.text("Batch:") + total_lines = 0 + total_ast = 0 + + missing_keys = [] + for f in self.context_files: + f_path = f.path if hasattr(f, "path") else str(f) + mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 + cache_key = f"{f_path}_{mtime}" + if cache_key not in self._file_stats_cache: + missing_keys.append((f_path, cache_key)) + else: + stats = self._file_stats_cache[cache_key] + total_lines += stats.get("lines", 0) + total_ast += stats.get("ast_elements", 0) + + # Process one missing key per frame or spawn a worker + if missing_keys and not self._file_stats_worker_active: + def _stats_worker(): + self._file_stats_worker_active = True + try: + import threading + for path, key in missing_keys[:10]: # Process small batches + self._file_stats_cache[key] = aggregate.compute_file_stats(path) + finally: + self._file_stats_worker_active = False + + import threading + threading.Thread(target=_stats_worker, daemon=True).start() + + #region: Batch Action Bar imgui.text("Batch:") imgui.same_line() for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]: if imgui.button(f"{mode.capitalize()}##batch"): diff --git a/src/history.py b/src/history.py index a033e34..54a8776 100644 --- a/src/history.py +++ b/src/history.py @@ -16,6 +16,7 @@ class UISnapshot: auto_add_history: bool disc_entries: list[dict] files: list[dict] + context_files: list[dict] screenshots: list[str] def to_dict(self) -> dict: @@ -34,6 +35,7 @@ class UISnapshot: "auto_add_history": self.auto_add_history, "disc_entries": self.disc_entries, "files": self.files, + "context_files": self.context_files, "screenshots": self.screenshots } @@ -54,6 +56,7 @@ class UISnapshot: auto_add_history=data.get("auto_add_history", False), disc_entries=data.get("disc_entries", []), files=data.get("files", []), + context_files=data.get("context_files", []), screenshots=data.get("screenshots", []) ) diff --git a/tests/test_context_composition_phase6.py b/tests/test_context_composition_phase6.py index 9b5b759..81d91ff 100644 --- a/tests/test_context_composition_phase6.py +++ b/tests/test_context_composition_phase6.py @@ -83,8 +83,8 @@ def test_view_mode_default_summary(tmp_path): test_file = base_dir / "test.py" test_file.write_text("def hello():\n print('world')\n", encoding="utf-8") - # Test with simple string path - files = ["test.py"] + # Test with explicit summary mode + files = [{"path": "test.py", "view_mode": "summary"}] items = aggregate.build_file_items(base_dir, files) assert len(items) == 1 diff --git a/tests/test_mma_concurrent_tracks_sim.py b/tests/test_mma_concurrent_tracks_sim.py index 6a6fbf7..9c36a39 100644 --- a/tests/test_mma_concurrent_tracks_sim.py +++ b/tests/test_mma_concurrent_tracks_sim.py @@ -35,6 +35,10 @@ def test_mma_concurrent_tracks_execution(live_gui) -> None: client = api_hook_client.ApiHookClient() assert client.wait_for_server(timeout=15), "Hook server did not start" + # 0. Reset session to clear any stale state + client.click('btn_reset') + time.sleep(1.0) + # 1. Setup provider to custom mock mock_path = os.path.abspath("tests/mock_concurrent_mma.py") client.set_value('current_provider', 'gemini_cli') diff --git a/tests/test_mma_concurrent_tracks_stress_sim.py b/tests/test_mma_concurrent_tracks_stress_sim.py index 48fc7a9..d7c68b3 100644 --- a/tests/test_mma_concurrent_tracks_stress_sim.py +++ b/tests/test_mma_concurrent_tracks_stress_sim.py @@ -31,6 +31,10 @@ def test_mma_concurrent_tracks_stress(live_gui) -> None: client = api_hook_client.ApiHookClient() assert client.wait_for_server(timeout=15), "Hook server did not start" + # 0. Reset session to clear any stale state + client.click('btn_reset') + time.sleep(1.0) + # 1. Setup mock provider client.set_value('current_provider', 'gemini_cli') client.set_value('gcli_path', f'"{sys.executable}" "{os.path.abspath("tests/mock_concurrent_mma.py")}"')