docs(workspace-profiles): new guide covering profile schema, manager, scope inheritance, and auto-switch
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user