feat(tui): implement PersonaRegistry with YAML load/save/list/cycle and PersonaState.cycle_persona() #10603

Merged
HAL9000 merged 3 commits from feat/tui-v370/persona-registry into master 2026-06-14 01:24:58 +00:00
2 changed files with 39 additions and 0 deletions
+21
View File
@@ -160,6 +160,11 @@ class PersonaRegistry:
lock.close()
def export_persona(self, name: str, output_path: Path) -> Path:
"""Export a persona to a YAML file at the given path.
Accepts both absolute and relative output paths. Raises ValueError
if the persona does not exist in the registry.
"""
persona = self.get(name)
if persona is None:
raise ValueError(f"Persona not found: {name}")
@@ -169,6 +174,11 @@ class PersonaRegistry:
return safe_output
def import_persona(self, input_path: Path) -> Persona:
"""Import a persona from a YAML file at the given path.
Accepts both absolute and relative input paths. Raises ValueError
if the file does not contain a valid persona dict.
"""
safe_input = self.resolve_import_path(input_path)
raw = yaml.safe_load(safe_input.read_text(encoding="utf-8")) or {}
if not isinstance(raw, dict):
@@ -178,6 +188,11 @@ class PersonaRegistry:
return persona
def load_state(self) -> dict[str, Any]:
"""Load the TUI state dict from the state YAML file.
Returns an empty dict if the file does not exist or contains
non-dict content.
"""
if not self.state_path.exists():
return {}
raw = yaml.safe_load(self.state_path.read_text(encoding="utf-8")) or {}
@@ -186,15 +201,21 @@ class PersonaRegistry:
return dict(raw)
def save_state(self, state: dict[str, Any]) -> None:
"""Persist the TUI state dict to the state YAML file atomically."""
self.ensure_dirs()
self._atomic_write_yaml(self.state_path, state)
def get_last_persona(self) -> str | None:
"""Return the name of the last active persona, or None if unset."""
state = self.load_state()
value = state.get("last_persona")
return value if isinstance(value, str) and value else None
def set_last_persona(self, name: str) -> None:
"""Persist the given persona name as the last active persona.
Uses an exclusive file lock to prevent concurrent write conflicts.
"""
lock = self._lock_file(self.state_lock_path)
try:
state = self.load_state()
+18
View File
@@ -21,11 +21,20 @@ class PersonaState:
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
return self.registry.get_last_persona() or default.name
def active_name(self, session_id: str) -> str:
"""Return the active persona name for the given session.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Initialises the session to the default persona on first access.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
"""
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
if session_id not in self.active_by_session:
self.active_by_session[session_id] = self._resolve_default_name()
return self.active_by_session[session_id]
def active_persona(self, session_id: str) -> Persona:
"""Return the active Persona object for the given session.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Falls back to the registry default if the stored name is no longer
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
present in the registry.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
"""
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
name = self.active_name(session_id)
persona = self.registry.get(name)
if persona is None:
@@ -34,6 +43,11 @@ class PersonaState:
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
return persona
def set_active_persona(self, session_id: str, persona_name: str) -> Persona:
"""Set the active persona for the given session and persist the choice.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Raises ValueError if the persona name is not found in the registry.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Initialises the session preset to "default" if not already set.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
"""
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
persona = self.registry.get(persona_name)
if persona is None:
raise ValueError(f"Unknown persona: {persona_name}")
@@ -44,6 +58,10 @@ class PersonaState:
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
return persona
def current_preset(self, session_id: str) -> str:
"""Return the current argument preset name for the given session.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Defaults to "default" on first access.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
"""
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
if session_id not in self.preset_by_session:
self.preset_by_session[session_id] = "default"
return self.preset_by_session[session_id]
2
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.
Review

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.

Suggestion: cycle_persona() calls self.registry.list_personas() on every Tab press. Consider caching. Also [p.name] plus .index() is O(n); dict/set would be O(1). Low priority.