Private
Public Access
0
0

docs(workspace-profiles): new guide covering profile schema, manager, scope inheritance, and auto-switch

This commit is contained in:
2026-06-02 19:55:08 -04:00
parent 94a8d06724
commit 5379312bc7
+320
View File
@@ -0,0 +1,320 @@
# Workspace Profiles (Docking Layouts)
[Top](../README.md) | [Architecture](guide_architecture.md) | [MMA](guide_mma.md) | [Simulations](guide_simulations.md)
---
## Overview
Workspace Profiles are named, persistent snapshots of the ImGui docking layout, window visibility, and panel state. Users can save their current layout as a profile, switch between profiles on demand, and have profiles auto-apply based on context (active project, active MMA tier, or current task).
This is essential for multi-monitor workflows and for users who frequently switch between "focused work" (one or two panels visible) and "broad context" (all panels visible across multiple monitors).
This guide covers:
1. **Architecture** — Where WorkspaceProfile fits in the rendering system
2. **Data Model** — The `WorkspaceProfile` schema
3. **WorkspaceManager** — CRUD and activation
4. **Scope Inheritance** — Global vs Project profiles
5. **Contextual Auto-Switch** — Experimental feature for binding profiles to context
6. **Testing** — Test patterns
---
## Architecture
Workspace Profiles live at the boundary between the ImGui render loop and the persistent configuration system. The render loop reads the active profile; the WorkspaceManager updates the active profile and persists changes.
```
┌────────────────────────────────────────────────┐
│ ImGui Render Loop │
│ - Applies docking layout from active profile │
│ - Reads window visibility / position from │
│ active profile at startup │
│ - Updates active profile when user resizes │
│ / moves / shows / hides windows │
└─────────────────┬──────────────────────────────┘
│ reads / writes
┌────────────────────────────────────────────────┐
│ WorkspaceManager (src/workspace_manager.py) │
│ - Profiles dict (active + saved) │
│ - save_profile(name, scope) │
│ - load_profile(name) │
│ - delete_profile(name, scope) │
│ - bind_to_context(context_id, profile_name) │
│ - auto_switch(context_id) │
└─────────────────┬──────────────────────────────┘
│ persists to
┌────────────────────────────────────────────────┐
│ TOML Files │
│ - Global: <user_config>/workspace_profiles.toml│
│ - Project: <project_root>/workspace_profiles.toml│
└────────────────────────────────────────────────┘
```
**Lifecycle**:
- On application startup, the last-active profile is loaded (per-project or global, depending on the loaded project).
- On user action (resize, move, show, hide), the active profile is updated in memory.
- On explicit "Save Profile" or application exit, the active profile is persisted to disk.
- Profile activation (manual or auto-switch) triggers a layout re-apply.
---
## Data Model
### `WorkspaceProfile` (`src/models.py`)
A snapshot of window state at a moment in time.
```python
@dataclass
class WorkspaceProfile:
name: str
# Window state (ImGui docking layout)
docking_layout: bytes # Serialized ImGui docking configuration
# Window visibility per window name
window_visibility: Dict[str, bool] # {"mma_dashboard": True, "context_panel": True, ...}
# Active theme
theme: str = "dark" # "dark" | "light" | "nerv" | ...
# Active theme FX state
theme_fx_enabled: bool = True
# Capture metadata
captured_at: str = "" # ISO 8601 timestamp
description: str = "" # Optional human description
```
| Field | Type | Purpose |
|---|---|---|
| `name` | `str` | Unique identifier within the scope. |
| `docking_layout` | `bytes` | Serialized ImGui docking configuration. Stored as a base64-encoded byte string in TOML. |
| `window_visibility` | `Dict[str, bool]` | Maps window name to visibility. Windows not in the dict use the default (visible). |
| `theme` | `str` | Active theme name. See [guide_nerv_theme.md](guide_nerv_theme.md). |
| `theme_fx_enabled` | `bool` | Whether theme FX (e.g., NERV scanlines) are enabled. |
| `captured_at` | `str` | ISO 8601 timestamp of when the profile was captured. |
| `description` | `str` | Optional human-readable description. |
### Serialization
The `docking_layout` is captured via ImGui's `SaveIniSettingsToMemory()` and serialized as bytes. On load, `LoadIniSettingsFromMemory()` restores the layout.
The TOML representation uses base64 for the binary layout:
```toml
[profiles.focused_work]
docking_layout = "BASE64_ENCODED_BYTES"
window_visibility = { mma_dashboard = false, context_panel = true, ai_settings = false }
theme = "nerv"
theme_fx_enabled = true
captured_at = "2026-05-15T14:32:05"
description = "Minimal layout for focused code writing"
```
---
## WorkspaceManager
```python
from src.workspace_manager import WorkspaceManager
manager = WorkspaceManager(project_root=Path("/path/to/project"))
```
### Methods
```python
def load_all_profiles(self) -> Dict[str, WorkspaceProfile]:
"""Merges global and project profiles into a single dict."""
def save_profile(self, profile: WorkspaceProfile, scope: str = "project") -> None:
"""Saves a profile to the specified scope (global or project)."""
def delete_profile(self, name: str, scope: str = "project") -> None:
"""Removes a profile from the specified scope."""
def activate_profile(self, name: str) -> None:
"""Applies the profile's layout, visibility, and theme to the live GUI."""
def capture_current_as(self, name: str, scope: str = "project") -> WorkspaceProfile:
"""Captures the current GUI state as a new profile."""
def bind_to_context(self, context_id: str, profile_name: str) -> None:
"""Binds a profile to a context (active tier, project, etc.) for auto-switching."""
def auto_switch(self, context_id: str) -> bool:
"""If a profile is bound to this context, activates it. Returns True if switched."""
```
### Profile Activation
`activate_profile(name)` performs:
1. Read the profile from the loaded dict.
2. Apply `docking_layout` via `ImGui.LoadIniSettingsFromMemory()`.
3. Apply `window_visibility` to the live windows (hide/show as needed).
4. Apply the theme via the theme system.
5. Set the theme FX state.
Activation is immediate; the next frame renders the new layout.
### Capturing Current State
`capture_current_as(name, scope)`:
1. Read the current ImGui state via `ImGui.SaveIniSettingsToMemory()`.
2. Snapshot window visibility.
3. Read the active theme from the theme system.
4. Set the timestamp and optional description.
5. Save to the specified scope.
This is the "Save Profile" action in the GUI.
---
## Scope Inheritance
| Scope | Path |
|---|---|
| Global | `<user_config>/workspace_profiles.toml` |
| Project | `<project_root>/workspace_profiles.toml` |
**Merge rule**: `load_all_profiles()` returns global first, then project entries **override** globals of the same name. This is the same pattern as personas and presets.
**Example**:
- Global has `focused_work` with theme `"dark"`.
- Project has `focused_work` with theme `"nerv"`.
- The project version wins for this project. Other projects without the override use the global version.
**When to use which**:
- **Global**: Layouts you want across all projects (e.g., "minimal layout for reading", "presentation layout").
- **Project**: Layouts specific to a project's workflow (e.g., "Python project layout with discussion at bottom").
---
## Contextual Auto-Switch (Experimental)
The `bind_to_context(context_id, profile_name)` and `auto_switch(context_id)` methods enable automatic profile switching based on context.
### Context IDs
| Context | Format | Example |
|---|---|---|
| MMA Tier | `tier:<tier_name>` | `tier:tier3-worker` |
| Project | `project:<project_name>` | `project:my_app` |
| User-defined | `<custom>` | `code_review`, `documentation` |
### Activation
`auto_switch(context_id)` is called on:
- MMA tier transitions (e.g., when moving from Tier 1 to Tier 2)
- Project load/unload
- User-defined triggers (via the GUI's "Apply Profile to Context" action)
If a profile is bound to the context, it's activated. Otherwise, the current profile remains.
### Configuration
```toml
[workspace]
auto_switch_profiles = false # Master toggle; default off
```
When `auto_switch_profiles = false` (the default), `auto_switch` is a no-op. Set to `true` to enable.
### Current Status (as of 2026-06-02)
The `bind_to_context` and `auto_switch` methods exist on `WorkspaceManager`. The MMA's `ConductorEngine` does **not** yet call `auto_switch` on tier transitions. This is planned integration work (see [guide_mma.md#workspace-profile-auto-switching-roadmap](guide_mma.md#workspace-profile-auto-switching-roadmap)).
---
## GUI Integration
### Profile Selector
A dropdown in the View menu lists all available profiles. Selecting one activates it.
### Save Profile
A "Save Current as Profile..." action in the View menu prompts for a name and scope (Global / Project), then calls `capture_current_as()`.
### Delete Profile
A "Delete Profile" action removes the active profile (with confirmation).
### Bind to Context (Experimental)
A "Bind Profile to Context..." action lets the user specify a context ID and bind the current profile. This is gated behind the `auto_switch_profiles` config flag.
---
## Configuration
```toml
[workspace]
auto_switch_profiles = false
last_active_profile = "default" # Set automatically on activation
```
Per-project overrides go in `manual_slop.toml` under `[workspace]`.
---
## Testing
### Unit Tests
- `tests/test_workspace_manager.py` — CRUD, scope merging, profile serialization
- `tests/test_workspace_profiles_sim.py` — End-to-end via `live_gui`: save, switch, verify layout
### Test Pattern
```python
def test_profile_save_and_load(tmp_path, live_gui):
# Set up a project
# ... (live_gui fixture handles this)
# Capture the current state as a profile
client.capture_current_as("test_profile", scope="project")
# Modify the layout (hide a panel)
client.hide_panel("mma_dashboard")
# Activate the saved profile
client.activate_profile("test_profile")
# Verify the panel is visible again
state = client.get_window_state("mma_dashboard")
assert state["visible"] == True
```
### Layout Stability
The `docking_layout` is a binary blob (base64 in TOML). It's not human-readable, so tests typically use a "save, modify, restore" pattern rather than asserting specific layout details.
---
## Limitations
1. **Theme FX State is Global**: NERV FX and other theme effects are global settings, not per-profile. Saving a profile with `theme_fx_enabled = false` and then activating it would change the global setting.
2. **No Profile Variants per Monitor**: A single profile applies to all connected monitors. Multi-monitor users with different layouts per monitor must use ImGui's native multi-viewport support, not profiles.
3. **No Profile Composition**: A profile cannot inherit from another. Duplication is the only way to share settings.
4. **ImGui-Specific**: Profiles are tightly coupled to ImGui's docking format. Migrating to a different GUI framework would require a profile format migration.
5. **No Undo for Profile Activation**: Activating a profile changes the layout immediately. If the user doesn't like it, they must switch to another profile manually.
6. **Contextual Auto-Switch Is Off by Default**: Requires explicit opt-in via `auto_switch_profiles = true`. Even when on, the MMA integration is not yet implemented.
---
## Future Work
- **MMA Integration** — `ConductorEngine` calls `auto_switch` on tier transitions. See [guide_mma.md#workspace-profile-auto-switching-roadmap](guide_mma.md#workspace-profile-auto-switching-roadmap).
- **Per-Monitor Layouts** — Capture and restore layouts per connected monitor.
- **Profile Composition** — Allow a profile to inherit from a base profile.
- **Animated Transitions** — Smoothly animate the layout change rather than snapping.
- **Profile Templates** — Pre-built profiles for common workflows.
- **Versioned Profiles** — Allow multiple versions of a profile, switchable via the GUI.
See [guide_mma.md](guide_mma.md) for the broader workspace profile integration roadmap.