diff --git a/src/app_controller.py b/src/app_controller.py index 30852488..823d3638 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -47,7 +47,6 @@ from src.warmup import WarmupManager 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). [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] @@ -90,11 +89,11 @@ class ConfirmDialog: return self._approved, self._script #region: API Handlers + async def _api_get_key(controller: 'AppController', header_key: str) -> str: """ - - Validates the API key from the request header against configuration. - [SDM: src/app_controller.py:_api_get_key] + Validates the API key from the request header against configuration. + [SDM: src/app_controller.py:_api_get_key] """ HTTPException = _require_warmed("fastapi").HTTPException headless_cfg = controller.config.get("headless", {}) @@ -109,16 +108,15 @@ async def _api_get_key(controller: 'AppController', header_key: str) -> str: def _api_health(controller: 'AppController') -> dict[str, str]: """ - - Returns the health status of the API. - [SDM: src/app_controller.py:_api_health] + Returns the health status of the API. + [SDM: src/app_controller.py:_api_health] """ return {"status": "ok"} def _api_get_gui_state(controller: 'AppController') -> dict[str, Any]: """ - Returns the current GUI state for specific fields. - [SDM: src/app_controller.py:_api_get_gui_state] + Returns the current GUI state for specific fields. + [SDM: src/app_controller.py:_api_get_gui_state] """ gettable = getattr(controller, "_gettable_fields", {}) state = {} @@ -138,35 +136,32 @@ def _api_get_gui_state(controller: 'AppController') -> dict[str, Any]: def _api_get_mma_status(controller: 'AppController') -> dict[str, Any]: """ - - Dedicated endpoint for MMA-related status. - [SDM: src/app_controller.py:_api_get_mma_status] + Dedicated endpoint for MMA-related status. + [SDM: src/app_controller.py:_api_get_mma_status] """ return { - "mma_status": controller.mma_status, - "ai_status": controller.ai_status, - "mma_streams": controller.mma_streams, - "worker_status": controller._worker_status, - "tool_stats": controller._tool_stats, - "active_tier": controller.active_tier, - "active_tickets": controller.active_tickets, + "mma_status": controller.mma_status, + "ai_status": controller.ai_status, + "mma_streams": controller.mma_streams, + "worker_status": controller._worker_status, + "tool_stats": controller._tool_stats, + "active_tier": controller.active_tier, + "active_tickets": controller.active_tickets, "proposed_tracks": controller.proposed_tracks, - "tracks": controller.tracks, - "tier_usage": controller.mma_tier_usage + "tracks": controller.tracks, + "tier_usage": controller.mma_tier_usage } def _api_post_gui(controller: 'AppController', req: dict) -> dict[str, str]: """ - - Pushes a GUI task to the event queue. - [SDM: src/app_controller.py:_api_post_gui] + Pushes a GUI task to the event queue. + [SDM: src/app_controller.py:_api_post_gui] """ controller.event_queue.put("gui_task", req) return {"status": "queued"} def _api_get_api_session(controller: 'AppController') -> dict[str, Any]: """ - Returns current discussion session entries. [SDM: src/app_controller.py:_api_get_api_session] """ @@ -175,7 +170,6 @@ def _api_get_api_session(controller: 'AppController') -> dict[str, Any]: def _api_post_api_session(controller: 'AppController', req: dict) -> dict[str, str]: """ - Updates session entries. [SDM: src/app_controller.py:_api_post_api_session] """ @@ -186,7 +180,6 @@ def _api_post_api_session(controller: 'AppController', req: dict) -> dict[str, s def _api_get_api_project(controller: 'AppController') -> dict[str, Any]: """ - Returns current project data. [SDM: src/app_controller.py:_api_get_api_project] """ @@ -194,7 +187,6 @@ def _api_get_api_project(controller: 'AppController') -> dict[str, Any]: def _api_get_performance(controller: 'AppController') -> dict[str, Any]: """ - Returns performance monitor metrics. [SDM: src/app_controller.py:_api_get_performance] """ @@ -202,7 +194,6 @@ def _api_get_performance(controller: 'AppController') -> dict[str, Any]: def _api_get_diagnostics(controller: 'AppController') -> dict[str, Any]: """ - Alias for performance metrics. [SDM: src/app_controller.py:_api_get_diagnostics] """ @@ -210,7 +201,6 @@ def _api_get_diagnostics(controller: 'AppController') -> dict[str, Any]: def _api_status(controller: 'AppController') -> dict[str, Any]: """ - Returns the current status of the application. [SDM: src/app_controller.py:_api_status] """ @@ -223,7 +213,6 @@ def _api_status(controller: 'AppController') -> dict[str, Any]: def _api_generate(controller: 'AppController', req: GenerateRequest) -> dict[str, Any]: """ - Triggers an AI generation request using the current project context. [SDM: src/app_controller.py:_api_generate] """ @@ -336,7 +325,6 @@ def _api_generate(controller: 'AppController', req: GenerateRequest) -> dict[str async def _api_stream(controller: 'AppController', req: GenerateRequest) -> Any: """ - Placeholder for streaming AI generation responses (Not yet implemented). [SDM: src/app_controller.py:_api_stream] """ @@ -345,7 +333,6 @@ async def _api_stream(controller: 'AppController', req: GenerateRequest) -> Any: def _api_pending_actions(controller: 'AppController') -> list[dict[str, Any]]: """ - Lists all pending PowerShell scripts awaiting confirmation. [SDM: src/app_controller.py:_api_pending_actions] """ @@ -357,7 +344,6 @@ def _api_pending_actions(controller: 'AppController') -> list[dict[str, Any]]: def _api_confirm_action(controller: 'AppController', action_id: str, req: ConfirmRequest) -> dict[str, str]: """ - Approves or rejects a pending action. [SDM: src/app_controller.py:_api_confirm_action] """ @@ -376,7 +362,6 @@ def _api_confirm_action(controller: 'AppController', action_id: str, req: Confir def _api_list_sessions(controller: 'AppController') -> list[str]: """ - Lists all session IDs. [SDM: src/app_controller.py:_api_list_sessions] """ @@ -387,7 +372,6 @@ def _api_list_sessions(controller: 'AppController') -> list[str]: def _api_get_session(controller: 'AppController', session_id: str) -> dict[str, Any]: """ - Returns the content of the comms.log for a specific session. [SDM: src/app_controller.py:_api_get_session] """ @@ -399,7 +383,6 @@ def _api_get_session(controller: 'AppController', session_id: str) -> dict[str, def _api_delete_session(controller: 'AppController', session_id: str) -> dict[str, str]: """ - Deletes a specific session directory. [SDM: src/app_controller.py:_api_delete_session] """ @@ -413,7 +396,6 @@ def _api_delete_session(controller: 'AppController', session_id: str) -> dict[st def _api_get_context(controller: 'AppController') -> dict[str, Any]: """ - Returns the current aggregated project context. [SDM: src/app_controller.py:_api_get_context] """ @@ -433,12 +415,12 @@ def _api_get_context(controller: 'AppController') -> dict[str, Any]: def _api_token_stats(controller: 'AppController') -> dict[str, Any]: """ - Returns current token usage and budget statistics. [SDM: src/app_controller.py:_api_token_stats] """ return controller._token_stats -#endregion + +#endregion: API Handlers #region: GUI Task Handlers @@ -742,11 +724,10 @@ def _handle_hide_patch_modal(controller: 'AppController', task: dict): controller._pending_patch_text = None controller._pending_patch_files = [] -#endregion +#endregion: GUI Task Handlers def _install_sigint_exit_handler(controller: 'AppController') -> None: """ - Install a SIGINT handler that drains the controller's I/O pool (wait=False) and calls ``os._exit(0)``. This sidesteps the broken Python interpreter finalization chain that hangs the process when @@ -779,7 +760,6 @@ def _install_sigint_exit_handler(controller: 'AppController') -> None: class AppController: """ - The headless controller for the Manual Slop application. Owns the application state and manages background services. """ @@ -794,22 +774,22 @@ class AppController: # lazily as events occur (warmup_done by on_complete callback, # first_frame by mark_first_frame_rendered, appcontroller_init_done # and gui_run_started by mark_gui_run_started). - self._init_start_ts: float = time.time() - self._warmup_done_ts: Optional[float] = None - self._first_frame_ts: Optional[float] = None + self._init_start_ts: float = time.time() + self._warmup_done_ts: Optional[float] = None + self._first_frame_ts: Optional[float] = None self._appcontroller_init_done_ts: Optional[float] = None - self._gui_run_started_ts: Optional[float] = None + self._gui_run_started_ts: Optional[float] = None # --- Locks --- - self._send_thread_lock: threading.Lock = threading.Lock() - self._disc_entries_lock: threading.Lock = threading.Lock() - self._pending_comms_lock: threading.Lock = threading.Lock() - self._pending_tool_calls_lock: threading.Lock = threading.Lock() + self._send_thread_lock: threading.Lock = threading.Lock() + self._disc_entries_lock: threading.Lock = threading.Lock() + self._pending_comms_lock: threading.Lock = threading.Lock() + self._pending_tool_calls_lock: threading.Lock = threading.Lock() self._pending_history_adds_lock: threading.Lock = threading.Lock() - self._pending_gui_tasks_lock: threading.Lock = threading.Lock() - self._pending_dialog_lock: threading.Lock = threading.Lock() - self._api_event_queue_lock: threading.Lock = threading.Lock() - self._rag_engine_lock: threading.Lock = threading.Lock() - self._project_switch_lock: threading.Lock = threading.Lock() + self._pending_gui_tasks_lock: threading.Lock = threading.Lock() + self._pending_dialog_lock: threading.Lock = threading.Lock() + self._api_event_queue_lock: threading.Lock = threading.Lock() + self._rag_engine_lock: threading.Lock = threading.Lock() + self._project_switch_lock: threading.Lock = threading.Lock() # --- Shared background pool + proactive warmup (startup_speedup_20260606) --- self._io_pool = make_io_pool() @@ -831,371 +811,375 @@ class AppController: if not defer_warmup: self.start_warmup() - # --- Internal State --- - self._ai_status: str = "idle" - self._mma_status: str = "idle" - self._pending_gui_tasks: List[Dict[str, Any]] = [] - self._api_event_queue: List[Dict[str, Any]] = [] - self._pending_dialog: Optional[ConfirmDialog] = None - self._pending_dialog_open: bool = False - self._pending_actions: Dict[str, ConfirmDialog] = {} - self._pending_ask_dialog: bool = False - self._loop_thread: Optional[threading.Thread] = None - self._worker_status: Dict[str, str] = {} - self._pending_patch_text: Optional[str] = None - self._pending_patch_files: List[str] = [] - self._show_patch_modal: bool = False - self._patch_error_message: Optional[str] = None - self._tool_log: List[Dict[str, Any]] = [] - self._tool_stats: Dict[str, Dict[str, Any]] = {} - self._cached_cache_stats: Dict[str, Any] = {} - self._cached_files: List[str] = [] - self._token_history: List[Dict[str, Any]] = [] - self._session_start_time: float = time.time() - self._ticket_start_times: dict[str, float] = {} - self._avg_ticket_time: float = 0.0 - self._completed_ticket_count: int = 0 - self._comms_log: List[Dict[str, Any]] = [] - self._last_telemetry_time: float = 0.0 - self._current_provider: str = "gemini" - self._current_model: str = "gemini-2.5-flash-lite" - self._autofocus_response_tab = False - self._show_track_proposal_modal: bool = False - self._pending_comms: List[Dict[str, Any]] = [] - self._pending_tool_calls: List[Dict[str, Any]] = [] - self._pending_history_adds: List[Dict[str, Any]] = [] - self._perf_last_update: float = 0.0 - self._last_autosave: float = time.time() - self._ask_dialog_open: bool = False - self._ask_request_id: Optional[str] = None - self._ask_tool_data: Optional[Dict[str, Any]] = None - self._pending_mma_approvals: List[Dict[str, Any]] = [] - self._mma_approval_open: bool = False - self._mma_approval_edit_mode: bool = False - self._project_switch_in_progress: bool = False - self._project_switch_pending_path: Optional[str] = None - self._mma_approval_payload: str = "" - self._pending_mma_spawns: List[Dict[str, Any]] = [] - self._mma_spawn_open: bool = False - self._mma_spawn_edit_mode: bool = False - self._mma_spawn_prompt: str = '' - self._mma_spawn_context: str = '' - self._trigger_blink: bool = False - self._is_blinking: bool = False - self._blink_start_time: float = 0.0 - self._trigger_script_blink: bool = False - self._is_script_blinking: bool = False - self._script_blink_start_time: float = 0.0 - self._scroll_disc_to_bottom: bool = False - self._scroll_comms_to_bottom: bool = False - self._scroll_tool_calls_to_bottom: bool = False - self._gemini_cache_text: str = "" - self._last_stable_md: str = '' - self._token_stats: Dict[str, Any] = {} - self._comms_log_dirty: bool = True - self._tool_log_dirty: bool = True - self._token_stats_dirty: bool = True - self._tier_stream_last_len: Dict[str, int] = {} - self._current_session_usage = None - self._current_mma_tier_usage = None - self.vendor_quota: Dict[str, Any] = {} - self.last_error: Optional[Dict[str, str]] = None - self.token_tracker: Dict[str, Any] = {"used": 0, "limit": 0, "cache_hits": 0, "cache_misses": 0} - self._current_token_history = None - self._current_session_start_time = None - self._inject_file_path: str = "" - self._inject_mode: str = "skeleton" - self._inject_preview: str = "" - self._show_inject_modal: bool = False - self._editing_preset_name: str = "" - self._editing_preset_content: str = "" - self._editing_preset_temperature: float = 0.0 - self._editing_preset_top_p: float = 0.0 + #region: --- Internal State --- + self._ai_status: str = "idle" + self._mma_status: str = "idle" + self._pending_gui_tasks: List[Dict[str, Any]] = [] + self._api_event_queue: List[Dict[str, Any]] = [] + self._pending_dialog: Optional[ConfirmDialog] = None + self._pending_dialog_open: bool = False + self._pending_actions: Dict[str, ConfirmDialog] = {} + self._pending_ask_dialog: bool = False + self._loop_thread: Optional[threading.Thread] = None + self._worker_status: Dict[str, str] = {} + self._pending_patch_text: Optional[str] = None + self._pending_patch_files: List[str] = [] + self._show_patch_modal: bool = False + self._patch_error_message: Optional[str] = None + self._tool_log: List[Dict[str, Any]] = [] + self._tool_stats: Dict[str, Dict[str, Any]] = {} + self._cached_cache_stats: Dict[str, Any] = {} + self._cached_files: List[str] = [] + self._token_history: List[Dict[str, Any]] = [] + self._session_start_time: float = time.time() + self._ticket_start_times: dict[str, float] = {} + self._avg_ticket_time: float = 0.0 + self._completed_ticket_count: int = 0 + self._comms_log: List[Dict[str, Any]] = [] + self._last_telemetry_time: float = 0.0 + self._current_provider: str = "gemini" + self._current_model: str = "gemini-2.5-flash-lite" + self._autofocus_response_tab = False + self._show_track_proposal_modal: bool = False + self._pending_comms: List[Dict[str, Any]] = [] + self._pending_tool_calls: List[Dict[str, Any]] = [] + self._pending_history_adds: List[Dict[str, Any]] = [] + self._perf_last_update: float = 0.0 + self._last_autosave: float = time.time() + self._ask_dialog_open: bool = False + self._ask_request_id: Optional[str] = None + self._ask_tool_data: Optional[Dict[str, Any]] = None + self._pending_mma_approvals: List[Dict[str, Any]] = [] + self._mma_approval_open: bool = False + self._mma_approval_edit_mode: bool = False + self._project_switch_in_progress: bool = False + self._project_switch_pending_path: Optional[str] = None + self._mma_approval_payload: str = "" + self._pending_mma_spawns: List[Dict[str, Any]] = [] + self._mma_spawn_open: bool = False + self._mma_spawn_edit_mode: bool = False + self._mma_spawn_prompt: str = '' + self._mma_spawn_context: str = '' + self._trigger_blink: bool = False + self._is_blinking: bool = False + self._blink_start_time: float = 0.0 + self._trigger_script_blink: bool = False + self._is_script_blinking: bool = False + self._script_blink_start_time: float = 0.0 + self._scroll_disc_to_bottom: bool = False + self._scroll_comms_to_bottom: bool = False + self._scroll_tool_calls_to_bottom: bool = False + self._gemini_cache_text: str = "" + self._last_stable_md: str = '' + self._token_stats: Dict[str, Any] = {} + self._comms_log_dirty: bool = True + self._tool_log_dirty: bool = True + self._token_stats_dirty: bool = True + self._tier_stream_last_len: Dict[str, int] = {} + self._current_session_usage = None + self._current_mma_tier_usage = None + self.vendor_quota: Dict[str, Any] = {} + self.last_error: Optional[Dict[str, str]] = None + self.token_tracker: Dict[str, Any] = {"used": 0, "limit": 0, "cache_hits": 0, "cache_misses": 0} + self._current_token_history = None + self._current_session_start_time = None + self._inject_file_path: str = "" + self._inject_mode: str = "skeleton" + self._inject_preview: str = "" + self._show_inject_modal: bool = False + self._editing_preset_name: str = "" + self._editing_preset_content: str = "" + self._editing_preset_temperature: float = 0.0 + self._editing_preset_top_p: float = 0.0 self._editing_preset_max_output_tokens: int = 4096 - self._editing_preset_scope: str = "project" - self._editing_tool_preset_name: str = "" - self._editing_tool_preset_categories: Dict[str, Dict[str, Any]] = {} - self._editing_tool_preset_scope: str = "project" - self._show_base_prompt_diff_modal: bool = False - self._autosave_interval: float = 60.0 - self._show_add_ticket_form: bool = False - self._track_discussion_active: bool = False + self._editing_preset_scope: str = "project" + self._editing_tool_preset_name: str = "" + self._editing_tool_preset_categories: Dict[str, Dict[str, Any]] = {} + self._editing_tool_preset_scope: str = "project" + self._show_base_prompt_diff_modal: bool = False + self._autosave_interval: float = 60.0 + self._show_add_ticket_form: bool = False + self._track_discussion_active: bool = False + #endregion: Internal State - # --- Core State --- - self.config: Dict[str, Any] = {} - self.project: Dict[str, Any] = {} - self.active_project_path: str = "" - self.project_paths: List[str] = [] - self.active_discussion: str = "main" - self.disc_entries: List[Dict[str, Any]] = [] - self.discussion_sent_markdown: str = "" + #region: --- Core State --- + self.config: Dict[str, Any] = {} + self.project: Dict[str, Any] = {} + self.active_project_path: str = "" + self.project_paths: List[str] = [] + self.active_discussion: str = "main" + self.disc_entries: List[Dict[str, Any]] = [] + self.discussion_sent_markdown: str = "" self.discussion_sent_system_prompt: str = "" - self.disc_roles: List[str] = [] - self.tracks: List[Dict[str, Any]] = [] - self.active_track: Optional[models.Track] = None - self.engines: Dict[str, multi_agent_conductor.ConductorEngine] = {} - self.mma_streams: Dict[str, str] = {} - self.MAX_STREAM_SIZE: int = 10 * 1024 - self.session_usage: Dict[str, Any] = { - "input_tokens": 0, - "output_tokens": 0, - "cache_read_input_tokens": 0, + self.disc_roles: List[str] = [] + self.tracks: List[Dict[str, Any]] = [] + self.active_track: Optional[models.Track] = None + self.engines: Dict[str, multi_agent_conductor.ConductorEngine] = {} + self.mma_streams: Dict[str, str] = {} + self.MAX_STREAM_SIZE: int = 10 * 1024 + self.session_usage: Dict[str, Any] = { + "input_tokens": 0, + "output_tokens": 0, + "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, - "total_tokens": 0, - "last_latency": 0.0, - "percentage": 0.0 + "total_tokens": 0, + "last_latency": 0.0, + "percentage": 0.0 } self.mma_tier_usage: Dict[str, Dict[str, Any]] = { "Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview", "tool_preset": None}, "Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview", "tool_preset": None}, - "Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None}, - "Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None}, + "Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None}, + "Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None}, } - self.mcp_config: models.MCPConfiguration = models.MCPConfiguration() - self.view_presets: list[models.NamedViewPreset] = [] - self.rag_config: Optional[models.RAGConfig] = None - self.rag_status: str = 'idle' - self.temperature: float = 0.0 - self.top_p: float = 1.0 - self.max_tokens: int = 8192 - self.history_trunc_limit: int = 8000 - self.ai_response: str = '' - self.last_md: str = '' - self.last_aggregate_markdown: str = '' - self.last_resolved_system_prompt: str = '' - self.last_md_path: Optional[Path] = None - self.last_file_items: List[Any] = [] - self.send_thread: Optional[threading.Thread] = None - self.show_windows: Dict[str, bool] = {} - self.show_script_output: bool = False - self.text_viewer_title: str = '' - self.text_viewer_content: str = '' - self.text_viewer_type: str = 'text' - self.perf_history: Dict[str, List[float]] = {'frame_time': [0.0]*100, 'fps': [0.0]*100, 'cpu': [0.0]*100, 'input_lag': [0.0]*100} - self.mma_step_mode: bool = False - self.active_tier: Optional[str] = None - self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1") - self.is_viewing_prior_session: bool = False - self.prior_session_entries: List[Dict[str, Any]] = [] - self.prior_tool_calls: List[Dict[str, Any]] = [] - self.prior_disc_entries: List[Dict[str, Any]] = [] - self.prior_mma_dashboard_state = {} - self.diagnostic_log: List[Dict[str, Any]] = [] - self.ui_active_tool_preset: str | None = None - self.ui_active_bias_profile: str | None = None - self.available_models: List[str] = [] - self.all_available_models: Dict[str, List[str]] = {} - self.proposed_tracks: List[Dict[str, Any]] = [] - self.show_preset_manager_window: bool = False + self.mcp_config: models.MCPConfiguration = models.MCPConfiguration() + self.view_presets: list[models.NamedViewPreset] = [] + self.rag_config: Optional[models.RAGConfig] = None + self.rag_status: str = 'idle' + self.temperature: float = 0.0 + self.top_p: float = 1.0 + self.max_tokens: int = 8192 + self.history_trunc_limit: int = 8000 + self.ai_response: str = '' + self.last_md: str = '' + self.last_aggregate_markdown: str = '' + self.last_resolved_system_prompt: str = '' + self.last_md_path: Optional[Path] = None + self.last_file_items: List[Any] = [] + self.send_thread: Optional[threading.Thread] = None + self.show_windows: Dict[str, bool] = {} + self.show_script_output: bool = False + self.text_viewer_title: str = '' + self.text_viewer_content: str = '' + self.text_viewer_type: str = 'text' + self.perf_history: Dict[str, List[float]] = {'frame_time': [0.0]*100, 'fps': [0.0]*100, 'cpu': [0.0]*100, 'input_lag': [0.0]*100} + self.mma_step_mode: bool = False + self.active_tier: Optional[str] = None + self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1") + self.is_viewing_prior_session: bool = False + self.prior_session_entries: List[Dict[str, Any]] = [] + self.prior_tool_calls: List[Dict[str, Any]] = [] + self.prior_disc_entries: List[Dict[str, Any]] = [] + self.prior_mma_dashboard_state = {} + self.diagnostic_log: List[Dict[str, Any]] = [] + self.ui_active_tool_preset: str | None = None + self.ui_active_bias_profile: str | None = None + self.available_models: List[str] = [] + self.all_available_models: Dict[str, List[str]] = {} + self.proposed_tracks: List[Dict[str, Any]] = [] + self.show_preset_manager_window: bool = False self.show_tool_preset_manager_window: bool = False - self.show_persona_editor_window: bool = False + self.show_persona_editor_window: bool = False + #endregion: Internal State - # --- UI State --- - self.ui_ai_input: str = "" - self.ui_disc_new_name_input: str = "" - self.ui_disc_new_role_input: str = "" - self.ui_epic_input: str = "" - self.ui_new_track_name: str = "" - self.ui_new_track_desc: str = "" - self.ui_new_track_type: str = "feature" - self.ui_project_conductor_dir: str = "" - self.ui_conductor_setup_summary: str = "" - self.ui_last_script_text: str = "" - self.ui_last_script_output: str = "" - self.ui_new_ticket_id: str = "" - self.ui_new_ticket_desc: str = "" - self.ui_new_ticket_target: str = "" - self.ui_new_ticket_deps: str = "" - self.ui_output_dir: str = "" - self.ui_files_base_dir: str = "" - self.ui_shots_base_dir: str = "" - self.ui_project_git_dir: str = "" - self.ui_project_system_prompt: str = "" - self.ui_project_execution_mode: str = "native" - self.ui_gemini_cli_path: str = "gemini" - self.ui_word_wrap: bool = True - self.ui_auto_add_history: bool = False - self.ui_separate_message_panel: bool = False - self.ui_separate_response_panel: bool = False + #region: --- UI State --- + self.ui_ai_input: str = "" + self.ui_disc_new_name_input: str = "" + self.ui_disc_new_role_input: str = "" + self.ui_epic_input: str = "" + self.ui_new_track_name: str = "" + self.ui_new_track_desc: str = "" + self.ui_new_track_type: str = "feature" + self.ui_project_conductor_dir: str = "" + self.ui_conductor_setup_summary: str = "" + self.ui_last_script_text: str = "" + self.ui_last_script_output: str = "" + self.ui_new_ticket_id: str = "" + self.ui_new_ticket_desc: str = "" + self.ui_new_ticket_target: str = "" + self.ui_new_ticket_deps: str = "" + self.ui_output_dir: str = "" + self.ui_files_base_dir: str = "" + self.ui_shots_base_dir: str = "" + self.ui_project_git_dir: str = "" + self.ui_project_system_prompt: str = "" + self.ui_project_execution_mode: str = "native" + self.ui_gemini_cli_path: str = "gemini" + self.ui_word_wrap: bool = True + self.ui_auto_add_history: bool = False + self.ui_separate_message_panel: bool = False + self.ui_separate_response_panel: bool = False self.ui_separate_tool_calls_panel: bool = False - self.ui_global_system_prompt: str = "" - self.ui_base_system_prompt: str = "" - self.ui_use_default_base_prompt: bool = True - self.ui_project_context_marker: str = "" - self.ui_agent_tools: Dict[str, bool] = {} - self.ui_manual_approve: bool = False - self.ui_disc_truncate_pairs: int = 2 - self.ui_auto_scroll_comms: bool = True - self.ui_auto_scroll_tool_calls: bool = True - self.ui_focus_agent: Optional[str] = None - self.ui_active_persona: str = "" + self.ui_global_system_prompt: str = "" + self.ui_base_system_prompt: str = "" + self.ui_use_default_base_prompt: bool = True + self.ui_project_context_marker: str = "" + self.ui_agent_tools: Dict[str, bool] = {} + self.ui_manual_approve: bool = False + self.ui_disc_truncate_pairs: int = 2 + self.ui_auto_scroll_comms: bool = True + self.ui_auto_scroll_tool_calls: bool = True + self.ui_focus_agent: Optional[str] = None + self.ui_active_persona: str = "" + #endregion: UI State # --- Media/Context --- - self.files: List[models.FileItem] = [] + self.files: List[models.FileItem] = [] self.context_files: List[models.FileItem] = [] - self.screenshots: List[str] = [] + self.screenshots: List[str] = [] # --- Services --- self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue() - self.rag_engine: Optional[Any] = None + self.rag_engine: Optional[Any] = None - # --- Configuration Maps --- + #region: --- Configuration Maps --- self._settable_fields: Dict[str, str] = { - 'ai_input': 'ui_ai_input', - 'project_git_dir': 'ui_project_git_dir', - 'auto_add_history': 'ui_auto_add_history', - 'disc_new_name_input': 'ui_disc_new_name_input', - 'gcli_path': 'ui_gemini_cli_path', - 'output_dir': 'ui_output_dir', - 'files_base_dir': 'ui_files_base_dir', - 'files': 'ui_file_paths', - 'screenshots': 'screenshots', - 'ai_status': 'ai_status', - 'ai_response': 'ai_response', - 'active_discussion': 'active_discussion', - 'current_provider': 'current_provider', - 'current_model': 'current_model', - 'token_budget_pct': '_token_budget_pct', - 'token_budget_current': '_token_budget_current', - 'token_budget_label': '_token_budget_label', - 'show_confirm_modal': 'show_confirm_modal', - 'mma_epic_input': 'ui_epic_input', - 'mma_status': 'mma_status', - 'rag_status': 'rag_status', - 'rag_enabled': 'rag_enabled', - 'rag_source': 'rag_source', - 'rag_emb_provider': 'rag_emb_provider', - 'rag_mcp_server': 'rag_mcp_server', - 'rag_mcp_tool': 'rag_mcp_tool', - 'rag_chunk_size': 'rag_chunk_size', - 'rag_chunk_overlap': 'rag_chunk_overlap', - 'rag_collection_name': 'rag_collection_name', - 'mcp_config_json': 'mcp_config_json', - 'mma_active_tier': 'active_tier', - 'ui_new_track_name': 'ui_new_track_name', - 'ui_new_track_desc': 'ui_new_track_desc', - 'manual_approve': 'ui_manual_approve', - 'global_system_prompt': 'ui_global_system_prompt', - 'project_system_prompt': 'ui_project_system_prompt', - 'base_system_prompt': 'ui_base_system_prompt', - 'use_default_base_prompt': 'ui_use_default_base_prompt', - 'show_base_prompt_diff_modal': '_show_base_prompt_diff_modal', - 'global_preset_name': 'ui_global_preset_name', - 'project_preset_name': 'ui_project_preset_name', - 'ui_active_tool_preset': 'ui_active_tool_preset', - 'ui_active_bias_profile': 'ui_active_bias_profile', - 'temperature': 'temperature', - 'max_tokens': 'max_tokens', - 'show_preset_manager_window': 'show_preset_manager_window', - 'show_tool_preset_manager_window': 'show_tool_preset_manager_window', - 'show_persona_editor_window': 'show_persona_editor_window', - '_editing_preset_name': '_editing_preset_name', - '_editing_preset_content': '_editing_preset_content', - '_editing_preset_temperature': '_editing_preset_temperature', - '_editing_preset_top_p': '_editing_preset_top_p', + 'ai_input': 'ui_ai_input', + 'project_git_dir': 'ui_project_git_dir', + 'auto_add_history': 'ui_auto_add_history', + 'disc_new_name_input': 'ui_disc_new_name_input', + 'gcli_path': 'ui_gemini_cli_path', + 'output_dir': 'ui_output_dir', + 'files_base_dir': 'ui_files_base_dir', + 'files': 'ui_file_paths', + 'screenshots': 'screenshots', + 'ai_status': 'ai_status', + 'ai_response': 'ai_response', + 'active_discussion': 'active_discussion', + 'current_provider': 'current_provider', + 'current_model': 'current_model', + 'token_budget_pct': '_token_budget_pct', + 'token_budget_current': '_token_budget_current', + 'token_budget_label': '_token_budget_label', + 'show_confirm_modal': 'show_confirm_modal', + 'mma_epic_input': 'ui_epic_input', + 'mma_status': 'mma_status', + 'rag_status': 'rag_status', + 'rag_enabled': 'rag_enabled', + 'rag_source': 'rag_source', + 'rag_emb_provider': 'rag_emb_provider', + 'rag_mcp_server': 'rag_mcp_server', + 'rag_mcp_tool': 'rag_mcp_tool', + 'rag_chunk_size': 'rag_chunk_size', + 'rag_chunk_overlap': 'rag_chunk_overlap', + 'rag_collection_name': 'rag_collection_name', + 'mcp_config_json': 'mcp_config_json', + 'mma_active_tier': 'active_tier', + 'ui_new_track_name': 'ui_new_track_name', + 'ui_new_track_desc': 'ui_new_track_desc', + 'manual_approve': 'ui_manual_approve', + 'global_system_prompt': 'ui_global_system_prompt', + 'project_system_prompt': 'ui_project_system_prompt', + 'base_system_prompt': 'ui_base_system_prompt', + 'use_default_base_prompt': 'ui_use_default_base_prompt', + 'show_base_prompt_diff_modal': '_show_base_prompt_diff_modal', + 'global_preset_name': 'ui_global_preset_name', + 'project_preset_name': 'ui_project_preset_name', + 'ui_active_tool_preset': 'ui_active_tool_preset', + 'ui_active_bias_profile': 'ui_active_bias_profile', + 'temperature': 'temperature', + 'max_tokens': 'max_tokens', + 'show_preset_manager_window': 'show_preset_manager_window', + 'show_tool_preset_manager_window': 'show_tool_preset_manager_window', + 'show_persona_editor_window': 'show_persona_editor_window', + '_editing_preset_name': '_editing_preset_name', + '_editing_preset_content': '_editing_preset_content', + '_editing_preset_temperature': '_editing_preset_temperature', + '_editing_preset_top_p': '_editing_preset_top_p', '_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens', - '_editing_preset_scope': '_editing_preset_scope', - '_editing_tool_preset_name': '_editing_tool_preset_name', - '_editing_tool_preset_categories': '_editing_tool_preset_categories', - '_editing_tool_preset_scope': '_editing_tool_preset_scope', - 'show_windows': 'show_windows', - 'ui_separate_task_dag': 'ui_separate_task_dag', - 'ui_separate_usage_analytics': 'ui_separate_usage_analytics', - 'ui_separate_tier1': 'ui_separate_tier1', - 'ui_separate_tier2': 'ui_separate_tier2', - 'ui_separate_tier3': 'ui_separate_tier3', - 'ui_separate_tier4': 'ui_separate_tier4', - 'text_viewer_title': 'text_viewer_title', - 'text_viewer_type': 'text_viewer_type', - 'disc_entries': 'disc_entries', - 'ui_file_paths': 'ui_file_paths', - 'ui_auto_switch_layout': 'ui_auto_switch_layout', - 'ui_tier_layout_bindings': 'ui_tier_layout_bindings', - 'ui_focus_agent': 'ui_focus_agent' + '_editing_preset_scope': '_editing_preset_scope', + '_editing_tool_preset_name': '_editing_tool_preset_name', + '_editing_tool_preset_categories': '_editing_tool_preset_categories', + '_editing_tool_preset_scope': '_editing_tool_preset_scope', + 'show_windows': 'show_windows', + 'ui_separate_task_dag': 'ui_separate_task_dag', + 'ui_separate_usage_analytics': 'ui_separate_usage_analytics', + 'ui_separate_tier1': 'ui_separate_tier1', + 'ui_separate_tier2': 'ui_separate_tier2', + 'ui_separate_tier3': 'ui_separate_tier3', + 'ui_separate_tier4': 'ui_separate_tier4', + 'text_viewer_title': 'text_viewer_title', + 'text_viewer_type': 'text_viewer_type', + 'disc_entries': 'disc_entries', + 'ui_file_paths': 'ui_file_paths', + 'ui_auto_switch_layout': 'ui_auto_switch_layout', + 'ui_tier_layout_bindings': 'ui_tier_layout_bindings', + 'ui_focus_agent': 'ui_focus_agent' } self._gettable_fields = dict(self._settable_fields) self._gettable_fields.update({ - 'show_windows': 'show_windows', # Key 'show_windows' maps to field 'show_windows' on controller - 'ui_focus_agent': 'ui_focus_agent', - 'active_discussion': 'active_discussion', - '_track_discussion_active': '_track_discussion_active', - 'proposed_tracks': 'proposed_tracks', - 'mma_streams': 'mma_streams', - '_worker_status': '_worker_status', - 'active_track': 'active_track', - 'active_tickets': 'active_tickets', - 'tracks': 'tracks', - 'thinking_indicator': 'thinking_indicator', - 'operations_live_indicator': 'operations_live_indicator', - 'prior_session_indicator': 'prior_session_indicator', - '_show_patch_modal': '_show_patch_modal', - '_pending_patch_text': '_pending_patch_text', - '_pending_patch_files': '_pending_patch_files', - '_inject_file_path': '_inject_file_path', - '_inject_mode': '_inject_mode', - '_inject_preview': '_inject_preview', - '_show_inject_modal': '_show_inject_modal', - 'bg_shader_enabled': 'bg_shader_enabled', - 'global_system_prompt': 'ui_global_system_prompt', - 'project_system_prompt': 'ui_project_system_prompt', - 'base_system_prompt': 'ui_base_system_prompt', - 'use_default_base_prompt': 'ui_use_default_base_prompt', - 'show_base_prompt_diff_modal': '_show_base_prompt_diff_modal', - 'global_preset_name': 'ui_global_preset_name', - 'project_preset_name': 'ui_project_preset_name', - 'ui_active_tool_preset': 'ui_active_tool_preset', - 'ui_active_bias_profile': 'ui_active_bias_profile', - 'temperature': 'temperature', - 'max_tokens': 'max_tokens', - 'show_preset_manager_window': 'show_preset_manager_window', - 'show_tool_preset_manager_window': 'show_tool_preset_manager_window', - 'show_persona_editor_window': 'show_persona_editor_window', - '_editing_preset_name': '_editing_preset_name', - '_editing_preset_content': '_editing_preset_content', - '_editing_preset_temperature': '_editing_preset_temperature', - '_editing_preset_top_p': '_editing_preset_top_p', + 'show_windows': 'show_windows', # Key 'show_windows' maps to field 'show_windows' on controller + 'ui_focus_agent': 'ui_focus_agent', + 'active_discussion': 'active_discussion', + '_track_discussion_active': '_track_discussion_active', + 'proposed_tracks': 'proposed_tracks', + 'mma_streams': 'mma_streams', + '_worker_status': '_worker_status', + 'active_track': 'active_track', + 'active_tickets': 'active_tickets', + 'tracks': 'tracks', + 'thinking_indicator': 'thinking_indicator', + 'operations_live_indicator': 'operations_live_indicator', + 'prior_session_indicator': 'prior_session_indicator', + '_show_patch_modal': '_show_patch_modal', + '_pending_patch_text': '_pending_patch_text', + '_pending_patch_files': '_pending_patch_files', + '_inject_file_path': '_inject_file_path', + '_inject_mode': '_inject_mode', + '_inject_preview': '_inject_preview', + '_show_inject_modal': '_show_inject_modal', + 'bg_shader_enabled': 'bg_shader_enabled', + 'global_system_prompt': 'ui_global_system_prompt', + 'project_system_prompt': 'ui_project_system_prompt', + 'base_system_prompt': 'ui_base_system_prompt', + 'use_default_base_prompt': 'ui_use_default_base_prompt', + 'show_base_prompt_diff_modal': '_show_base_prompt_diff_modal', + 'global_preset_name': 'ui_global_preset_name', + 'project_preset_name': 'ui_project_preset_name', + 'ui_active_tool_preset': 'ui_active_tool_preset', + 'ui_active_bias_profile': 'ui_active_bias_profile', + 'temperature': 'temperature', + 'max_tokens': 'max_tokens', + 'show_preset_manager_window': 'show_preset_manager_window', + 'show_tool_preset_manager_window': 'show_tool_preset_manager_window', + 'show_persona_editor_window': 'show_persona_editor_window', + '_editing_preset_name': '_editing_preset_name', + '_editing_preset_content': '_editing_preset_content', + '_editing_preset_temperature': '_editing_preset_temperature', + '_editing_preset_top_p': '_editing_preset_top_p', '_editing_preset_max_output_tokens': '_editing_preset_max_output_tokens', - '_editing_preset_scope': '_editing_preset_scope', - 'ui_separate_task_dag': 'ui_separate_task_dag', - 'ui_separate_usage_analytics': 'ui_separate_usage_analytics', - 'ui_separate_tier1': 'ui_separate_tier1', - 'ui_separate_tier2': 'ui_separate_tier2', - 'ui_separate_tier3': 'ui_separate_tier3', - 'ui_separate_tier4': 'ui_separate_tier4', - 'text_viewer_title': 'text_viewer_title', - 'text_viewer_type': 'text_viewer_type' + '_editing_preset_scope': '_editing_preset_scope', + 'ui_separate_task_dag': 'ui_separate_task_dag', + 'ui_separate_usage_analytics': 'ui_separate_usage_analytics', + 'ui_separate_tier1': 'ui_separate_tier1', + 'ui_separate_tier2': 'ui_separate_tier2', + 'ui_separate_tier3': 'ui_separate_tier3', + 'ui_separate_tier4': 'ui_separate_tier4', + 'text_viewer_title': 'text_viewer_title', + 'text_viewer_type': 'text_viewer_type' }) - self.context_preset_manager = ContextPresetManager() - self.perf_monitor = performance_monitor.get_monitor() + self.context_preset_manager = ContextPresetManager() + self.perf_monitor = performance_monitor.get_monitor() self._perf_profiling_enabled = False self._gui_task_handlers: Dict[str, Callable] = { - "refresh_api_metrics": _handle_refresh_api_metrics, - "set_ai_status": _handle_set_ai_status, - "set_mma_status": _handle_set_mma_status, - "handle_ai_response": _handle_ai_response, - "mma_stream": _handle_mma_stream, - "mma_stream_append": _handle_mma_stream, - "show_track_proposal": _handle_show_track_proposal, - "mma_state_update": _handle_mma_state_update, - "custom_callback": _handle_custom_callback, - "set_value": _handle_set_value, - "click": _handle_click, - "drag": _handle_drag, - "right_click": _handle_right_click, - "select_list_item": _handle_select_list_item, - "ask": _handle_ask, - "clear_ask": _handle_clear_ask, - "mma_step_approval": _handle_mma_step_approval, - "mma_spawn_approval": _handle_mma_spawn_approval, - "ticket_started": _handle_ticket_started, - "ticket_completed": _handle_ticket_completed, - "bead_updated": _handle_bead_updated, - "set_comms_dirty": _handle_set_comms_dirty, - "set_tool_log_dirty": _handle_set_tool_log_dirty, + "refresh_api_metrics": _handle_refresh_api_metrics, + "set_ai_status": _handle_set_ai_status, + "set_mma_status": _handle_set_mma_status, + "handle_ai_response": _handle_ai_response, + "mma_stream": _handle_mma_stream, + "mma_stream_append": _handle_mma_stream, + "show_track_proposal": _handle_show_track_proposal, + "mma_state_update": _handle_mma_state_update, + "custom_callback": _handle_custom_callback, + "set_value": _handle_set_value, + "click": _handle_click, + "drag": _handle_drag, + "right_click": _handle_right_click, + "select_list_item": _handle_select_list_item, + "ask": _handle_ask, + "clear_ask": _handle_clear_ask, + "mma_step_approval": _handle_mma_step_approval, + "mma_spawn_approval": _handle_mma_spawn_approval, + "ticket_started": _handle_ticket_started, + "ticket_completed": _handle_ticket_completed, + "bead_updated": _handle_bead_updated, + "set_comms_dirty": _handle_set_comms_dirty, + "set_tool_log_dirty": _handle_set_tool_log_dirty, "refresh_from_project": _handle_refresh_from_project, - "show_patch_modal": _handle_show_patch_modal, - "hide_patch_modal": _handle_hide_patch_modal, + "show_patch_modal": _handle_show_patch_modal, + "hide_patch_modal": _handle_hide_patch_modal, } + #endregion: Configuration Map self._init_actions() @property @@ -1218,15 +1202,15 @@ class AppController: if self._first_frame_ts is not None: return self._first_frame_ts = ts if ts is not None else time.time() try: - warmup_ms = (self._warmup_done_ts - self._init_start_ts) * 1000 if self._warmup_done_ts is not None else 0.0 + warmup_ms = (self._warmup_done_ts - self._init_start_ts) * 1000 if self._warmup_done_ts is not None else 0.0 frame_after_init_ms = (self._first_frame_ts - self._init_start_ts) * 1000 # Phase breakdown: which main-thread phase dominated? - init_phase_ms = (self._appcontroller_init_done_ts - self._init_start_ts) * 1000 if self._appcontroller_init_done_ts is not None else None - gui_phase_ms = (self._gui_run_started_ts - self._appcontroller_init_done_ts) * 1000 if (self._gui_run_started_ts is not None and self._appcontroller_init_done_ts is not None) else None - render_phase_ms = (self._first_frame_ts - self._gui_run_started_ts) * 1000 if self._gui_run_started_ts is not None else None - phase_parts = [] - if init_phase_ms is not None: phase_parts.append(f"init={init_phase_ms:.0f}ms") - if gui_phase_ms is not None: phase_parts.append(f"gui_setup={gui_phase_ms:.0f}ms") + init_phase_ms = (self._appcontroller_init_done_ts - self._init_start_ts) * 1000 if self._appcontroller_init_done_ts is not None else None + gui_phase_ms = (self._gui_run_started_ts - self._appcontroller_init_done_ts) * 1000 if (self._gui_run_started_ts is not None and self._appcontroller_init_done_ts is not None) else None + render_phase_ms = (self._first_frame_ts - self._gui_run_started_ts) * 1000 if self._gui_run_started_ts is not None else None + phase_parts = [] + if init_phase_ms is not None: phase_parts.append(f"init={init_phase_ms:.0f}ms") + if gui_phase_ms is not None: phase_parts.append(f"gui_setup={gui_phase_ms:.0f}ms") if render_phase_ms is not None: phase_parts.append(f"first_render={render_phase_ms:.0f}ms") phase_str = f" [{', '.join(phase_parts)}]" if phase_parts else "" if self._warmup_done_ts is None: @@ -1247,20 +1231,18 @@ class AppController: and _gui_run_started_ts. The 3-phase breakdown in startup_timeline() then shows: AppController init, GUI bundle setup, first render. [SDM: src/app_controller.py:mark_gui_run_started] [C: src/gui_2.py:App.run]""" - if self._appcontroller_init_done_ts is None: - self._appcontroller_init_done_ts = ts if ts is not None else time.time() - if self._gui_run_started_ts is None: - self._gui_run_started_ts = ts if ts is not None else time.time() + if self._appcontroller_init_done_ts is None: self._appcontroller_init_done_ts = ts if ts is not None else time.time() + if self._gui_run_started_ts is None: self._gui_run_started_ts = ts if ts is not None else time.time() def startup_timeline(self) -> dict: """Returns a dict with all startup timestamps and precomputed deltas. Fields: init_start_ts, appcontroller_init_done_ts, gui_run_started_ts, warmup_done_ts, first_frame_ts, warmup_ms, appcontroller_init_ms, gui_setup_ms, first_render_ms, first_frame_after_init_ms, first_frame_after_warmup_ms. The 3 phase breakdowns answer 'which main-thread phase dominated?': AppController init, GUI bundle setup, first render. [SDM: src/app_controller.py:startup_timeline] [C: src/api_hooks.py:HookHandler.do_GET /api/startup_timeline]""" result: dict = { - "cold_start_ts": self.cold_start_ts, - "init_start_ts": self._init_start_ts, + "cold_start_ts": self.cold_start_ts, + "init_start_ts": self._init_start_ts, "appcontroller_init_done_ts": self._appcontroller_init_done_ts, - "gui_run_started_ts": self._gui_run_started_ts, - "warmup_done_ts": self._warmup_done_ts, - "first_frame_ts": self._first_frame_ts, + "gui_run_started_ts": self._gui_run_started_ts, + "warmup_done_ts": self._warmup_done_ts, + "first_frame_ts": self._first_frame_ts, } if self.cold_start_ts is not None: result["module_imports_ms"] = (self._init_start_ts - self.cold_start_ts) * 1000 @@ -1268,7 +1250,7 @@ class AppController: result["cold_start_to_first_frame_ms"] = (self._first_frame_ts - self.cold_start_ts) * 1000 else: result["module_imports_ms"] = None - if self._warmup_done_ts is not None: + if self._warmup_done_ts is not None: result["warmup_ms"] = (self._warmup_done_ts - self._init_start_ts) * 1000 else: result["warmup_ms"] = None @@ -1343,7 +1325,6 @@ class AppController: def _update_inject_preview(self) -> None: """ - Updates the preview content based on the selected file and injection mode. [C: src/gui_2.py:App._render_text_viewer_window, 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] """ @@ -1360,7 +1341,7 @@ class AppController: with open(target_path, "r", encoding="utf-8") as f: content = f.read() if self._inject_mode == "skeleton" and target_path.endswith(".py"): - parser = ASTParser("python") + parser = ASTParser("python") preview = parser.get_skeleton(content) else: preview = content @@ -1399,6 +1380,7 @@ class AppController: @property def rag_enabled(self) -> bool: return self.rag_config.enabled if self.rag_config else False + def _sync_rag_engine(self): """ Re-initializes the RAG engine in a background thread to avoid blocking the UI. @@ -1425,6 +1407,7 @@ class AppController: @property def rag_enabled(self) -> bool: return self.rag_config.enabled if self.rag_config else False + @rag_enabled.setter def rag_enabled(self, value: bool) -> None: if self.rag_config: @@ -1434,6 +1417,7 @@ class AppController: @property def rag_source(self) -> str: return self.rag_config.vector_store.provider if self.rag_config else 'mock' + @rag_source.setter def rag_source(self, value: str) -> None: if self.rag_config: @@ -1443,6 +1427,7 @@ class AppController: @property def rag_emb_provider(self) -> str: return self.rag_config.embedding_provider if self.rag_config else 'gemini' + @rag_emb_provider.setter def rag_emb_provider(self, value: str) -> None: if self.rag_config: @@ -1452,6 +1437,7 @@ class AppController: @property def rag_chunk_size(self) -> int: return self.rag_config.chunk_size if self.rag_config else 1000 + @rag_chunk_size.setter def rag_chunk_size(self, value: int) -> None: if self.rag_config: self.rag_config.chunk_size = value @@ -1550,19 +1536,19 @@ class AppController: self._right_clickable_actions: dict[str, Callable[..., Any]] = {} self._predefined_callbacks: dict[str, Callable[..., Any]] = { '_test_callback_func_write_to_file': self._test_callback_func_write_to_file, - '_set_env_var': lambda k, v: os.environ.update({k: v}), - '_set_attr': lambda k, v: setattr(self, k, v), - '_apply_preset': self._apply_preset, - '_cb_save_preset': self._cb_save_preset, - '_cb_delete_preset': self._cb_delete_preset, - '_cb_save_tool_preset': self._cb_save_tool_preset, - '_cb_delete_tool_preset': self._cb_delete_tool_preset, - '_switch_project': self._switch_project, - '_refresh_from_project': self._refresh_from_project, - 'save_workspace_profile': self._cb_save_workspace_profile, - 'load_workspace_profile': self._cb_load_workspace_profile, - 'delete_workspace_profile': self._cb_delete_workspace_profile, - '_cb_create_track': self._cb_create_track, + '_set_env_var': lambda k, v: os.environ.update({k: v}), + '_set_attr': lambda k, v: setattr(self, k, v), + '_apply_preset': self._apply_preset, + '_cb_save_preset': self._cb_save_preset, + '_cb_delete_preset': self._cb_delete_preset, + '_cb_save_tool_preset': self._cb_save_tool_preset, + '_cb_delete_tool_preset': self._cb_delete_tool_preset, + '_switch_project': self._switch_project, + '_refresh_from_project': self._refresh_from_project, + 'save_workspace_profile': self._cb_save_workspace_profile, + 'load_workspace_profile': self._cb_load_workspace_profile, + 'delete_workspace_profile': self._cb_delete_workspace_profile, + '_cb_create_track': self._cb_create_track, } def _update_gcli_adapter(self, path: str) -> None: @@ -1578,9 +1564,8 @@ class AppController: def _process_pending_gui_tasks(self) -> None: """ - - Processes pending GUI tasks from the queue on the main render thread. - [C: src/gui_2.py:App._render_main_interface, 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_process_pending_gui_tasks_drag, tests/test_process_pending_gui_tasks.py:test_process_pending_gui_tasks_right_click, tests/test_process_pending_gui_tasks.py:test_redundant_calls_in_process_pending_gui_tasks] + Processes pending GUI tasks from the queue on the main render thread. + [C: src/gui_2.py:App._render_main_interface, 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_process_pending_gui_tasks_drag, tests/test_process_pending_gui_tasks.py:test_process_pending_gui_tasks_right_click, 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: @@ -1589,8 +1574,8 @@ class AppController: metrics = self.perf_monitor.get_metrics() self.event_queue.websocket_server.broadcast("telemetry", metrics) - if not self._pending_gui_tasks: - return + if not self._pending_gui_tasks: return + with self._pending_gui_tasks_lock: tasks = self._pending_gui_tasks[:] self._pending_gui_tasks.clear() @@ -1607,9 +1592,8 @@ class AppController: def _process_pending_history_adds(self) -> None: """ - - Synchronizes pending history entries to the active discussion and project state. - [C: src/gui_2.py:App._render_main_interface] + Synchronizes pending history entries to the active discussion and project state. + [C: src/gui_2.py:App._render_main_interface] """ with self._pending_history_adds_lock: items = self._pending_history_adds[:] @@ -1621,9 +1605,9 @@ class AppController: item.get("role", "unknown") if item.get("role") and item["role"] not in self.disc_roles: self.disc_roles.append(item["role"]) - disc_sec = self.project.get("discussion", {}) + disc_sec = self.project.get("discussion", {}) discussions = disc_sec.get("discussions", {}) - disc_data = discussions.get(self.active_discussion) + disc_data = discussions.get(self.active_discussion) if disc_data is not None: if item.get("disc_title", self.active_discussion) == self.active_discussion: if self.disc_entries is not disc_data.get("history"): @@ -1636,9 +1620,8 @@ class AppController: def _process_pending_tool_calls(self) -> bool: """ - - Drains pending tool calls into the tool log. Returns True if any were processed. - [C: src/gui_2.py:App._render_main_interface] + Drains pending tool calls into the tool log. Returns True if any were processed. + [C: src/gui_2.py:App._render_main_interface] """ with self._pending_tool_calls_lock: items = self._pending_tool_calls[:] @@ -1660,83 +1643,58 @@ class AppController: with open(callback_path, "w") as f: f.write(data) - def _handle_approve_script(self, user_data=None) -> None: - """Approves the currently pending PowerShell script.""" - with self._pending_dialog_lock: - dlg = self._pending_dialog - if dlg: - with dlg._condition: - dlg._approved = True - dlg._done = True - dlg._condition.notify_all() - self._pending_dialog = None - - def _handle_reject_script(self, user_data=None) -> None: - """Rejects the currently pending PowerShell script.""" - with self._pending_dialog_lock: - dlg = self._pending_dialog - if dlg: - with dlg._condition: - dlg._approved = False - dlg._done = True - dlg._condition.notify_all() - self._pending_dialog = None - def init_state(self): """ - - Initializes the application state from configurations. - [C: src/gui_2.py:App.__init__, src/gui_2.py:App._save_paths, src/gui_2.py:App._set_external_editor_default, 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_context_composition_decoupled.py:test_do_generate_uses_context_files, 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] + Initializes the application state from configurations. + [C: src/gui_2.py:App.__init__, src/gui_2.py:App._save_paths, src/gui_2.py:App._set_external_editor_default, 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_context_composition_decoupled.py:test_do_generate_uses_context_files, 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 - self.ui_separate_tier1 = False - self.ui_separate_tier2 = False - self.ui_separate_tier3 = False - self.ui_separate_tier4 = False - self.ui_separate_message_panel = False - self.ui_separate_response_panel = False + self.active_tickets = [] + self.ui_separate_task_dag = False + self.ui_separate_usage_analytics = False + self.ui_separate_tier1 = False + self.ui_separate_tier2 = False + self.ui_separate_tier3 = False + self.ui_separate_tier4 = False + self.ui_separate_message_panel = False + self.ui_separate_response_panel = False self.ui_separate_tool_calls_panel = False - self.ui_separate_external_tools = False + self.ui_separate_external_tools = False self.config = models.load_config() theme.load_from_config(self.config) ai_cfg = self.config.get("ai", {}) - self._current_provider = ai_cfg.get("provider", "gemini") - self._current_model = ai_cfg.get("model", "gemini-2.5-flash-lite") - self.temperature = ai_cfg.get("temperature", 0.0) - self.top_p = ai_cfg.get("top_p", 1.0) - self.max_tokens = ai_cfg.get("max_tokens", 8192) + self._current_provider = ai_cfg.get("provider", "gemini") + self._current_model = ai_cfg.get("model", "gemini-2.5-flash-lite") + self.temperature = ai_cfg.get("temperature", 0.0) + self.top_p = ai_cfg.get("top_p", 1.0) + self.max_tokens = ai_cfg.get("max_tokens", 8192) self.history_trunc_limit = ai_cfg.get("history_trunc_limit", 8000) projects_cfg = self.config.get("projects", {}) - self.project_paths = list(projects_cfg.get("paths", [])) + self.project_paths = list(projects_cfg.get("paths", [])) self.active_project_path = projects_cfg.get("active", "") self._load_active_project() # Track 5: Path isolation overrides if self.project and 'paths' in self.project: project_root = Path(self.active_project_path).parent if self.active_project_path else Path.cwd() - proj_paths = self.project.get('paths', {}) + proj_paths = self.project.get('paths', {}) if 'logs_dir' in proj_paths: lpath = Path(proj_paths['logs_dir']) - if not lpath.is_absolute(): - lpath = project_root / lpath + if not lpath.is_absolute(): lpath = project_root / lpath os.environ['SLOP_LOGS_DIR'] = str(lpath) if 'scripts_dir' in proj_paths: spath = Path(proj_paths['scripts_dir']) - if not spath.is_absolute(): - spath = project_root / spath + if not spath.is_absolute(): spath = project_root / spath os.environ['SLOP_SCRIPTS_DIR'] = str(spath) paths.reset_resolved() - path_info = paths.get_full_path_info() - self.ui_logs_dir = str(path_info['logs_dir']['path']) + path_info = paths.get_full_path_info() + self.ui_logs_dir = str(path_info['logs_dir']['path']) self.ui_scripts_dir = str(path_info['scripts_dir']['path']) if not self.project or not isinstance(self.project, dict) or "project" not in self.project: name = Path(self.active_project_path).stem if self.active_project_path else "unnamed" self.project = project_manager.default_project(name) - self.workspace_manager = workspace_manager.WorkspaceManager(project_root=Path(self.active_project_path).parent if self.active_project_path else None) + self.workspace_manager = workspace_manager.WorkspaceManager(project_root=Path(self.active_project_path).parent if self.active_project_path else None) self.workspace_profiles = self.workspace_manager.load_all_profiles() # Deserialize FileItems in files.paths raw_paths = self.project.get("files", {}).get("paths", []) @@ -1753,8 +1711,9 @@ class AppController: self.disc_roles = list(disc_sec.get("roles", ["User", "AI", "Vendor API", "System", "Reasoning", "Context"])) self.active_discussion = disc_sec.get("active", "main") disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {}) - with self._disc_entries_lock: - self.disc_entries[:] = models.parse_history_entries(disc_data.get("history", []), self.disc_roles) + + with self._disc_entries_lock: self.disc_entries[:] = models.parse_history_entries(disc_data.get("history", []), self.disc_roles) + # UI state self.ui_output_dir = self.project.get("output", {}).get("output_dir", "./md_gen") self.ui_files_base_dir = self.project.get("files", {}).get("base_dir", ".") @@ -1771,13 +1730,13 @@ class AppController: self.ui_base_system_prompt = self.config.get("ai", {}).get("base_system_prompt", "") self.ui_use_default_base_prompt = self.config.get("ai", {}).get("use_default_base_prompt", True) self.ui_project_context_marker = proj_meta.get("context_marker", "") - + self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None) self.presets = self.preset_manager.load_all() self.tool_preset_manager = tool_presets.ToolPresetManager(Path(self.active_project_path).parent if self.active_project_path else None) self.tool_presets = self.tool_preset_manager.load_all_presets() self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles() - + mcp_path = self.project.get('project', {}).get('mcp_config_path') or self.config.get('ai', {}).get('mcp_config_path') if mcp_path: mcp_p = Path(mcp_path) @@ -1789,13 +1748,13 @@ class AppController: self.mcp_config = models.MCPConfiguration() else: self.mcp_config = models.MCPConfiguration() - + rag_data = self.config.get('rag') if rag_data: self.rag_config = models.RAGConfig.from_dict(rag_data) else: self.rag_config = models.RAGConfig() - + self.rag_engine = None if self.rag_config.enabled: self._sync_rag_engine() @@ -1803,16 +1762,16 @@ class AppController: from src.personas import PersonaManager self.persona_manager = PersonaManager(Path(self.active_project_path).parent if self.active_project_path else None) self.personas = self.persona_manager.load_all() - + self._fetch_models(self.current_provider) - + self.ui_active_tool_preset = os.environ.get('SLOP_TOOL_PRESET') or ai_cfg.get("active_tool_preset") self.ui_active_bias_profile = ai_cfg.get("active_bias_profile") ai_client.set_tool_preset(self.ui_active_tool_preset) ai_client.set_bias_profile(self.ui_active_bias_profile) self.ui_global_preset_name = ai_cfg.get("active_preset") self.ui_project_preset_name = proj_meta.get("active_preset") - + gui_cfg = self.config.get("gui", {}) self.ui_separate_message_panel = gui_cfg.get('separate_message_panel', False) self.ui_separate_response_panel = gui_cfg.get('separate_response_panel', False) @@ -1821,30 +1780,30 @@ class AppController: self.ui_tier_layout_bindings = gui_cfg.get("tier_layout_bindings", {"Tier 1": "", "Tier 2": "", "Tier 3": "", "Tier 4": ""}) from src import bg_shader bg_shader.get_bg().enabled = gui_cfg.get("bg_shader_enabled", False) - + _default_windows = { - "Project Settings": True, - "Files & Media": True, - "AI Settings": True, - "MMA Dashboard": True, - "Task DAG": False, - "Usage Analytics": False, - "Tier 1": False, - "Tier 2": False, - "Tier 3": False, - "Tier 4": False, - "Tier 1: Strategy": True, + "Project Settings": True, + "Files & Media": True, + "AI Settings": True, + "MMA Dashboard": True, + "Task DAG": False, + "Usage Analytics": False, + "Tier 1": False, + "Tier 2": False, + "Tier 3": False, + "Tier 4": False, + "Tier 1: Strategy": True, "Tier 2: Tech Lead": True, - "Tier 3: Workers": True, - "Tier 4: QA": True, - "Discussion Hub": True, - "Operations Hub": True, - "Message": False, - "Response": False, - "Tool Calls": False, - "Text Viewer": False, - "Theme": True, - "Log Management": False, + "Tier 3: Workers": True, + "Tier 4: QA": True, + "Discussion Hub": True, + "Operations Hub": True, + "Message": False, + "Response": False, + "Tool Calls": False, + "Text Viewer": False, + "Theme": True, + "Log Management": False, } saved = self.config.get("gui", {}).get("show_windows", {}) self.show_windows = {k: saved.get(k, v) for k, v in _default_windows.items()} @@ -1958,7 +1917,7 @@ class AppController: if not old_call['result']: old_call['result'] = output break - + if kind == 'response' and 'usage' in payload: u = payload['usage'] for k in ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'total_tokens']: @@ -1973,7 +1932,7 @@ class AppController: 'output': u.get('output_tokens', 0) or 0, 'model': entry.get('model', 'unknown') }) - + if kind == "history_add": content = payload.get("content", payload.get("text", payload.get("message", ""))) content = _resolve_log_ref(content, session_dir) @@ -2157,39 +2116,6 @@ class AppController: print(f"Error during log pruning: {e}") self.submit_io(run_prune) - def _fetch_models(self, provider: str) -> None: - """ - [C: src/gui_2.py:App.run] - """ - # In the desktop GUI, model listing imports the provider SDKs (the same - # ~2s C-extension load warmup pays for). Defer it until the first frame is - # painted so it doesn't contend for the GIL during window creation; the - # deferred fetch is fired from start_warmup(). - if self._defer_warmup and not self._warmup_started: - self._pending_fetch_provider = provider - return - self.ai_status = "fetching models..." - - def do_fetch() -> None: - try: - for p in models.PROVIDERS: - try: - self.all_available_models[p] = ai_client.list_models(p) - except Exception as e: - self.all_available_models[p] = [] - - models_list = self.all_available_models.get(provider, []) - self.available_models = models_list - if self.current_model not in models_list and models_list: - self.current_model = models_list[0] - ai_client.set_provider(self._current_provider, self.current_model) - if self.ai_status == "fetching models...": - self.ai_status = f"models loaded: {len(models_list)}" - except Exception as e: - if self.ai_status == "fetching models...": - self.ai_status = f"model fetch error: {e}" - self.submit_io(do_fetch) - def start_services(self, app: Any = None): """ Starts background threads. @@ -2225,10 +2151,9 @@ class AppController: def warmup_status(self) -> dict: """ - - Snapshot of the warmup progress. {pending, completed, failed}. - Cheap (lock-guarded copy). Polled by the GUI status indicator. - [SDM: src/app_controller.py:warmup_status] + Snapshot of the warmup progress. {pending, completed, failed}. + Cheap (lock-guarded copy). Polled by the GUI status indicator. + [SDM: src/app_controller.py:warmup_status] """ return self._warmup.status() @@ -2240,7 +2165,6 @@ class AppController: def is_warmup_done(self) -> bool: """ - True once all warmup jobs have completed (or failed). [SDM: src/app_controller.py:is_warmup_done] """ @@ -2265,7 +2189,6 @@ class AppController: def wait_for_warmup(self, timeout: Optional[float] = None) -> bool: """ - Block until warmup completes. Returns True on done, False on timeout. [SDM: src/app_controller.py:wait_for_warmup] """ @@ -2273,7 +2196,6 @@ class AppController: def on_warmup_complete(self, callback: Callable[[dict], None]) -> None: """ - Register a callback for warmup completion. If already done, fires immediately on the calling thread. [SDM: src/app_controller.py:on_warmup_complete] @@ -2351,150 +2273,6 @@ class AppController: self.submit_io(queue_fallback) self._process_event_queue() - def _process_event_queue(self) -> None: - """Listens for and processes events from the SyncEventQueue.""" - while True: - event_name, payload = self.event_queue.get() - if event_name == "shutdown": - break - if event_name == "user_request": - self.submit_io(self._handle_request_event, payload) - elif event_name == "gui_task": - with self._pending_gui_tasks_lock: - # Directly append the task from the hook server. - # It already contains 'action' and any necessary fields. - self._pending_gui_tasks.append(payload) - elif event_name == "mma_state_update": - with self._pending_gui_tasks_lock: - self._pending_gui_tasks.append({ - "action": "mma_state_update", - "payload": payload - }) - elif event_name == "mma_stream": - with self._pending_gui_tasks_lock: - self._pending_gui_tasks.append({ - "action": "mma_stream_append", - "payload": payload - }) - elif event_name in ("mma_spawn_approval", "mma_step_approval"): - with self._pending_gui_tasks_lock: - # These payloads already contain the 'action' field - self._pending_gui_tasks.append(payload) - elif event_name == "response": - with self._pending_gui_tasks_lock: - self._pending_gui_tasks.append({ - "action": "handle_ai_response", - "payload": payload - }) - if self.test_hooks_enabled: - with self._api_event_queue_lock: - self._api_event_queue.append({"type": "response", "payload": payload}) - elif event_name == "ticket_started": - with self._pending_gui_tasks_lock: - self._pending_gui_tasks.append({ - "action": "ticket_started", - "payload": payload - }) - elif event_name == "ticket_completed": - with self._pending_gui_tasks_lock: - self._pending_gui_tasks.append({ - "action": "ticket_completed", - "payload": payload - }) - elif event_name == "refresh_external_mcps": - import asyncio - asyncio.run(self.refresh_external_mcps()) - - def _handle_request_event(self, event: events.UserRequestEvent) -> None: - """ - 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...' - - user_msg = event.prompt - - # 1. RAG Retrieval (Enrich prompt before logging to history) - if self.rag_engine and self.rag_config and self.rag_config.enabled: - try: - chunks = self.rag_engine.search(user_msg) - if chunks: - context_block = "## Retrieved Context\n\n" - for i, chunk in enumerate(chunks): - path = chunk.get("metadata", {}).get("path", "unknown") - context_block += f"### Chunk {i+1} (Source: {path})\n{chunk.get('document', '')}\n\n" - user_msg = context_block + user_msg - except Exception as e: - sys.stderr.write(f"RAG search error: {e}\n") - sys.stderr.flush() - - # 2. Symbol Resolution (Enrich prompt before logging to history) - try: - symbols = parse_symbols(user_msg) - file_paths = [f['path'] for f in event.file_items] - for symbol in symbols: - res = get_symbol_definition(symbol, file_paths) - if res: - file_path, definition, line = res - user_msg += f'\n\n[Definition: {symbol} from {file_path} (line {line})]\n```python\n{definition}\n```' - except Exception as e: - sys.stderr.write(f"Symbol resolution error: {e}\n") - sys.stderr.flush() - - # 3. Log the final enriched prompt to history - self.event_queue.put("comms", { - "kind": "request", - "payload": { - "message": user_msg, - "collapsed": False - } - }) - - ai_client.set_current_tier(None) # Ensure main discussion is untagged - # Clear response area for new turn - self.ai_response = "" - csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()]) - custom_prompt = "\n\n".join(csp) - ai_client.set_custom_system_prompt(custom_prompt) - ai_client.set_base_system_prompt(self.ui_base_system_prompt) - ai_client.set_use_default_base_prompt(self.ui_use_default_base_prompt) - ai_client.set_project_context_marker(self.ui_project_context_marker) - self.last_resolved_system_prompt = ai_client.get_combined_system_prompt() - self.discussion_sent_markdown = event.stable_md - self.discussion_sent_system_prompt = self.last_resolved_system_prompt - ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit, self.top_p) - ai_client.set_agent_tools(self.ui_agent_tools) # Force update adapter path right before send to bypass potential duplication issues - self._update_gcli_adapter(self.ui_gemini_cli_path) - try: - resp = ai_client.send( - event.stable_md, - user_msg, - event.base_dir, - event.file_items, - event.disc_text, - stream=True, - stream_callback=lambda text: self._on_ai_stream(text), - pre_tool_callback=self._confirm_and_run, - qa_callback=ai_client.run_tier4_analysis, - patch_callback=ai_client.run_tier4_patch_callback, - rag_engine=None # Already handled above - ) - self.event_queue.put("response", {"text": resp, "status": "done", "role": "AI"}) - except ai_client.ProviderError as e: - self.event_queue.put("response", {"text": e.ui_message(), "status": "error", "role": "Vendor API"}) - except Exception as e: - self.event_queue.put("response", {"text": f"ERROR: {e}", "status": "error", "role": "System"}) - - 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() - with self._pending_tool_calls_lock: - self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier}) - @property def _pending_mma_spawn(self) -> Optional[Dict[str, Any]]: return self._pending_mma_spawns[0] if self._pending_mma_spawns else None @@ -2534,10 +2312,9 @@ class AppController: def create_api(self) -> FastAPI: """ - - Creates and configures the FastAPI application for headless mode. - [SDM: src/app_controller.py:AppController.create_api] - [C: src/gui_2.py:App.run, tests/test_headless_service.py:TestHeadlessAPI.setUp] + Creates and configures the FastAPI application for headless mode. + [SDM: src/app_controller.py:AppController.create_api] + [C: src/gui_2.py:App.run, tests/test_headless_service.py:TestHeadlessAPI.setUp] """ fastapi = _require_warmed("fastapi") FastAPI = fastapi.FastAPI @@ -2627,23 +2404,6 @@ class AppController: return _api_token_stats(self) return api - 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 _handle_clear_summary_cache(self, user_data: Any = None) -> None: - self.summary_cache.clear() - self.ai_status = 'summary cache cleared' - - 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 clear_last_error(self) -> None: """Reset last_error after a successful response cycle. [C: src/vendor_state.py:get_vendor_state] @@ -2856,101 +2616,6 @@ class AppController: #endregion: Usage Analytics - #region: Context - - 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 save_context_preset(self, preset: models.ContextPreset) -> None: - self.context_preset_manager.save_preset(self.project, preset) - self._save_active_project() - - def load_context_preset(self, name: str) -> models.ContextPreset: - presets = self.context_preset_manager.load_all(self.project) - if name not in presets: - raise KeyError(f"Context preset '{name}' not found.") - preset = presets[name] - - # Update only temporary context state, not project files - import copy - self.context_files = [] - for f in preset.files: - fi = models.FileItem(path=f.path, view_mode=f.view_mode) - fi.custom_slices = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else [] - fi.ast_mask = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {} - fi.ast_signatures = getattr(f, 'ast_signatures', False) - fi.ast_definitions = getattr(f, 'ast_definitions', False) - self.context_files.append(fi) - - self.screenshots = list(preset.screenshots) - return preset - - def _update_cached_stats(self) -> None: - from src import ai_client - self._cached_cache_stats = ai_client.get_gemini_cache_stats() - 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() - - #endregion: Context - - #region: RAG - - def _set_rag_status(self, status: str) -> None: - """Thread-safe update of rag_status via the GUI task queue.""" - with self._pending_gui_tasks_lock: - self._pending_gui_tasks.append({ - "action": "set_value", - "item": "rag_status", - "value": status - }) - - def _rebuild_rag_index(self) -> None: - """Background thread that re-indexes all files in the current project.""" - if not self.rag_config or not self.rag_config.enabled or not self.rag_engine: - return - - def _run(): - try: - self._set_rag_status("indexing...") - import concurrent.futures - - # 1. Incremental indexing of current files in parallel - with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: - futures = [] - def do_index(p): - if self.rag_engine: self.rag_engine.index_file(p) - for f in self.files: - path = f.path if hasattr(f, "path") else str(f) - futures.append(executor.submit(do_index, path)) - concurrent.futures.wait(futures) - - # 2. Cleanup stale entries (files no longer tracked) - indexed_paths = self.rag_engine.get_all_indexed_paths() - current_paths = {f.path if hasattr(f, "path") else str(f) for f in self.files} - stale_paths = [p for p in indexed_paths if p not in current_paths] - if stale_paths: - self.rag_engine.delete_documents_by_path(stale_paths) - - self._set_rag_status("ready") - except Exception as e: - self._set_rag_status(f"error: {e}") - - self.submit_io(_run) - - #endregion: RAG - #region: Project def _configure_mcp_for_project(self) -> None: @@ -3161,8 +2826,153 @@ class AppController: #endregion: Project + #region: Context + + 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 _handle_clear_summary_cache(self, user_data: Any = None) -> None: + self.summary_cache.clear() + self.ai_status = 'summary cache cleared' + + 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_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 save_context_preset(self, preset: models.ContextPreset) -> None: + self.context_preset_manager.save_preset(self.project, preset) + self._save_active_project() + + def load_context_preset(self, name: str) -> models.ContextPreset: + presets = self.context_preset_manager.load_all(self.project) + if name not in presets: + raise KeyError(f"Context preset '{name}' not found.") + preset = presets[name] + + # Update only temporary context state, not project files + import copy + self.context_files = [] + for f in preset.files: + fi = models.FileItem(path=f.path, view_mode=f.view_mode) + fi.custom_slices = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else [] + fi.ast_mask = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {} + fi.ast_signatures = getattr(f, 'ast_signatures', False) + fi.ast_definitions = getattr(f, 'ast_definitions', False) + self.context_files.append(fi) + + self.screenshots = list(preset.screenshots) + return preset + + def _update_cached_stats(self) -> None: + from src import ai_client + self._cached_cache_stats = ai_client.get_gemini_cache_stats() + 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() + + #endregion: Context + + #region: RAG + + def _set_rag_status(self, status: str) -> None: + """Thread-safe update of rag_status via the GUI task queue.""" + with self._pending_gui_tasks_lock: + self._pending_gui_tasks.append({ + "action": "set_value", + "item": "rag_status", + "value": status + }) + + def _rebuild_rag_index(self) -> None: + """Background thread that re-indexes all files in the current project.""" + if not self.rag_config or not self.rag_config.enabled or not self.rag_engine: + return + + def _run(): + try: + self._set_rag_status("indexing...") + import concurrent.futures + + # 1. Incremental indexing of current files in parallel + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: + futures = [] + def do_index(p): + if self.rag_engine: self.rag_engine.index_file(p) + for f in self.files: + path = f.path if hasattr(f, "path") else str(f) + futures.append(executor.submit(do_index, path)) + concurrent.futures.wait(futures) + + # 2. Cleanup stale entries (files no longer tracked) + indexed_paths = self.rag_engine.get_all_indexed_paths() + current_paths = {f.path if hasattr(f, "path") else str(f) for f in self.files} + stale_paths = [p for p in indexed_paths if p not in current_paths] + if stale_paths: + self.rag_engine.delete_documents_by_path(stale_paths) + + self._set_rag_status("ready") + except Exception as e: + self._set_rag_status(f"error: {e}") + + self.submit_io(_run) + + #endregion: RAG + #region: AI Settings + def _fetch_models(self, provider: str) -> None: + """ + [C: src/gui_2.py:App.run] + """ + # In the desktop GUI, model listing imports the provider SDKs (the same + # ~2s C-extension load warmup pays for). Defer it until the first frame is + # painted so it doesn't contend for the GIL during window creation; the + # deferred fetch is fired from start_warmup(). + if self._defer_warmup and not self._warmup_started: + self._pending_fetch_provider = provider + return + self.ai_status = "fetching models..." + + def do_fetch() -> None: + try: + for p in models.PROVIDERS: + try: + self.all_available_models[p] = ai_client.list_models(p) + except Exception as e: + self.all_available_models[p] = [] + + models_list = self.all_available_models.get(provider, []) + self.available_models = models_list + if self.current_model not in models_list and models_list: + self.current_model = models_list[0] + ai_client.set_provider(self._current_provider, self.current_model) + if self.ai_status == "fetching models...": + self.ai_status = f"models loaded: {len(models_list)}" + except Exception as e: + if self.ai_status == "fetching models...": + self.ai_status = f"model fetch error: {e}" + self.submit_io(do_fetch) + def _apply_preset(self, name: str, scope: str) -> None: """ [C: src/gui_2.py:App._render_system_prompts_panel] @@ -3637,6 +3447,96 @@ class AppController: #region: Operations + def _handle_request_event(self, event: events.UserRequestEvent) -> None: + """ + 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...' + + user_msg = event.prompt + + # 1. RAG Retrieval (Enrich prompt before logging to history) + if self.rag_engine and self.rag_config and self.rag_config.enabled: + try: + chunks = self.rag_engine.search(user_msg) + if chunks: + context_block = "## Retrieved Context\n\n" + for i, chunk in enumerate(chunks): + path = chunk.get("metadata", {}).get("path", "unknown") + context_block += f"### Chunk {i+1} (Source: {path})\n{chunk.get('document', '')}\n\n" + user_msg = context_block + user_msg + except Exception as e: + sys.stderr.write(f"RAG search error: {e}\n") + sys.stderr.flush() + + # 2. Symbol Resolution (Enrich prompt before logging to history) + try: + symbols = parse_symbols(user_msg) + file_paths = [f['path'] for f in event.file_items] + for symbol in symbols: + res = get_symbol_definition(symbol, file_paths) + if res: + file_path, definition, line = res + user_msg += f'\n\n[Definition: {symbol} from {file_path} (line {line})]\n```python\n{definition}\n```' + except Exception as e: + sys.stderr.write(f"Symbol resolution error: {e}\n") + sys.stderr.flush() + + # 3. Log the final enriched prompt to history + self.event_queue.put("comms", { + "kind": "request", + "payload": { + "message": user_msg, + "collapsed": False + } + }) + + ai_client.set_current_tier(None) # Ensure main discussion is untagged + # Clear response area for new turn + self.ai_response = "" + csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()]) + custom_prompt = "\n\n".join(csp) + ai_client.set_custom_system_prompt(custom_prompt) + ai_client.set_base_system_prompt(self.ui_base_system_prompt) + ai_client.set_use_default_base_prompt(self.ui_use_default_base_prompt) + ai_client.set_project_context_marker(self.ui_project_context_marker) + self.last_resolved_system_prompt = ai_client.get_combined_system_prompt() + self.discussion_sent_markdown = event.stable_md + self.discussion_sent_system_prompt = self.last_resolved_system_prompt + ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit, self.top_p) + ai_client.set_agent_tools(self.ui_agent_tools) # Force update adapter path right before send to bypass potential duplication issues + self._update_gcli_adapter(self.ui_gemini_cli_path) + try: + resp = ai_client.send( + event.stable_md, + user_msg, + event.base_dir, + event.file_items, + event.disc_text, + stream=True, + stream_callback=lambda text: self._on_ai_stream(text), + pre_tool_callback=self._confirm_and_run, + qa_callback=ai_client.run_tier4_analysis, + patch_callback=ai_client.run_tier4_patch_callback, + rag_engine=None # Already handled above + ) + self.event_queue.put("response", {"text": resp, "status": "done", "role": "AI"}) + except ai_client.ProviderError as e: + self.event_queue.put("response", {"text": e.ui_message(), "status": "error", "role": "Vendor API"}) + except Exception as e: + self.event_queue.put("response", {"text": f"ERROR: {e}", "status": "error", "role": "System"}) + + 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() + with self._pending_tool_calls_lock: + self._pending_tool_calls.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier}) + def _offload_entry_payload(self, entry: Dict[str, Any]) -> Dict[str, Any]: optimized = copy.deepcopy(entry) kind = optimized.get("kind") @@ -3870,10 +3770,86 @@ class AppController: return "py_get_skeleton" return "other" + def _handle_approve_script(self, user_data=None) -> None: + """Approves the currently pending PowerShell script.""" + with self._pending_dialog_lock: + dlg = self._pending_dialog + if dlg: + with dlg._condition: + dlg._approved = True + dlg._done = True + dlg._condition.notify_all() + self._pending_dialog = None + + def _handle_reject_script(self, user_data=None) -> None: + """Rejects the currently pending PowerShell script.""" + with self._pending_dialog_lock: + dlg = self._pending_dialog + if dlg: + with dlg._condition: + dlg._approved = False + dlg._done = True + dlg._condition.notify_all() + self._pending_dialog = None + #endregion: Operations #region MMA (Controller) + def _process_event_queue(self) -> None: + """Listens for and processes events from the SyncEventQueue.""" + while True: + event_name, payload = self.event_queue.get() + if event_name == "shutdown": + break + if event_name == "user_request": + self.submit_io(self._handle_request_event, payload) + elif event_name == "gui_task": + with self._pending_gui_tasks_lock: + # Directly append the task from the hook server. + # It already contains 'action' and any necessary fields. + self._pending_gui_tasks.append(payload) + elif event_name == "mma_state_update": + with self._pending_gui_tasks_lock: + self._pending_gui_tasks.append({ + "action": "mma_state_update", + "payload": payload + }) + elif event_name == "mma_stream": + with self._pending_gui_tasks_lock: + self._pending_gui_tasks.append({ + "action": "mma_stream_append", + "payload": payload + }) + elif event_name in ("mma_spawn_approval", "mma_step_approval"): + with self._pending_gui_tasks_lock: + # These payloads already contain the 'action' field + self._pending_gui_tasks.append(payload) + elif event_name == "response": + with self._pending_gui_tasks_lock: + self._pending_gui_tasks.append({ + "action": "handle_ai_response", + "payload": payload + }) + if self.test_hooks_enabled: + with self._api_event_queue_lock: + self._api_event_queue.append({"type": "response", "payload": payload}) + elif event_name == "ticket_started": + with self._pending_gui_tasks_lock: + self._pending_gui_tasks.append({ + "action": "ticket_started", + "payload": payload + }) + elif event_name == "ticket_completed": + with self._pending_gui_tasks_lock: + self._pending_gui_tasks.append({ + "action": "ticket_completed", + "payload": payload + }) + elif event_name == "refresh_external_mcps": + import asyncio + asyncio.run(self.refresh_external_mcps()) + def _cb_plan_epic(self) -> None: """ [C: src/gui_2.py:App._render_mma_epic_planner, tests/test_mma_orchestration_gui.py:test_cb_plan_epic_launches_thread] diff --git a/src/gui_2.py b/src/gui_2.py index cfd45aa8..23320b28 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -107,11 +107,11 @@ from src import cost_tracker from src import history from src import imgui_scopes as imscope from src import paths -from src import presets +# from src import presets from src import project_manager from src import session_logger from src import log_registry -from src import log_pruner +# from src import log_pruner from src import models from src.models import GenerateRequest, ConfirmRequest from src import mcp_client @@ -140,11 +140,11 @@ def hide_tk_root() -> Tk: # Standard Color Constants (now bound to the theming system) def C_OUT() -> imgui.ImVec4: return theme.get_color("status_info") -def C_IN() -> imgui.ImVec4: return theme.get_color("status_success") +def C_IN() -> imgui.ImVec4: return theme.get_color("status_success") def C_REQ() -> imgui.ImVec4: return theme.get_color("status_warning") def C_RES() -> imgui.ImVec4: return theme.get_color("status_success") -def C_TC() -> imgui.ImVec4: return theme.get_color("status_warning") -def C_TR() -> imgui.ImVec4: return theme.get_color("status_info") +def C_TC() -> imgui.ImVec4: return theme.get_color("status_warning") +def C_TR() -> imgui.ImVec4: return theme.get_color("status_info") def C_TRS() -> imgui.ImVec4: return theme.get_color("status_info") def C_LBL() -> imgui.ImVec4: return theme.get_color("text_disabled") def C_VAL() -> imgui.ImVec4: return theme.get_color("text") @@ -702,34 +702,34 @@ class App: 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], - context_files=[f.to_dict() if hasattr(f, 'to_dict') else f for f in self.context_files], - screenshots=list(self.screenshots) + 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], + context_files = [f.to_dict() if hasattr(f, 'to_dict') else f for f in self.context_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_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 + 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 @@ -762,18 +762,18 @@ class App: except Exception: ini = "" panel_states = { - "ui_separate_context_preview": getattr(self, "ui_separate_context_preview", False), - "ui_separate_message_panel": getattr(self, "ui_separate_message_panel", False), - "ui_separate_response_panel": getattr(self, "ui_separate_response_panel", False), + "ui_separate_context_preview": getattr(self, "ui_separate_context_preview", False), + "ui_separate_message_panel": getattr(self, "ui_separate_message_panel", False), + "ui_separate_response_panel": getattr(self, "ui_separate_response_panel", False), "ui_separate_tool_calls_panel": getattr(self, "ui_separate_tool_calls_panel", False), - "ui_separate_task_dag": getattr(self, "ui_separate_task_dag", False), - "ui_separate_usage_analytics": getattr(self, "ui_separate_usage_analytics", False), - "ui_separate_tier1": getattr(self, "ui_separate_tier1", False), - "ui_separate_tier2": getattr(self, "ui_separate_tier2", False), - "ui_separate_tier3": getattr(self, "ui_separate_tier3", False), - "ui_separate_tier4": getattr(self, "ui_separate_tier4", False), - "ui_separate_external_tools": getattr(self, "ui_separate_external_tools", False), - "ui_discussion_split_h": getattr(self, "ui_discussion_split_h", 300.0), + "ui_separate_task_dag": getattr(self, "ui_separate_task_dag", False), + "ui_separate_usage_analytics": getattr(self, "ui_separate_usage_analytics", False), + "ui_separate_tier1": getattr(self, "ui_separate_tier1", False), + "ui_separate_tier2": getattr(self, "ui_separate_tier2", False), + "ui_separate_tier3": getattr(self, "ui_separate_tier3", False), + "ui_separate_tier4": getattr(self, "ui_separate_tier4", False), + "ui_separate_external_tools": getattr(self, "ui_separate_external_tools", False), + "ui_discussion_split_h": getattr(self, "ui_discussion_split_h", 300.0), } return models.WorkspaceProfile( name=name, diff --git a/src/mcp_client.py b/src/mcp_client.py index dfbad2af..d11a4a42 100644 --- a/src/mcp_client.py +++ b/src/mcp_client.py @@ -1304,10 +1304,8 @@ def get_external_mcp_manager() -> ExternalMCPManager: def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str: """ - - - Dispatch an MCP tool call by name. Returns the result as a string. - [C: tests/test_gemini_cli_edge_cases.py:test_gemini_cli_parameter_resilience, tests/test_mcp_client_beads.py:test_bd_mcp_tools, tests/test_mcp_ts_integration.py:test_ts_c_get_code_outline_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_signature_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_skeleton_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_update_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_code_outline_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_signature_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_skeleton_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_update_definition_dispatch, tests/test_py_struct_tools.py:test_mcp_dispatch_errors, tests/test_py_struct_tools.py:test_mcp_dispatch_integration] + Dispatch an MCP tool call by name. Returns the result as a string. + [C: tests/test_gemini_cli_edge_cases.py:test_gemini_cli_parameter_resilience, tests/test_mcp_client_beads.py:test_bd_mcp_tools, tests/test_mcp_ts_integration.py:test_ts_c_get_code_outline_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_signature_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_skeleton_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_update_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_code_outline_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_signature_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_skeleton_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_update_definition_dispatch, tests/test_py_struct_tools.py:test_mcp_dispatch_errors, tests/test_py_struct_tools.py:test_mcp_dispatch_integration] """ # Handle aliases path = str(tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", ""))))