Gitea (and any case-sensitive filesystem) was rendering the [Top]
nav links in /docs as broken because of two bugs:
1. Case-sensitivity: 22 links used '../README.md' (all-uppercase)
but the actual file is 'docs/Readme.md' (capital R, lowercase
rest). 21 guide_*.md nav bars were affected, plus 1 internal
cross-link in Readme.md itself. Works on Windows (case-
insensitive) but broken on Linux/Gitea.
Fix: 22 occurrences across 22 files changed
'../README.md' -> '../Readme.md'
2. Wrong relative-path level: 16 links used '../../conductor/...'
from 'docs/guide_*.md' to reach 'conductor/'. This goes up 2
levels to 'projects/', which doesn't exist. The correct path
from 'docs/guide_*.md' to 'conductor/' is 1 level up
('../conductor/...'). 12 unique patterns across 10 files
affected.
Fix: 16 occurrences across 10 files changed
'../../conductor/' -> '../conductor/'
3. Bonus: 1 planned-guide link in guide_context_curation.md
referenced a never-written 'guide_context_presets.md'. The
ContextPreset schema is now fully covered in the new
'guide_context_aggregation.md' (per the 2026-06-08 docs
refresh). Fix: link target updated.
No content was changed, only link paths. 24 files, 37 link
replacements, 37 deletions.
Verification:
- All .md links in docs/ now resolve to existing files
(validated by path-resolution check from each file's directory)
- The 3 new guides from the previous docs refresh commit
(guide_discussions.md, guide_state_lifecycle.md,
guide_context_aggregation.md) had the case bug inherited from
guide_architecture.md's existing nav pattern; their top-of-file
nav bars are now correct
- The 21 pre-existing guide nav bars that had the same bug
(all 21 of them, except the 3 that used the correct case:
guide_mma.md, guide_simulations.md, guide_tools.md) are now
also fixed
- Inter-guide links (e.g. [Discussions](guide_discussions.md))
were not affected; they were always correct because both the
link text and the actual filename are lowercase
This is a docs-only fix. No code modified.
18 KiB
src/app_controller.py — Headless Orchestrator & State Hub
Top | Architecture | Discussions | State Lifecycle | Context Aggregation | MMA | Testing
Overview
src/app_controller.py (~166KB) is the headless controller that owns the application's state and business logic. It decouples the GUI (gui_2.py) from the underlying subsystems (AI, presets, personas, RAG, history, MMA, paths, hot reload).
When --enable-test-hooks is passed, the controller also spins up the HookServer so external tests/scripts can drive the running app.
Architecture
┌─────────────────────────────────────────────────┐
│ gui_2.py (App) │
│ - Pure immediate-mode UI │
│ - Reads app_state for rendering │
│ - Calls controller methods for mutations │
└─────────────────┬───────────────────────────────┘
│ delegates to
▼
┌─────────────────────────────────────────────────┐
│ app_controller.py: AppController │
│ - State container (AppState) │
│ - Subsystem coordination (presets, personas, ...) │
│ - Headless mode: skips GUI init, starts hook │
│ server on port 8999 │
│ - Provides _predefined_callbacks and │
│ _gettable_fields for the Hook API │
└─────────────────┬───────────────────────────────┘
│ owns/uses
▼
┌─────────────────────────────────────────────────┐
│ Subsystems │
│ - PresetManager (src/presets.py) │
│ - PersonaManager (src/personas.py) │
│ - ContextPresetManager (src/context_presets.py) │
│ - ToolPresetManager (src/tool_presets.py) │
│ - ToolBiasEngine (src/tool_bias.py) │
│ - RAGEngine (src/rag_engine.py) │
│ - HistoryManager (src/history.py) │
│ - WorkspaceManager (src/workspace_manager.py) │
│ - HookServer (src/api_hooks.py) │
│ - HotReloader (src/hot_reload.py) │
│ - PathManager (src/paths.py) │
└─────────────────────────────────────────────────┘
The AppController Class
__init__(self, enable_test_hooks: bool = False)
Initializes the controller. Key state:
class AppController:
def __init__(self, enable_test_hooks: bool = False):
# 1. Path resolution (src/paths.py)
self.paths = PathManager()
# 2. State container
self.app_state = AppState()
# 3. Subsystem managers
self.presets = PresetManager(self.paths)
self.personas = PersonaManager(self.paths)
self.context_presets = ContextPresetManager(self.paths)
self.tool_presets = ToolPresetManager(self.paths)
self.tool_bias = ToolBiasEngine()
self.history = HistoryManager(self.paths)
self.workspace = WorkspaceManager(self.paths)
self.rag_engine = RAGEngine(self.paths) # Lazy
# 4. Hook API surface
self._predefined_callbacks: dict[str, Callable] = {}
self._gettable_fields: dict[str, str] = {}
# 5. AI client (lazy)
self.ai_client = None
# 6. MMA conductor (lazy)
self.mma_conductor = None
# 7. Sync event queue (daemon <-> UI bridge)
self.event_queue = SyncEventQueue()
# 8. Optional hook server
if enable_test_hooks:
self.hook_server = HookServer()
self.hook_server.start()
The App (in gui_2.py) then reads controller.app_state, controller.presets, etc. for rendering.
register_hooks(app: App)
Called by gui_2.py after instantiation. The controller populates the predefined callbacks and gettable fields that the Hook API can invoke.
def register_hooks(self, app: 'App') -> None:
"""Register App methods as predefined callbacks for the Hook API."""
self._predefined_callbacks['_toggle_command_palette'] = app._toggle_command_palette
self._predefined_callbacks['_open_command_palette'] = app._open_command_palette
# ... etc, many more ...
self._gettable_fields['show_command_palette'] = 'show_command_palette'
self._gettable_fields['current_provider'] = 'current_provider'
# ... etc ...
This is the only bridge between the GUI's app methods and the external Hook API. If a method is not in _predefined_callbacks, external callers cannot invoke it.
Subsystem Coordination Methods
The controller has methods that span multiple subsystems:
reload_presets()— re-reads preset TOML files from diskreload_personas()— same for personasreload_context_presets()— validates files existapply_persona(persona_name, target)— switches model, system prompt, and tool weights for a targetdispatch_mma_track(track_id)— kicks off a multi-agent trackreset_session()— clears discussion history, resets UI state, etc.save_state_to_disk()/load_state_from_disk()— for historical session replayinject_context_files(paths)— adds files to the active context composition
The AppState Dataclass
app_state is a flat dataclass holding all GUI-visible state. Examples:
@dataclass
class AppState:
current_provider: str = "gemini"
current_model: str = "gemini-3-flash-preview"
temperature: float = 0.7
top_p: float = 0.95
max_output_tokens: int = 8192
system_prompt: str = ""
discussion_history: list[DiscussionEntry] = field(default_factory=list)
context_files: list[ContextFileEntry] = field(default_factory=list)
context_screenshots: list[str] = field(default_factory=list)
show_command_palette: bool = False
show_preset_manager: bool = False
show_persona_editor: bool = False
show_context_preview: bool = False
show_diagnostics: bool = False
show_mma_dashboard: bool = False
# ... many more
The App reads from app_state for rendering and writes back via setter methods. All setters are exposed to the Hook API via _gettable_fields and other "settable" registries.
Preset & Persona Management
PresetManager (in src/presets.py)
The controller delegates preset CRUD to PresetManager. The controller itself only coordinates when presets change (re-apply to active session, update system prompt, etc.).
# In controller
def on_preset_changed(self, new_preset_name: str) -> None:
preset = self.presets.get(new_preset_name)
self.app_state.system_prompt = preset.full_text # Base + persona
self.app_state.temperature = preset.temperature
self.app_state.top_p = preset.top_p
self.app_state.max_output_tokens = preset.max_output_tokens
PersonaManager (in src/personas.py)
Consolidates model settings + system prompt + tool weights into a single named entity.
# In controller
def apply_persona(self, persona_name: str, target: str = "tier3") -> None:
persona = self.personas.get(persona_name)
if persona.model:
self.app_state.current_model = persona.model
if persona.system_prompt:
self.app_state.system_prompt = persona.system_prompt
if persona.tool_weights:
self.tool_bias.apply_weights(persona.tool_weights, target=target)
if persona.bias_profile:
self.tool_bias.apply_profile(persona.bias_profile)
target is the MMA tier ("tier1", "tier2", "tier3", "tier4"). This is how MMA agents get isolated cognitive load.
ContextPresetManager (in src/context_presets.py)
Saves/loads complete context compositions (files, screenshots, view modes). Validates that referenced files still exist on load.
ToolPresetManager (in src/tool_presets.py)
Manages tool enable/disable + weights. Persisted to tool_presets.toml.
ToolBiasEngine (in src/tool_bias.py)
Applies weights and global bias profiles. Generates the "Tooling Strategy" section appended to system prompts.
History Management
HistoryManager (in src/history.py) implements the non-provider undo/redo system.
def on_ui_state_change(self) -> None:
"""Called when the UI changes (e.g., text input). Pushes a snapshot."""
snapshot = self.history.capture(self.app_state)
self.history.push(snapshot)
self.app_state.can_undo = self.history.can_undo()
self.app_state.can_redo = self.history.can_redo()
Snapshots include:
- All text inputs (system prompt, AI input, code blocks)
- Model parameters (Temperature, Top-P, Max Output Tokens)
- Context (files, screenshots)
- Discussion history (for discussion mutations)
Capacity is fixed (default: 50 snapshots). Older entries are evicted.
Branching History ("Takes")
HistoryManager also tracks timeline branching. When the user reverts and then takes a new action, a new "take" is created. The full history graph is preserved for back-navigation.
RAG Engine Integration
RAGEngine (in src/rag_engine.py) is owned by the controller but lazy-loaded on first use:
def get_rag_engine(self) -> RAGEngine:
if self._rag_engine is None:
from src.rag_engine import RAGEngine
self._rag_engine = RAGEngine(self.paths)
return self._rag_engine
The GUI exposes a RAG settings panel that calls controller.get_rag_engine().set_provider(...), set_chunk_size(...), etc.
RAG Lifecycle
- Indexing:
controller.index_project()walks the project workspace, chunks files, embeds them, writes to ChromaDB (or external MCP). - Search:
controller.search_context_files(query)returns top-k fragments with source paths. - Injection: Fragments are prepended to the AI's prompt via
ai_client.send(...). The controller orchestrates the flow.
MMA Conductor Integration
MultiAgentConductor (in src/multi_agent_conductor.py) is also lazy-loaded:
def get_mma_conductor(self) -> 'MultiAgentConductor':
if self._mma_conductor is None:
from src.multi_agent_conductor import MultiAgentConductor
self._mma_conductor = MultiAgentConductor(self)
return self._mma_conductor
The controller passes itself into the conductor so workers can access presets/personas/RAG during execution.
Dispatch Flow
controller.dispatch_mma_track(track_id)
-> conductor.load_track(track_id)
-> conductor.start_workers(track)
-> workers run in parallel via WorkerPool
-> workers call back into controller (presets, personas, etc.)
-> results pushed to controller.app_state.discussion_history
-> conductor emits events to controller.event_queue
The event_queue is consumed by the GUI on the main thread to update display.
Hot Reload
The controller can hot-reload Python modules while preserving state. This is critical for GUI iteration:
def hot_reload(self, module_name: str) -> None:
"""Reload a module and re-apply its render functions to the app."""
from src.hot_reload import HotReloader
reloader = HotReloader(self.app)
reloader.reload(module_name)
gui_2.py registers all its render functions with the reloader at startup. On reload, the reloader swaps the function references without losing app state.
See docs/guide_hot_reload.md for the full mechanism.
The SyncEventQueue
A queue.Queue-based bridge between the daemon threads (AI workers, MMA workers) and the GUI main thread.
class SyncEventQueue:
def put(self, event: Event) -> None: ...
def get_nowait(self) -> Event | None: ...
def get_all(self) -> list[Event]: ...
The GUI polls controller.event_queue.get_all() once per frame and dispatches events to render functions.
Event Types
MMA_TICKET_COMPLETEDMMA_LOG_MESSAGEAI_RESPONSE_CHUNKAI_RESPONSE_COMPLETETOOL_CALL_STARTEDTOOL_CALL_COMPLETEDPERSONA_APPLIEDWORKSPACE_LOADEDRAG_INDEX_COMPLETEHOOK_CALLBACK_RECEIVED
The controller translates subsystem-specific events into these generic types.
The Headless Mode
When sloppy.py is launched with --headless, the controller is instantiated without an App:
# In sloppy.py --headless
controller = AppController(enable_test_hooks=True)
# ... run server-only logic ...
# No GUI, no ImGui context, but full subsystem access
This is the Headless Backend Service mode. The controller still listens on :8999 and serves all Hook API endpoints. Tests and external scripts can drive the headless service.
Hook API Surface (Defined Here)
The controller is the single source of truth for what the Hook API can do. Three registries:
_predefined_callbacks: dict[str, Callable]
Maps hook name → App method. Populated by register_hooks(app).
self._predefined_callbacks['_toggle_command_palette'] = app._toggle_command_palette
self._predefined_callbacks['_open_command_palette'] = app._open_command_palette
self._predefined_callbacks['save_context_preset'] = app.save_context_preset
self._predefined_callbacks['load_context_preset'] = app.load_context_preset
# ... ~30 entries
_gettable_fields: dict[str, str]
Maps hook name → AppState field name. Used by get_value Hook API action.
self._gettable_fields['show_command_palette'] = 'show_command_palette'
self._gettable_fields['current_provider'] = 'current_provider'
self._gettable_fields['current_model'] = 'current_model'
# ... etc
_action_handlers: dict[str, Callable]
Maps action name (e.g., "click", "set_value", "custom_callback") → handler.
self._action_handlers['click'] = self._handle_click
self._action_handlers['set_value'] = self._handle_set_value
self._action_handlers['custom_callback'] = self._handle_custom_callback
# ... etc
The HookServer in src/api_hooks.py consumes these registries to route incoming requests.
Testing
The controller is tested via the live_gui fixture (full integration) and targeted unit tests.
Unit Tests
tests/test_app_controller_init.py— instantiation, subsystem wiringtests/test_preset_manager.py— preset CRUDtests/test_persona_manager.py— persona CRUD + applicationtests/test_context_presets.py— context preset CRUD + file validationtests/test_history.py— undo/redotests/test_workspace_manager.py— workspace profile CRUD
Integration Tests (live_gui)
Use the ApiHookClient to drive the controller and verify state mutations.
def test_apply_persona(live_gui):
client = ApiHookClient()
client.push_event("custom_callback", {"callback": "apply_persona", "args": ["code-reviewer"]})
time.sleep(0.5)
model = client.get_value("current_model")
assert "code" in model.lower() or model == "code-reviewer-model"
Common Pitfalls
- Don't instantiate
AppControllertwice in the same process: The singleton holds RAG engine, MMA conductor, hook server. A second instance would conflict. - Don't read
app_statefrom a daemon thread without locking: Useevent_queuefor cross-thread communication. - Always go through the controller for subsystem changes: Don't call
self.presets.save(...)from the GUI directly; callcontroller.save_preset(...)so the event is broadcast. - When adding a new Hook API callback, register it in BOTH
_predefined_callbacksANDregister_hooks: The latter is what populates the registry from an App instance.
See Also
- guide_architecture.md — Threading and event flow
- guide_mma.md — How MMA workers use the controller
- guide_ai_client.md — How
ai_clientintegrates - guide_api_hooks.md — The Hook API the controller exposes
- guide_hot_reload.md — How the controller supports state-preserving reloads
- guide_discussions.md — The Discussion system (Takes, branching,
_switch_discussion,_branch_discussion,_rename_discussion,_delete_discussion,_flush_disc_entries_to_project) - guide_state_lifecycle.md — The
_handle_reset_sessionand_handle_compress_discussionflows, theApp.__getattr__/__setattr__state delegation pattern, and theHistoryManagerintegration - guide_context_aggregation.md — The
aggregate.pypipeline that the controller calls per send (per-provider + Tier 3 worker) src/presets.py,src/personas.py,src/context_presets.py,src/tool_presets.py,src/tool_bias.py— Subsystem managerssrc/history.py—HistoryManagersrc/rag_engine.py—RAGEnginesrc/multi_agent_conductor.py—MultiAgentConductorsrc/hot_reload.py—HotReloadersrc/api_hooks.py—HookServer(uses the controller's registries)src/paths.py—PathManager- conductor/tracks/nagent_review_20260608/report.md — Deep-dive analysis of the controller's per-provider history globals and other state patterns