refactor(sdm): Refine SDM tags to 'External Only' and update core files. Pruned internal references to conserve tokens.

This commit is contained in:
2026-05-09 14:31:13 -04:00
parent e9ebcb859a
commit 696c08692e
5 changed files with 720 additions and 59 deletions
+278 -27
View File
@@ -42,6 +42,9 @@ from src import rag_engine
from src import theme_2 as theme
def hide_tk_root() -> Tk:
"""
[C: src/gui_2.py:App._render_files_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._render_provider_panel, src/gui_2.py:App._render_screenshots_panel, src/gui_2.py:App._render_theme_panel, src/gui_2.py:App.render_path_field]
"""
root = Tk()
root.withdraw()
root.wm_attributes("-topmost", True)
@@ -49,12 +52,17 @@ def hide_tk_root() -> Tk:
def parse_symbols(text: str) -> list[str]:
"""
Finds all occurrences of '@SymbolName' in text and returns SymbolName.
SymbolName can be a function, class, or method (e.g. @MyClass, @my_func, @MyClass.my_method).
Finds all occurrences of '@SymbolName' in text and returns SymbolName.
SymbolName can be a function, class, or method (e.g. @MyClass, @my_func, @MyClass.my_method).
[C: tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_basic, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_edge_cases, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_methods, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_mixed, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_no_symbols]
"""
return re.findall(r"@([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)", text)
def get_symbol_definition(symbol: str, files: list[str]) -> tuple[str, str, int] | None:
"""
[C: tests/test_symbol_lookup.py:TestSymbolLookup.test_get_symbol_definition_found, tests/test_symbol_lookup.py:TestSymbolLookup.test_get_symbol_definition_not_found]
"""
for file_path in files:
result = mcp_client.py_get_symbol_info(file_path, symbol)
if isinstance(result, tuple):
@@ -74,6 +82,9 @@ class ConfirmRequest(BaseModel):
class ConfirmDialog:
def __init__(self, script: str, base_dir: str) -> None:
"""
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
"""
self._uid = str(uuid.uuid4())
self._script = str(script) if script is not None else ""
self._base_dir = str(base_dir) if base_dir is not None else ""
@@ -82,6 +93,9 @@ class ConfirmDialog:
self._approved = False
def wait(self) -> tuple[bool, str]:
"""
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
"""
start_time = time.time()
with self._condition:
while not self._done:
@@ -92,12 +106,18 @@ class ConfirmDialog:
class MMAApprovalDialog:
def __init__(self, ticket_id: str, payload: str) -> None:
"""
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
"""
self._payload = payload
self._condition = threading.Condition()
self._done = False
self._approved = False
def wait(self) -> tuple[bool, str]:
"""
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
"""
start_time = time.time()
with self._condition:
while not self._done:
@@ -108,6 +128,9 @@ class MMAApprovalDialog:
class MMASpawnApprovalDialog:
def __init__(self, ticket_id: str, role: str, prompt: str, context_md: str) -> None:
"""
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
"""
self._prompt = prompt
self._context_md = context_md
self._condition = threading.Condition()
@@ -116,6 +139,9 @@ class MMASpawnApprovalDialog:
self._abort = False
def wait(self) -> dict[str, Any]:
"""
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
"""
start_time = time.time()
with self._condition:
while not self._done:
@@ -131,12 +157,16 @@ class MMASpawnApprovalDialog:
class AppController:
"""
The headless controller for the Manual Slop application.
Owns the application state and manages background services.
The headless controller for the Manual Slop application.
Owns the application state and manages background services.
"""
def __init__(self):
# Initialize locks first to avoid initialization order issues
"""
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
"""
self._send_thread_lock: threading.Lock = threading.Lock()
self._disc_entries_lock: threading.Lock = threading.Lock()
self._pending_comms_lock: threading.Lock = threading.Lock()
@@ -486,7 +516,10 @@ class AppController:
return self.ui_files_base_dir
def _update_inject_preview(self) -> None:
"""Updates the preview content based on the selected file and injection mode."""
"""
Updates the preview content based on the selected file and injection mode.
[C: src/gui_2.py:App._gui_func, tests/test_skeleton_injection.py:test_update_inject_preview_full, tests/test_skeleton_injection.py:test_update_inject_preview_skeleton, tests/test_skeleton_injection.py:test_update_inject_preview_truncation]
"""
if not self._inject_file_path:
self._inject_preview = ""
return
@@ -720,6 +753,9 @@ class AppController:
def _process_pending_gui_tasks(self) -> None:
# Periodic telemetry broadcast
"""
[C: src/gui_2.py:App._gui_func, tests/test_api_hook_extensions.py:test_app_processes_new_actions, tests/test_gui_updates.py:test_gui_updates_on_event, tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_mma_orchestration_gui.py:test_handle_ai_response_fallback, tests/test_mma_orchestration_gui.py:test_handle_ai_response_with_stream_id, tests/test_mma_orchestration_gui.py:test_process_pending_gui_tasks_mma_spawn_approval, tests/test_mma_orchestration_gui.py:test_process_pending_gui_tasks_show_track_proposal, tests/test_process_pending_gui_tasks.py:test_gcli_path_updates_adapter, tests/test_process_pending_gui_tasks.py:test_redundant_calls_in_process_pending_gui_tasks]
"""
now = time.time()
if hasattr(self, 'event_queue') and hasattr(self.event_queue, 'websocket_server') and self.event_queue.websocket_server:
if now - self._last_telemetry_time >= 1.0:
@@ -918,6 +954,9 @@ class AppController:
class AutoSpawnDialog:
def __init__(self, t): self.t = t
def wait(self):
"""
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
"""
return {'approved': True, 'abort': False, 'prompt': self.t.get("prompt"), 'context_md': self.t.get("context_md")}
task["dialog_container"][0] = AutoSpawnDialog(task)
continue
@@ -987,7 +1026,10 @@ class AppController:
print(f"Error executing GUI task: {e}")
def _process_pending_history_adds(self) -> None:
"""Synchronizes pending history entries to the active discussion and project state."""
"""
Synchronizes pending history entries to the active discussion and project state.
[C: src/gui_2.py:App._gui_func]
"""
with self._pending_history_adds_lock:
items = self._pending_history_adds[:]
self._pending_history_adds.clear()
@@ -1012,7 +1054,10 @@ class AppController:
self.disc_entries.append(item)
def _process_pending_tool_calls(self) -> bool:
"""Drains pending tool calls into the tool log. Returns True if any were processed."""
"""
Drains pending tool calls into the tool log. Returns True if any were processed.
[C: src/gui_2.py:App._gui_func]
"""
with self._pending_tool_calls_lock:
items = self._pending_tool_calls[:]
self._pending_tool_calls.clear()
@@ -1056,7 +1101,10 @@ class AppController:
self._pending_dialog = None
def init_state(self):
"""Initializes the application state from configurations."""
"""
Initializes the application state from configurations.
[C: src/gui_2.py:App.__init__, src/gui_2.py:App._render_paths_panel, src/gui_2.py:App._save_paths, tests/test_app_controller_mcp.py:test_app_controller_mcp_loading, tests/test_app_controller_mcp.py:test_app_controller_mcp_project_override, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_app_controller_init_state_loads_prompts]
"""
self.active_tickets = []
self.ui_separate_task_dag = False
self.ui_separate_usage_analytics = False
@@ -1198,6 +1246,9 @@ class AppController:
self.event_queue.put('refresh_external_mcps', None)
async def refresh_external_mcps(self):
"""
[C: tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
"""
await mcp_client.get_external_mcp_manager().stop_all()
# Start servers with auto_start=True
for name, cfg in self.mcp_config.mcpServers.items():
@@ -1205,6 +1256,9 @@ class AppController:
await mcp_client.get_external_mcp_manager().add_server(cfg)
def cb_load_prior_log(self, path: Optional[str] = None) -> None:
"""
[C: src/gui_2.py:App._render_log_management]
"""
root = hide_tk_root()
if path is None:
path = filedialog.askdirectory(
@@ -1393,6 +1447,9 @@ class AppController:
def cb_exit_prior_session(self):
"""
[C: src/gui_2.py:App._render_comms_history_panel, src/gui_2.py:App._render_discussion_panel]
"""
self.is_viewing_prior_session = False
if self._current_session_usage:
self.session_usage = self._current_session_usage
@@ -1475,6 +1532,9 @@ class AppController:
thread.start()
def _fetch_models(self, provider: str) -> None:
"""
[C: src/gui_2.py:App.run]
"""
self.ai_status = "fetching models..."
def do_fetch() -> None:
@@ -1498,14 +1558,20 @@ class AppController:
self.models_thread.start()
def start_services(self, app: Any = None):
"""Starts background threads."""
"""
Starts background threads.
[C: src/gui_2.py:App.__init__]
"""
self._prune_old_logs()
self._init_ai_and_hooks(app)
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
self._loop_thread.start()
def shutdown(self) -> None:
"""Stops background threads and cleans up resources."""
"""
Stops background threads and cleans up resources.
[C: src/gui_2.py:App.run, src/gui_2.py:App.shutdown, tests/conftest.py:app_instance, tests/conftest.py:mock_app]
"""
from src import ai_client
ai_client.cleanup()
if hasattr(self, 'hook_server') and self.hook_server:
@@ -1604,7 +1670,10 @@ class AppController:
asyncio.run(self.refresh_external_mcps())
def _handle_request_event(self, event: events.UserRequestEvent) -> None:
"""Processes a UserRequestEvent by calling the AI client."""
"""
Processes a UserRequestEvent by calling the AI client.
[C: tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_rag_integration.py:test_rag_integration]
"""
self.ai_status = 'sending...'
ai_client.set_current_tier(None) # Ensure main discussion is untagged
# Clear response area for new turn
@@ -1662,6 +1731,9 @@ class AppController:
self.event_queue.put("response", {"text": text, "status": "streaming...", "role": "AI"})
def _on_comms_entry(self, entry: Dict[str, Any]) -> None:
"""
[C: tests/test_app_controller_offloading.py:test_on_comms_entry_tool_result_offloading]
"""
optimized_entry = self._offload_entry_payload(entry)
session_logger.log_comms(optimized_entry)
entry["local_ts"] = time.time()
@@ -1756,6 +1828,9 @@ class AppController:
self._pending_comms.append(entry)
def _on_tool_log(self, script: str, result: str) -> None:
"""
[C: tests/test_app_controller_offloading.py:test_on_tool_log_offloading]
"""
session_logger.log_tool_call(script, result, None)
session_logger.log_tool_output(result)
source_tier = ai_client.get_current_tier()
@@ -1763,6 +1838,9 @@ class AppController:
self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
def _on_api_event(self, event_name: str = "generic_event", **kwargs: Any) -> None:
"""
[C: tests/test_gui_updates.py:test_gui_updates_on_event]
"""
payload = kwargs.get("payload", {})
# Push to background event queue, NOT GUI queue
self.event_queue.put("refresh_api_metrics", payload)
@@ -1778,6 +1856,9 @@ class AppController:
})
def _confirm_and_run(self, script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Optional[str]:
"""
[C: tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_mutating_tool_triggers_callback, tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_rejection_prevents_dispatch]
"""
if self.test_hooks_enabled and not getattr(self, "ui_manual_approve", False):
self.ai_status = "running powershell..."
output = shell_runner.run_powershell(script, base_dir, qa_callback=qa_callback, patch_callback=patch_callback)
@@ -1815,6 +1896,9 @@ class AppController:
return output
def _append_tool_log(self, script: str, result: str, source_tier: str | None = None, elapsed_ms: float = 0.0) -> None:
"""
[C: tests/test_mma_agent_focus_phase1.py:test_append_tool_log_dict_has_source_tier, tests/test_mma_agent_focus_phase1.py:test_append_tool_log_dict_keys, tests/test_mma_agent_focus_phase1.py:test_append_tool_log_stores_dict]
"""
self._tool_log.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
tool_name = self._extract_tool_name(script)
is_failure = "REJECTED" in result or "Error" in result or "error" in result.lower()
@@ -1910,7 +1994,10 @@ class AppController:
self._token_stats_dirty = True
def create_api(self) -> FastAPI:
"""Creates and configures the FastAPI application for headless mode."""
"""
Creates and configures the FastAPI application for headless mode.
[C: src/gui_2.py:App.run, tests/test_headless_service.py:TestHeadlessAPI.setUp]
"""
api = FastAPI(title="Manual Slop Headless API")
API_KEY_NAME = "X-API-KEY"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
@@ -1934,7 +2021,10 @@ class AppController:
@api.get("/api/gui/state", dependencies=[Depends(get_api_key)])
def get_gui_state() -> dict[str, Any]:
"""Returns the current GUI state for specific fields."""
"""
Returns the current GUI state for specific fields.
[C: tests/test_ai_settings_layout.py:test_change_provider_via_hook, tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_conductor_api_hook_integration.py:simulate_conductor_phase_completion, tests/test_external_editor_gui.py:test_button_click_is_received, tests/test_external_editor_gui.py:test_patch_modal_shows_with_configured_editor, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_text_viewer.py:test_text_viewer_state_update, tests/test_hooks.py:test_live_hook_server_responses, tests/test_live_gui_integration_v2.py:test_api_gui_state_live, tests/test_live_workflow.py:test_full_live_workflow, tests/test_live_workflow.py:wait_for_value, tests/test_patch_modal_gui.py:test_patch_apply_modal_workflow, tests/test_patch_modal_gui.py:test_patch_modal_appears_on_trigger, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_saved_presets_sim.py:test_preset_switching, tests/test_task_dag_popout_sim.py:test_task_dag_popout, tests/test_tool_management_layout.py:test_tool_management_gettable_fields, tests/test_tool_management_layout.py:test_tool_management_state_updates, tests/test_tool_presets_sim.py:test_tool_preset_switching, tests/test_usage_analytics_popout_sim.py:test_usage_analytics_popout]
"""
gettable = getattr(self, "_gettable_fields", {})
state = {}
import dataclasses
@@ -1948,7 +2038,10 @@ class AppController:
@api.get("/api/gui/mma_status", dependencies=[Depends(get_api_key)])
def get_mma_status() -> dict[str, Any]:
"""Dedicated endpoint for MMA-related status."""
"""
Dedicated endpoint for MMA-related status.
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_live_workflow.py:test_full_live_workflow, tests/test_mma_concurrent_tracks_sim.py:_poll_mma_status, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_mma_step_mode_sim.py:_poll_mma_status, tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow, tests/test_visual_orchestration.py:test_mma_epic_lifecycle, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing, tests/test_visual_sim_mma_v2.py:_poll]
"""
return {
"mma_status": self.mma_status,
"ai_status": self.ai_status,
@@ -1963,7 +2056,10 @@ class AppController:
@api.post("/api/gui", dependencies=[Depends(get_api_key)])
def post_gui(req: dict) -> dict[str, str]:
"""Pushes a GUI task to the event queue."""
"""
Pushes a GUI task to the event queue.
[C: tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_api_hook_client.py:test_post_gui_success, tests/test_gui2_parity.py:test_gui2_custom_callback_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_visual_mma.py:test_visual_mma_components]
"""
self.event_queue.put("gui_task", req)
return {"status": "queued"}
@@ -1988,7 +2084,10 @@ class AppController:
@api.get("/api/performance", dependencies=[Depends(get_api_key)])
def get_performance() -> dict[str, Any]:
"""Returns performance monitor metrics."""
"""
Returns performance monitor metrics.
[C: tests/test_gui2_performance.py:test_performance_benchmarking, tests/test_gui_performance_requirements.py:test_idle_performance_requirements, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing]
"""
return {"performance": self.perf_monitor.get_metrics()}
@api.get("/api/gui/diagnostics", dependencies=[Depends(get_api_key)])
@@ -2107,7 +2206,10 @@ class AppController:
@api.get("/api/v1/sessions/{session_id}", dependencies=[Depends(get_api_key)])
def get_session(session_id: str) -> dict[str, Any]:
"""Returns the content of the comms.log for a specific session."""
"""
Returns the content of the comms.log for a specific session.
[C: simulation/ping_pong.py:main, simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/sim_tools.py:ToolsSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/test_api_hook_client.py:test_get_session_success, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_live_workflow.py:test_full_live_workflow, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim]
"""
log_path = paths.get_logs_dir() / session_id / "comms.log"
if not log_path.exists():
raise HTTPException(status_code=404, detail="Session log not found")
@@ -2162,15 +2264,24 @@ class AppController:
self.ai_status = "config saved"
def _cb_reset_base_prompt(self, user_data=None) -> None:
"""
[C: src/gui_2.py:App._render_system_prompts_panel]
"""
self.ui_base_system_prompt = ai_client._SYSTEM_PROMPT
self.ui_use_default_base_prompt = False
def _cb_clear_summary_cache(self, user_data=None) -> None:
"""
[C: src/gui_2.py:App._render_files_panel]
"""
from src import summarize
summarize._summary_cache.clear()
self._push_mma_state_update()
def _cb_show_base_prompt_diff(self, user_data=None) -> None:
"""
[C: src/gui_2.py:App._render_system_prompts_panel]
"""
self._show_base_prompt_diff_modal = True
def _cb_disc_create(self) -> None:
@@ -2180,6 +2291,9 @@ class AppController:
self.ui_disc_new_name_input = ""
def _switch_project(self, path: str) -> None:
"""
[C: src/gui_2.py:App._render_projects_panel]
"""
if not Path(path).exists():
self.ai_status = f"project file not found: {path}"
return
@@ -2200,6 +2314,9 @@ class AppController:
def _refresh_from_project(self) -> None:
# Deserialize FileItems in files.paths
"""
[C: tests/test_mma_dashboard_refresh.py:test_mma_dashboard_initialization_refresh, tests/test_mma_dashboard_refresh.py:test_mma_dashboard_refresh]
"""
raw_paths = self.project.get("files", {}).get("paths", [])
self.files = []
for p in raw_paths:
@@ -2279,6 +2396,9 @@ class AppController:
self._rebuild_rag_index()
def _cb_save_workspace_profile(self, name: str, scope: str = 'project') -> None:
"""
[C: src/gui_2.py:App._render_save_workspace_profile_modal]
"""
if not hasattr(self, '_app') or not self._app:
return
profile = self._app._capture_workspace_profile(name)
@@ -2287,18 +2407,27 @@ class AppController:
self._app.workspace_profiles = self.workspace_profiles
def _cb_delete_workspace_profile(self, name: str, scope: str = 'project') -> None:
"""
[C: src/gui_2.py:App._show_menus]
"""
self.workspace_manager.delete_profile(name, scope=scope)
self.workspace_profiles = self.workspace_manager.load_all_profiles()
if hasattr(self, '_app') and self._app:
self._app.workspace_profiles = self.workspace_profiles
def _cb_load_workspace_profile(self, name: str) -> None:
"""
[C: src/gui_2.py:App._show_menus]
"""
if name in self.workspace_profiles:
profile = self.workspace_profiles[name]
if hasattr(self, '_app') and self._app:
self._app._apply_workspace_profile(profile)
def _apply_preset(self, name: str, scope: str) -> None:
"""
[C: src/gui_2.py:App._render_system_prompts_panel]
"""
print(f"[DEBUG] _apply_preset: name={name}, scope={scope}")
if name == "None":
if scope == "global":
@@ -2318,6 +2447,9 @@ class AppController:
self.ui_project_preset_name = name
def _cb_save_preset(self, name, content, scope):
"""
[C: src/gui_2.py:App._render_preset_manager_content]
"""
print(f"[DEBUG] _cb_save_preset: name={name}, scope={scope}")
if not name or not name.strip():
raise ValueError("Preset name cannot be empty or whitespace.")
@@ -2330,19 +2462,31 @@ class AppController:
print(f"[DEBUG] _cb_save_preset: saved {name}, total presets now {len(self.presets)}")
def _cb_delete_preset(self, name, scope):
"""
[C: src/gui_2.py:App._render_preset_manager_content]
"""
self.preset_manager.delete_preset(name, scope)
self.presets = self.preset_manager.load_all()
def _cb_save_tool_preset(self, name, categories, scope):
"""
[C: src/gui_2.py:App._render_tool_preset_manager_content]
"""
preset = models.ToolPreset(name=name, categories=categories)
self.tool_preset_manager.save_preset(preset, scope)
self.tool_presets = self.tool_preset_manager.load_all_presets()
def _cb_delete_tool_preset(self, name, scope):
"""
[C: src/gui_2.py:App._render_tool_preset_manager_content]
"""
self.tool_preset_manager.delete_preset(name, scope)
self.tool_presets = self.tool_preset_manager.load_all_presets()
def _cb_save_bias_profile(self, profile: models.BiasProfile, scope: str = "project"):
"""
[C: src/gui_2.py:App._render_tool_preset_manager_content]
"""
self.tool_preset_manager.save_bias_profile(profile, scope)
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
@@ -2351,15 +2495,24 @@ class AppController:
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
def _cb_save_persona(self, persona: models.Persona, scope: str = "project") -> None:
"""
[C: src/gui_2.py:App._render_persona_editor_window]
"""
self.persona_manager.save_persona(persona, scope)
self.personas = self.persona_manager.load_all()
def _cb_delete_persona(self, name: str, scope: str = "project") -> None:
"""
[C: src/gui_2.py:App._render_persona_editor_window]
"""
self.persona_manager.delete_persona(name, scope)
self.personas = self.persona_manager.load_all()
def _cb_load_track(self, track_id: str) -> None:
"""
[C: src/gui_2.py:App._render_mma_dashboard]
"""
state = project_manager.load_track_state(track_id, self.active_project_root)
if state:
try:
@@ -2391,6 +2544,9 @@ class AppController:
print(f"Error loading track {track_id}: {e}")
def _save_active_project(self) -> None:
"""
[C: src/gui_2.py:App.delete_context_preset, src/gui_2.py:App.save_context_preset]
"""
if self.active_project_path:
try:
cleaned = project_manager.clean_nones(self.project)
@@ -2399,11 +2555,17 @@ class AppController:
self.ai_status = f"save error: {e}"
def _get_discussion_names(self) -> list[str]:
"""
[C: src/gui_2.py:App._render_discussion_panel]
"""
disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {})
return sorted(discussions.keys())
def _switch_discussion(self, name: str) -> None:
"""
[C: src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_takes_panel]
"""
self._flush_disc_entries_to_project()
disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {})
@@ -2419,6 +2581,9 @@ class AppController:
self.ai_status = f"discussion: {name}"
def _flush_disc_entries_to_project(self) -> None:
"""
[C: src/gui_2.py:App._render_discussion_panel]
"""
history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries]
if self.active_track and self._track_discussion_active:
project_manager.save_track_history(self.active_track.id, history_strings, self.active_project_root)
@@ -2430,6 +2595,9 @@ class AppController:
disc_data["last_updated"] = project_manager.now_ts()
def _create_discussion(self, name: str) -> None:
"""
[C: src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_synthesis_panel, src/gui_2.py:App._render_takes_panel]
"""
disc_sec = self.project.setdefault("discussion", {})
discussions = disc_sec.setdefault("discussions", {})
if name in discussions:
@@ -2439,6 +2607,9 @@ class AppController:
self._switch_discussion(name)
def _branch_discussion(self, index: int) -> None:
"""
[C: src/gui_2.py:App._render_discussion_panel]
"""
self._flush_disc_entries_to_project()
# Generate a unique branch name
base_name = self.active_discussion.split("_take_")[0]
@@ -2453,6 +2624,9 @@ class AppController:
project_manager.branch_discussion(self.project, self.active_discussion, new_name, index)
self._switch_discussion(new_name)
def _rename_discussion(self, old_name: str, new_name: str) -> None:
"""
[C: src/gui_2.py:App._render_discussion_panel]
"""
disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {})
if old_name not in discussions:
@@ -2466,6 +2640,9 @@ class AppController:
disc_sec["active"] = new_name
def _delete_discussion(self, name: str) -> None:
"""
[C: src/gui_2.py:App._render_discussion_panel]
"""
disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {})
if len(discussions) <= 1:
@@ -2479,6 +2656,9 @@ class AppController:
self._switch_discussion(remaining[0])
def _handle_mma_respond(self, approved: bool, payload: str | None = None, abort: bool = False, prompt: str | None = None, context_md: str | None = None) -> None:
"""
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._handle_approve_mma_step, src/gui_2.py:App._handle_approve_spawn]
"""
if self._pending_mma_approvals:
task = self._pending_mma_approvals.pop(0)
dlg = task.get("dialog_container", [None])[0]
@@ -2504,7 +2684,10 @@ class AppController:
spawn_dlg._condition.notify_all()
def _handle_approve_ask(self) -> None:
"""Responds with approval for a pending /api/ask request."""
"""
Responds with approval for a pending /api/ask request.
[C: src/gui_2.py:App.__init__, src/gui_2.py:App._gui_func]
"""
if not self._ask_request_id: return
request_id = self._ask_request_id
@@ -2522,7 +2705,10 @@ class AppController:
self._ask_tool_data = None
def _handle_reject_ask(self) -> None:
"""Responds with rejection for a pending /api/ask request."""
"""
Responds with rejection for a pending /api/ask request.
[C: src/gui_2.py:App._gui_func]
"""
if not self._ask_request_id: return
request_id = self._ask_request_id
@@ -2540,7 +2726,10 @@ class AppController:
self._ask_tool_data = None
def _handle_reset_session(self) -> None:
"""Logic for resetting the AI session and GUI state."""
"""
Logic for resetting the AI session and GUI state.
[C: src/gui_2.py:App._render_message_panel]
"""
ai_client.reset_session()
ai_client.clear_comms_log()
self._tool_log.clear()
@@ -2568,9 +2757,15 @@ class AppController:
self._pending_gui_tasks.clear()
def _handle_md_only(self) -> None:
"""Logic for the 'MD Only' action."""
"""
Logic for the 'MD Only' action.
[C: src/gui_2.py:App._render_message_panel]
"""
def worker():
"""
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
"""
try:
md, path, *_ = self._do_generate()
self.last_md = md
@@ -2583,9 +2778,15 @@ class AppController:
threading.Thread(target=worker, daemon=True).start()
def _handle_generate_send(self) -> None:
"""Logic for the 'Gen + Send' action."""
"""
Logic for the 'Gen + Send' action.
[C: src/gui_2.py:App._render_message_panel, src/gui_2.py:App._render_synthesis_panel, src/gui_2.py:App._render_takes_panel, tests/test_gui_events_v2.py:test_handle_generate_send_pushes_event, tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
"""
def worker():
"""
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
"""
sys.stderr.write("[DEBUG] _handle_generate_send worker started\n")
sys.stderr.flush()
try:
@@ -2650,6 +2851,9 @@ class AppController:
self._cached_files = stats.get("cached_files", [])
def _refresh_api_metrics(self, payload: dict[str, Any], md_content: str | None = None) -> None:
"""
[C: tests/test_gui_updates.py:test_telemetry_data_updates_correctly]
"""
if "latency" in payload:
self.session_usage["last_latency"] = payload["latency"]
if "usage" in payload and "percentage" in payload["usage"]:
@@ -2674,11 +2878,17 @@ class AppController:
self._cached_tool_stats = dict(self._tool_stats)
def clear_cache(self) -> None:
"""
[C: src/gui_2.py:App._render_cache_panel]
"""
from src import ai_client
ai_client.cleanup()
self._update_cached_stats()
def get_session_insights(self) -> Dict[str, Any]:
"""
[C: src/gui_2.py:App._render_session_insights_panel]
"""
from src import cost_tracker
total_input = sum(e["input"] for e in self._token_history)
total_output = sum(e["output"] for e in self._token_history)
@@ -2701,6 +2911,9 @@ class AppController:
}
def _flush_to_project(self) -> None:
"""
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._show_menus]
"""
proj = self.project
proj.setdefault("output", {})["output_dir"] = self.ui_output_dir
proj.setdefault("files", {})["base_dir"] = self.ui_files_base_dir
@@ -2738,6 +2951,9 @@ class AppController:
project_manager.save_project(cleaned_proj, self.active_project_path)
def _flush_to_config(self) -> None:
"""
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._render_theme_panel, src/gui_2.py:App._show_menus, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_app_controller_flush_saves_prompts]
"""
self.config["ai"] = {
"provider": self.current_provider,
"model": self.current_model,
@@ -2778,7 +2994,10 @@ class AppController:
theme.save_to_config(self.config)
def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]:
"""Returns (full_md, output_path, file_items, stable_md, discussion_text)."""
"""
Returns (full_md, output_path, file_items, stable_md, discussion_text).
[C: src/gui_2.py:App._show_menus, tests/test_tiered_aggregation.py:test_app_controller_do_generate_uses_persona_strategy]
"""
self._flush_to_project()
self._flush_to_config()
models.save_config(self.config)
@@ -2809,6 +3028,9 @@ class AppController:
return full_md, path, file_items, stable_md, discussion_text
def _cb_plan_epic(self) -> None:
"""
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_mma_orchestration_gui.py:test_cb_plan_epic_launches_thread]
"""
def _bg_task() -> None:
sys.stderr.write("[DEBUG] _cb_plan_epic _bg_task started\n")
sys.stderr.flush()
@@ -2855,6 +3077,9 @@ class AppController:
threading.Thread(target=_bg_task, daemon=True).start()
def _cb_accept_tracks(self) -> None:
"""
[C: src/gui_2.py:App._render_track_proposal_modal]
"""
self._show_track_proposal_modal = False
def _bg_task() -> None:
@@ -2896,6 +3121,9 @@ class AppController:
threading.Thread(target=_bg_task, daemon=True).start()
def _cb_start_track(self, user_data: Any = None) -> None:
"""
[C: src/gui_2.py:App._render_track_proposal_modal]
"""
if isinstance(user_data, str):
# If track_id is provided directly
track_id = user_data
@@ -3005,6 +3233,9 @@ class AppController:
print(f"ERROR in _start_track_logic: {e}")
def _cb_ticket_retry(self, ticket_id: str) -> None:
"""
[C: tests/test_mma_ticket_actions.py:test_cb_ticket_retry]
"""
for t in self.active_tickets:
if t.get('id') == ticket_id:
t['status'] = 'todo'
@@ -3012,6 +3243,9 @@ class AppController:
self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
def _cb_ticket_skip(self, ticket_id: str) -> None:
"""
[C: tests/test_mma_ticket_actions.py:test_cb_ticket_skip]
"""
for t in self.active_tickets:
if t.get('id') == ticket_id:
t['status'] = 'skipped'
@@ -3031,7 +3265,10 @@ class AppController:
self.event_queue.put("mma_retry", {"ticket_id": ticket_id})
def kill_worker(self, worker_id: str) -> None:
"""Aborts a running worker."""
"""
Aborts a running worker.
[C: src/gui_2.py:App._cb_kill_ticket, tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread]
"""
engine = self.engines.get(self.active_track.id if self.active_track else None)
if engine:
engine.kill_worker(worker_id)
@@ -3051,7 +3288,10 @@ class AppController:
engine.resume()
def inject_context(self, data: dict) -> None:
"""Programmatic context injection."""
"""
Programmatic context injection.
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation]
"""
file_path = data.get("file_path")
if file_path:
if not os.path.isabs(file_path):
@@ -3097,6 +3337,9 @@ class AppController:
self._push_mma_state_update()
def _cb_run_conductor_setup(self) -> None:
"""
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_gui_phase3.py:test_conductor_setup_scan]
"""
base = paths.get_conductor_dir(project_path=self.active_project_root)
if not base.exists():
self.ui_conductor_setup_summary = f"Error: {base}/ directory not found."
@@ -3124,6 +3367,9 @@ class AppController:
self.ui_conductor_setup_summary = "\n".join(summary)
def _cb_create_track(self, name: str, desc: str, track_type: str) -> None:
"""
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_gui_phase3.py:test_create_track]
"""
if not name: return
date_suffix = datetime.now().strftime("%Y%m%d")
track_id = f"{name.lower().replace(' ', '_')}_{date_suffix}"
@@ -3149,6 +3395,9 @@ class AppController:
self.tracks = project_manager.get_all_tracks(self.active_project_root)
def _push_mma_state_update(self) -> None:
"""
[C: src/gui_2.py:App._cb_block_ticket, src/gui_2.py:App._cb_unblock_ticket, src/gui_2.py:App._render_mma_dashboard, src/gui_2.py:App._render_task_dag_panel, src/gui_2.py:App._render_ticket_queue, src/gui_2.py:App._reorder_ticket, src/gui_2.py:App.bulk_block, src/gui_2.py:App.bulk_execute, src/gui_2.py:App.bulk_skip, tests/test_gui_phase4.py:test_push_mma_state_update]
"""
if not self.active_track:
return
# Sync active_tickets (list of dicts) back to active_track.tickets (list of models.Ticket objects)
@@ -3170,7 +3419,10 @@ class AppController:
project_manager.save_track_state(self.active_track.id, state, self.active_project_root)
def _load_active_tickets(self) -> None:
"""Populates self.active_tickets based on the current execution mode."""
"""
Populates self.active_tickets based on the current execution mode.
[C: tests/test_gui_dag_beads.py:test_load_active_tickets_from_beads]
"""
if getattr(self, "ui_project_execution_mode", "native") == "beads":
from src import beads_client
bclient = beads_client.BeadsClient(Path(self.active_project_root))
@@ -3191,4 +3443,3 @@ class AppController:
self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in self.active_track.tickets]
else:
self.active_tickets = []