Private
Public Access
0
0
This commit is contained in:
2026-06-07 02:14:46 -04:00
8 changed files with 232 additions and 88 deletions
+2 -1
View File
@@ -12,7 +12,8 @@
"mcp__manual-slop__get_file_summary", "mcp__manual-slop__get_file_summary",
"mcp__manual-slop__get_tree", "mcp__manual-slop__get_tree",
"mcp__manual-slop__list_directory", "mcp__manual-slop__list_directory",
"mcp__manual-slop__py_get_skeleton" "mcp__manual-slop__py_get_skeleton",
"Bash(uv run *)"
] ]
}, },
"enableAllProjectMcpServers": true, "enableAllProjectMcpServers": true,
+11 -13
View File
@@ -12,11 +12,9 @@ use_default_base_prompt = true
[projects] [projects]
paths = [ paths = [
"C:/projects/gencpp/.ai/gencpp_sloppy.toml", "project.toml",
"C:/projects/manual_slop/manual_slop.toml",
"C:/projects/Pikuma/ps1-ai/pikuma_ps1.toml",
] ]
active = "C:/projects/Pikuma/ps1-ai/pikuma_ps1.toml" active = "project.toml"
[gui] [gui]
separate_message_panel = true separate_message_panel = true
@@ -62,23 +60,23 @@ Diagnostics = false
"Undo/Redo History" = false "Undo/Redo History" = false
[theme] [theme]
palette = "solarized_dark" palette = "Monokai"
font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf" font_path = "fonts/MapleMono-Regular.ttf"
font_size = 20.0 font_size = 20.0
scale = 1.0199999809265137 scale = 1.0199999809265137
transparency = 1.0 transparency = 1.0
child_transparency = 1.0 child_transparency = 1.0
[theme.tone_mapping.solarized_light]
brightness = 0.6899999976158142
contrast = 0.8600000143051147
gamma = 0.7699999809265137
[theme.tone_mapping.moss] [theme.tone_mapping.moss]
brightness = 1.059999942779541 brightness = 1.059999942779541
contrast = 0.5799999833106995 contrast = 0.5799999833106995
gamma = 1.059999942779541 gamma = 1.059999942779541
[theme.tone_mapping.solarized_light]
brightness = 0.6899999976158142
contrast = 0.8600000143051147
gamma = 0.7699999809265137
[theme.tone_mapping.Binks] [theme.tone_mapping.Binks]
brightness = 0.5600000023841858 brightness = 0.5600000023841858
contrast = 0.7900000214576721 contrast = 0.7900000214576721
@@ -97,8 +95,8 @@ api_key = "test-secret-key"
[paths] [paths]
conductor_dir = "C:\\projects\\gencpp\\.ai\\conductor" conductor_dir = "C:\\projects\\gencpp\\.ai\\conductor"
logs_dir = "C:\\projects\\manual_slop\\logs" logs_dir = "C:\\projects\\sloppy\\logs"
scripts_dir = "C:\\projects\\manual_slop\\scripts" scripts_dir = "C:\\projects\\sloppy\\scripts"
[rag] [rag]
enabled = false enabled = false
+19 -19
View File
@@ -44,20 +44,20 @@ Collapsed=0
DockId=0x00000010,0 DockId=0x00000010,0
[Window][Message] [Window][Message]
Pos=1448,29 Pos=561,29
Size=1465,1840 Size=1138,1195
Collapsed=0 Collapsed=0
DockId=0x00000006,1 DockId=0x00000006,1
[Window][Response] [Window][Response]
Pos=0,29 Pos=0,29
Size=1446,1840 Size=559,1195
Collapsed=0 Collapsed=0
DockId=0x00000010,5 DockId=0x00000010,5
[Window][Tool Calls] [Window][Tool Calls]
Pos=1448,29 Pos=561,29
Size=1465,1840 Size=1138,1195
Collapsed=0 Collapsed=0
DockId=0x00000006,3 DockId=0x00000006,3
@@ -77,7 +77,7 @@ DockId=0xAFC85805,2
[Window][Theme] [Window][Theme]
Pos=0,29 Pos=0,29
Size=1446,1840 Size=559,1195
Collapsed=0 Collapsed=0
DockId=0x00000010,1 DockId=0x00000010,1
@@ -105,26 +105,26 @@ Collapsed=0
DockId=0x0000000D,0 DockId=0x0000000D,0
[Window][Discussion Hub] [Window][Discussion Hub]
Pos=1448,29 Pos=561,29
Size=1465,1840 Size=1138,1195
Collapsed=0 Collapsed=0
DockId=0x00000006,0 DockId=0x00000006,0
[Window][Operations Hub] [Window][Operations Hub]
Pos=0,29 Pos=0,29
Size=1446,1840 Size=559,1195
Collapsed=0 Collapsed=0
DockId=0x00000010,4 DockId=0x00000010,4
[Window][Files & Media] [Window][Files & Media]
Pos=0,29 Pos=0,29
Size=1446,1840 Size=559,1195
Collapsed=0 Collapsed=0
DockId=0x00000010,2 DockId=0x00000010,2
[Window][AI Settings] [Window][AI Settings]
Pos=0,29 Pos=0,29
Size=1446,1840 Size=559,1195
Collapsed=0 Collapsed=0
DockId=0x00000010,3 DockId=0x00000010,3
@@ -140,8 +140,8 @@ Collapsed=0
DockId=0x00000006,2 DockId=0x00000006,2
[Window][Log Management] [Window][Log Management]
Pos=1448,29 Pos=561,29
Size=1465,1840 Size=1138,1195
Collapsed=0 Collapsed=0
DockId=0x00000006,2 DockId=0x00000006,2
@@ -410,7 +410,7 @@ DockId=0x00000006,1
[Window][Project Settings] [Window][Project Settings]
Pos=0,29 Pos=0,29
Size=1446,1840 Size=559,1195
Collapsed=0 Collapsed=0
DockId=0x00000010,0 DockId=0x00000010,0
@@ -510,7 +510,7 @@ Pos=60,60
Size=900,700 Size=900,700
Collapsed=0 Collapsed=0
[Window][###Text_Viewer] [Window][Text_Viewer]
Pos=58,169 Pos=58,169
Size=1801,1532 Size=1801,1532
Collapsed=0 Collapsed=0
@@ -520,7 +520,7 @@ Pos=156,171
Size=2176,1441 Size=2176,1441
Collapsed=0 Collapsed=0
[Window][###Text_Viewer_Unified] [Window][Text_Viewer_Unified]
Pos=182,742 Pos=182,742
Size=1163,908 Size=1163,908
Collapsed=0 Collapsed=0
@@ -829,13 +829,13 @@ Column 4 Weight=1.0000
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,29 Size=2913,1840 Split=X DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,29 Size=1699,1195 Split=X
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1446,1681 Split=Y Selected=0x3F1379AF DockNode ID=0x00000005 Parent=0x0000000B SizeRef=573,1681 Split=Y Selected=0x3F1379AF
DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x3F1379AF DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x3F1379AF
DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1465,1681 Selected=0x2C0206CE DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1138,1681 Selected=0x2C0206CE
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=488,1183 Selected=0x3AEC3498 DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=488,1183 Selected=0x3AEC3498
+1 -1
View File
@@ -9,5 +9,5 @@ active = "main"
[discussions.main] [discussions.main]
git_commit = "" git_commit = ""
last_updated = "2026-06-05T18:41:59" last_updated = "2026-06-06T13:21:40"
history = [] history = []
+10 -1
View File
@@ -421,13 +421,22 @@ def _classify_minimax_error(exc: Exception) -> ProviderError:
if "400" in body_l or "bad request" in body_l: return ProviderError("unknown", "minimax", Exception(f"MiniMax Bad Request: {body}")) if "400" in body_l or "bad request" in body_l: return ProviderError("unknown", "minimax", Exception(f"MiniMax Bad Request: {body}"))
return ProviderError("unknown", "minimax", Exception(body)) return ProviderError("unknown", "minimax", Exception(body))
def set_provider(provider: str, model: str) -> None: def set_provider(provider: str, model: str, validate: bool = True) -> None:
""" """
Updates the active LLM provider and model name. Updates the active LLM provider and model name.
When validate is True (default), the model is checked against the provider's
LIVE model list, which for gemini_cli/minimax means a blocking subprocess /
network call (and importing the provider SDK). Pass validate=False during
startup so the GUI's first frame is not blocked — AppController._fetch_models
corrects the model against the live list shortly after, off the main thread.
[C: src/app_controller.py:AppController._handle_reset_session, src/app_controller.py:AppController._init_ai_and_hooks, src/app_controller.py:AppController.current_model, src/app_controller.py:AppController.current_provider, src/app_controller.py:AppController.do_fetch, src/multi_agent_conductor.py:run_worker_lifecycle, src/orchestrator_pm.py:generate_tracks, tests/conftest.py:reset_ai_client, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_completion_logic, tests/test_deepseek_provider.py:test_deepseek_model_selection, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoning_logic, tests/test_deepseek_provider.py:test_deepseek_streaming, tests/test_deepseek_provider.py:test_deepseek_tool_calling, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_loop_termination, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_cli_parity_regression.py:test_send_invokes_adapter_send, tests/test_gui2_mcp.py:test_mcp_tool_call_is_dispatched, tests/test_minimax_provider.py:test_minimax_default_model, tests/test_minimax_provider.py:test_minimax_model_selection, tests/test_mma_agent_focus_phase1.py:test_append_comms_has_source_tier_key, tests/test_rag_integration.py:test_rag_integration, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_tier4_interceptor.py:test_gemini_provider_passes_qa_callback_to_run_script, tests/test_token_usage.py:test_token_usage_tracking] [C: src/app_controller.py:AppController._handle_reset_session, src/app_controller.py:AppController._init_ai_and_hooks, src/app_controller.py:AppController.current_model, src/app_controller.py:AppController.current_provider, src/app_controller.py:AppController.do_fetch, src/multi_agent_conductor.py:run_worker_lifecycle, src/orchestrator_pm.py:generate_tracks, tests/conftest.py:reset_ai_client, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_completion_logic, tests/test_deepseek_provider.py:test_deepseek_model_selection, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoning_logic, tests/test_deepseek_provider.py:test_deepseek_streaming, tests/test_deepseek_provider.py:test_deepseek_tool_calling, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_loop_termination, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_cli_parity_regression.py:test_send_invokes_adapter_send, tests/test_gui2_mcp.py:test_mcp_tool_call_is_dispatched, tests/test_minimax_provider.py:test_minimax_default_model, tests/test_minimax_provider.py:test_minimax_model_selection, tests/test_mma_agent_focus_phase1.py:test_append_comms_has_source_tier_key, tests/test_rag_integration.py:test_rag_integration, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_tier4_interceptor.py:test_gemini_provider_passes_qa_callback_to_run_script, tests/test_token_usage.py:test_token_usage_tracking]
""" """
global _provider, _model global _provider, _model
_provider = provider _provider = provider
if not validate:
_model = model
return
if provider == "gemini_cli": if provider == "gemini_cli":
valid_models = _list_gemini_cli_models() valid_models = _list_gemini_cli_models()
if model != "mock" and (model not in valid_models or model.startswith("deepseek")): if model != "mock" and (model not in valid_models or model.startswith("deepseek")):
+42 -3
View File
@@ -801,7 +801,7 @@ class AppController:
Owns the application state and manages background services. Owns the application state and manages background services.
""" """
def __init__(self, log_to_stderr: bool = True): def __init__(self, defer_warmup: bool = False, log_to_stderr: Optional[bool] = None):
""" """
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__] [C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
""" """
@@ -830,10 +830,22 @@ class AppController:
# --- Shared background pool + proactive warmup (startup_speedup_20260606) --- # --- Shared background pool + proactive warmup (startup_speedup_20260606) ---
self._io_pool = make_io_pool() self._io_pool = make_io_pool()
# Warmup progress is a diagnostic; keep stderr quiet unless explicitly asked.
# Explicit log_to_stderr arg wins; otherwise default to the SLOP_WARMUP_DEBUG env flag.
if log_to_stderr is None:
log_to_stderr = bool(os.environ.get("SLOP_WARMUP_DEBUG"))
self._warmup = WarmupManager(self._io_pool, log_to_stderr=log_to_stderr) self._warmup = WarmupManager(self._io_pool, log_to_stderr=log_to_stderr)
# Hook warmup completion to stamp warmup_done_ts for startup_timeline(). # Hook warmup completion to stamp warmup_done_ts for startup_timeline().
self._warmup.on_complete(self._on_warmup_complete_for_timeline) self._warmup.on_complete(self._on_warmup_complete_for_timeline)
self._warmup.submit(self._compute_warmup_list()) self._warmup_started: bool = False
self._defer_warmup: bool = defer_warmup
self._pending_fetch_provider: Optional[str] = None
# The desktop GUI defers warmup until the first frame is painted (see
# App._gui_func) so the ~2s of heavy SDK C-extension imports don't hold the
# GIL while the window and font atlas are being created — that contention is
# what made the window slow to appear. Headless/web/tests warm immediately.
if not defer_warmup:
self.start_warmup()
# --- Internal State --- # --- Internal State ---
self._ai_status: str = "idle" self._ai_status: str = "idle"
@@ -2193,6 +2205,13 @@ class AppController:
""" """
[C: src/gui_2.py:App.run] [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..." self.ai_status = "fetching models..."
def do_fetch() -> None: def do_fetch() -> None:
@@ -2274,6 +2293,23 @@ class AppController:
""" """
return self._warmup.is_done() return self._warmup.is_done()
def start_warmup(self) -> None:
"""
Submit the heavy-module warmup jobs to the io_pool (idempotent).
Separated from __init__ so the desktop GUI can call it AFTER the first
frame is painted, keeping the ~2s of SDK C-extension imports off the GIL
while the window is being created. Safe to call multiple times.
"""
if self._warmup_started:
return
self._warmup_started = True
self._warmup.submit(self._compute_warmup_list())
# Run any model fetch that was deferred while the window was being created.
if self._pending_fetch_provider is not None:
provider, self._pending_fetch_provider = self._pending_fetch_provider, None
self._fetch_models(provider)
def wait_for_warmup(self, timeout: Optional[float] = None) -> bool: def wait_for_warmup(self, timeout: Optional[float] = None) -> bool:
""" """
@@ -2329,7 +2365,10 @@ class AppController:
def _init_ai_and_hooks(self, app: Any = None) -> None: def _init_ai_and_hooks(self, app: Any = None) -> None:
from src import api_hooks from src import api_hooks
ai_client.set_provider(self._current_provider, self._current_model) # validate=False: skip the live model-list lookup (network/subprocess +
# provider-SDK import) on the main thread during startup. _fetch_models
# corrects the model against the live list after the first frame, off-thread.
ai_client.set_provider(self._current_provider, self._current_model, validate=False)
if self._current_provider == "gemini_cli": if self._current_provider == "gemini_cli":
if not ai_client._gemini_cli_adapter: if not ai_client._gemini_cli_adapter:
ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=self.ui_gemini_cli_path) ai_client._gemini_cli_adapter = ai_client.GeminiCliAdapter(binary_path=self.ui_gemini_cli_path)
-1
View File
@@ -1,7 +1,6 @@
# src/bg_shader.py # src/bg_shader.py
import time import time
import math import math
import numpy as np
from typing import Optional from typing import Optional
from imgui_bundle import imgui, nanovg as nvg, hello_imgui from imgui_bundle import imgui, nanovg as nvg, hello_imgui
+145 -47
View File
@@ -161,6 +161,75 @@ def truncate_entries(entries: list[dict[str, Any]], max_pairs: int) -> list[dict
if count == target: return entries[i:] if count == target: return entries[i:]
return entries return entries
def _detect_refresh_rate_win32() -> float:
"""Return the primary display's current refresh rate in Hz, or 0.0 on failure.
Uses user32.EnumDisplaySettingsW (ENUM_CURRENT_SETTINGS) which reads the value
directly from the display driver in microseconds. The previous implementation
shelled out to PowerShell + WMI (Get-CimInstance Win32_VideoController), which
cost ~350ms on every startup and blocked the first frame.
"""
try:
import ctypes
from ctypes import wintypes
class _DEVMODE(ctypes.Structure):
_fields_ = [
("dmDeviceName", wintypes.WCHAR * 32), ("dmSpecVersion", wintypes.WORD),
("dmDriverVersion", wintypes.WORD), ("dmSize", wintypes.WORD),
("dmDriverExtra", wintypes.WORD), ("dmFields", wintypes.DWORD),
("dmStuff", ctypes.c_byte * 16), ("dmColor", wintypes.SHORT),
("dmDuplex", wintypes.SHORT), ("dmYResolution", wintypes.SHORT),
("dmTTOption", wintypes.SHORT), ("dmCollate", wintypes.SHORT),
("dmFormName", wintypes.WCHAR * 32), ("dmLogPixels", wintypes.WORD),
("dmBitsPerPel", wintypes.DWORD), ("dmPelsWidth", wintypes.DWORD),
("dmPelsHeight", wintypes.DWORD), ("dmDisplayFlags", wintypes.DWORD),
("dmDisplayFrequency", wintypes.DWORD), ("dmICMMethod", wintypes.DWORD),
("dmICMIntent", wintypes.DWORD), ("dmMediaType", wintypes.DWORD),
("dmDitherType", wintypes.DWORD), ("dmReserved1", wintypes.DWORD),
("dmReserved2", wintypes.DWORD), ("dmPanningWidth", wintypes.DWORD),
("dmPanningHeight", wintypes.DWORD),
]
dm = _DEVMODE()
dm.dmSize = ctypes.sizeof(_DEVMODE)
if ctypes.windll.user32.EnumDisplaySettingsW(None, -1, ctypes.byref(dm)):
# dmDisplayFrequency is 0 or 1 for "default/hardware" on some drivers.
if dm.dmDisplayFrequency > 1:
return float(dm.dmDisplayFrequency)
except Exception:
pass
return 0.0
def _resolve_font_path(font_path: str, assets_dir: Path) -> str:
"""Normalize a configured font path to something hello_imgui can load.
hello_imgui resolves relative paths against the assets folder. A config may
carry a stale ABSOLUTE path from a different project checkout (e.g.
C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf after the repo
moved to C:/projects/sloppy). In that case the absolute file does not exist
and the load fails. This recovers by:
1. If the absolute path lives under the current assets folder -> relativize.
2. If the absolute path exists on disk as-is -> keep it.
3. Otherwise recover the basename under assets/fonts or assets.
4. Final fallback: the bundled default Inter font.
"""
p = Path(font_path)
if not p.is_absolute():
return font_path # already relative; hello_imgui searches the assets folder
try:
if p.is_relative_to(assets_dir):
return str(p.relative_to(assets_dir)).replace("\\", "/")
except (ValueError, AttributeError):
pass
if p.exists():
return font_path # absolute path to a real file elsewhere — load directly
# Stale absolute path: recover by basename relative to the assets folder.
name = p.name
for rel in (f"fonts/{name}", name):
if (assets_dir / rel).exists():
return rel
return "fonts/Inter-Regular.ttf"
class App: class App:
"""The main ImGui interface orchestrator for Manual Slop.""" """The main ImGui interface orchestrator for Manual Slop."""
@@ -172,7 +241,7 @@ class App:
# --- Core Dependencies & State --- # --- Core Dependencies & State ---
from src.startup_profiler import startup_profiler from src.startup_profiler import startup_profiler
with startup_profiler.phase("app_init_AppController"): with startup_profiler.phase("app_init_AppController"):
self.controller = app_controller.AppController() self.controller = app_controller.AppController(defer_warmup=True)
self.controller._app = self self.controller._app = self
with startup_profiler.phase("app_init_history_perfmon"): with startup_profiler.phase("app_init_history_perfmon"):
from src import history, performance_monitor from src import history, performance_monitor
@@ -187,7 +256,8 @@ class App:
# --- Command Palette --- # --- Command Palette ---
self.show_command_palette: bool = False self.show_command_palette: bool = False
# --- Initialization --- # --- Initialization ---
self.controller.init_state() with startup_profiler.phase("app_init_state"):
self.controller.init_state()
from src.hot_reloader import HotReloader, HotModule from src.hot_reloader import HotReloader, HotModule
if 'src.gui_2' not in HotReloader.HOT_MODULES: if 'src.gui_2' not in HotReloader.HOT_MODULES:
HotReloader.register(HotModule( HotReloader.register(HotModule(
@@ -196,11 +266,13 @@ class App:
state_keys=['active_discussion', 'show_windows', 'ui_file_paths', 'ui_screenshot_paths', 'disc_entries', 'disc_roles'], state_keys=['active_discussion', 'show_windows', 'ui_file_paths', 'ui_screenshot_paths', 'disc_entries', 'disc_roles'],
delegation_targets=['_render_main_interface', '_render_discussion_hub', '_render_files_and_media', '_render_ai_settings_hub', '_render_operations_hub', '_render_mma_dashboard'] delegation_targets=['_render_main_interface', '_render_discussion_hub', '_render_files_and_media', '_render_ai_settings_hub', '_render_operations_hub', '_render_mma_dashboard']
)) ))
self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.controller.active_project_root) with startup_profiler.phase("app_init_workspace"):
self.disc_entries = self.controller.disc_entries self.workspace_manager = workspace_manager.WorkspaceManager(project_root=self.controller.active_project_root)
self.disc_roles = self.controller.disc_roles self.disc_entries = self.controller.disc_entries
self.workspace_profiles = self.workspace_manager.load_all_profiles() self.disc_roles = self.controller.disc_roles
self.controller.start_services(self) self.workspace_profiles = self.workspace_manager.load_all_profiles()
with startup_profiler.phase("app_init_start_services"):
self.controller.start_services(self)
# --- Controller Callbacks & Actions --- # --- Controller Callbacks & Actions ---
self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset
self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset self.controller._predefined_callbacks['load_context_preset'] = self.load_context_preset
@@ -457,16 +529,13 @@ class App:
user_scale = theme.get_current_scale() user_scale = theme.get_current_scale()
self.runner_params.dpi_aware_params.dpi_window_size_factor = user_scale self.runner_params.dpi_aware_params.dpi_window_size_factor = user_scale
# Detect Monitor Refresh Rate for capping (Win32 only) # Detect Monitor Refresh Rate for capping (Win32 only).
# Uses the native EnumDisplaySettings call (~0.3ms) instead of spawning a
# PowerShell/WMI subprocess (~350ms) so the first frame is not blocked.
fps_cap = 60.0 fps_cap = 60.0
if sys.platform == "win32": if sys.platform == "win32":
try: rate = _detect_refresh_rate_win32()
# Use PowerShell to get max refresh rate across all controllers if rate: fps_cap = rate
cmd = "powershell -NoProfile -Command \"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty CurrentRefreshRate\""
out = subprocess.check_output(cmd, shell=True).decode().splitlines()
rates = [float(r.strip()) for r in out if r.strip().isdigit()]
if rates: fps_cap = max(rates)
except Exception: pass
# Enable idling with monitor refresh rate to effectively cap FPS # Enable idling with monitor refresh rate to effectively cap FPS
self.runner_params.fps_idling.enable_idling = True self.runner_params.fps_idling.enable_idling = True
@@ -476,11 +545,17 @@ class App:
self.runner_params.imgui_window_params.show_menu_view_themes = True self.runner_params.imgui_window_params.show_menu_view_themes = True
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
self.runner_params.ini_filename = "manualslop_layout.ini" self.runner_params.ini_filename = "manualslop_layout.ini"
def _profiled_setup_style() -> None:
with startup_profiler.phase("setup_imgui_style"):
theme.apply_current()
def _profiled_post_init() -> None:
with startup_profiler.phase("post_init"):
self._post_init()
self.runner_params.callbacks.show_gui = self._gui_func self.runner_params.callbacks.show_gui = self._gui_func
self.runner_params.callbacks.show_menus = self._show_menus self.runner_params.callbacks.show_menus = self._show_menus
self.runner_params.callbacks.load_additional_fonts = self._load_fonts self.runner_params.callbacks.load_additional_fonts = self._load_fonts
self.runner_params.callbacks.setup_imgui_style = theme.apply_current self.runner_params.callbacks.setup_imgui_style = _profiled_setup_style
self.runner_params.callbacks.post_init = self._post_init self.runner_params.callbacks.post_init = _profiled_post_init
self._fetch_models(self.current_provider) self._fetch_models(self.current_provider)
md_options = markdown_helper.get_renderer().options md_options = markdown_helper.get_renderer().options
immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options)) immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options))
@@ -489,42 +564,39 @@ class App:
session_logger.close_session() session_logger.close_session()
def _load_fonts(self) -> None: def _load_fonts(self) -> None:
# Set hello_imgui assets folder to the actual absolute path from src.startup_profiler import startup_profiler
assets_dir = Path(__file__).parent.parent / "assets" with startup_profiler.phase("load_fonts"):
if assets_dir.exists(): # Set hello_imgui assets folder to the actual absolute path
hello_imgui.set_assets_folder(str(assets_dir.absolute())) assets_dir = Path(__file__).parent.parent / "assets"
if assets_dir.exists():
hello_imgui.set_assets_folder(str(assets_dir.absolute()))
# Improved font rendering with oversampling # Improved font rendering with oversampling
config = imgui.ImFontConfig() config = imgui.ImFontConfig()
config.oversample_h = 3 config.oversample_h = 3
config.oversample_v = 3 config.oversample_v = 3
font_path, font_size = theme.get_font_loading_params() font_path, font_size = theme.get_font_loading_params()
if font_path: if font_path:
p = Path(font_path) font_path = _resolve_font_path(font_path, assets_dir)
if p.is_absolute(): # Just try loading it directly; hello_imgui will look in the assets folder
try: try:
if p.is_relative_to(assets_dir): with startup_profiler.phase("load_fonts.main_with_fontawesome"):
font_path = str(p.relative_to(assets_dir)).replace("\\", "/") self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config)
except (ValueError, AttributeError): except Exception as e:
pass # Fallback to original font_path if relative_to fails or on old Python print(f"Failed to load main font {font_path}: {e}")
self.main_font = None
# Just try loading it directly; hello_imgui will look in the assets folder else:
try:
self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config)
except Exception as e:
print(f"Failed to load main font {font_path}: {e}")
self.main_font = None self.main_font = None
else:
self.main_font = None
try: try:
params = hello_imgui.FontLoadingParams(font_config=config) with startup_profiler.phase("load_fonts.mono"):
self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params) params = hello_imgui.FontLoadingParams(font_config=config)
except Exception as e: self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params)
print(f"Failed to load mono font: {e}") except Exception as e:
self.mono_font = None print(f"Failed to load mono font: {e}")
self.mono_font = None
def _handle_approve_mma_step(self, user_data=None) -> None: def _handle_approve_mma_step(self, user_data=None) -> None:
"""UI-level wrapper for approving a pending MMA step.""" """UI-level wrapper for approving a pending MMA step."""
@@ -817,6 +889,32 @@ class App:
# ---------------------------------------------------------------- gui # ---------------------------------------------------------------- gui
def _gui_func(self) -> None: def _gui_func(self) -> None:
# One-shot: log when immapp first hands control to our render callback. The
# span init -> here is window/GL/context creation + the font/style/post_init
# callbacks (all opaque C++); the span here -> mark_first_frame_rendered is
# the cost of the first full-UI render. Splitting them attributes the gap.
if not getattr(self, "_gui_func_entered", False):
self._gui_func_entered = True
try:
init_ts = getattr(self.controller, "_init_start_ts", None)
if init_ts is not None:
sys.stderr.write(f"[startup] first _gui_func entry at {(time.time() - init_ts) * 1000:.1f}ms after init (window/GL + font/style/post_init callbacks done)\n")
sys.stderr.flush()
except Exception: pass
# One-shot: kick off the controller's heavy-module warmup on the shared
# io_pool once the FIRST frame has actually been painted. Waiting one frame
# keeps the ~2s of SDK C-extension imports from holding the GIL during
# window creation and font-atlas building, so the window appears at full
# speed; the SDKs are then warmed by the time the user sends their first
# message. start_warmup() is idempotent.
if not getattr(self, "_preload_started", False):
if getattr(self, "_first_frame_painted", False):
self.controller.start_warmup()
self._preload_started = True
else:
self._first_frame_painted = True
io = imgui.get_io() io = imgui.get_io()
if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.Key.r): if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.Key.r):
self._trigger_hot_reload() self._trigger_hot_reload()