Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions tests/run_agent/test_primary_runtime_restore.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ def _make_agent(fallback_model=None, provider="custom", base_url="https://my-llm
patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("web_search")),
patch("run_agent.check_toolset_requirements", return_value={}),
patch("run_agent.OpenAI"),
# Unit tests must not probe live endpoints. The compressor resolves
# context length lazily via a real network call against base_url; for
# reachable hosts (the nous portal case) the endpoint's answer for the
# empty test model (32K) trips agent_init's 64K floor and fails the
# test on network behavior, not code under test.
patch(
"agent.context_compressor.get_model_context_length",
return_value=200_000,
),
):
agent = AIAgent(
api_key="test-key-12345678",
Expand Down
61 changes: 61 additions & 0 deletions tui_gateway/methods_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ def _latest_profile_session_row(profile_path):
}
if include_sessions:
row["last_session"] = _latest_profile_session_row(p.path)

# Client-agnostic UI metadata (avatars, accent colors, pinned
# order, …) — stored server-side in profile.yaml so every
# machine connecting to this gateway paints the same roster.
try:
import yaml as _yaml
from pathlib import Path as _Path

meta_path = _Path(str(p.path)) / "profile.yaml"
if meta_path.is_file():
with open(meta_path, "r", encoding="utf-8") as f:
raw_meta = _yaml.safe_load(f) or {}
ui_meta = raw_meta.get("ui_meta")
if isinstance(ui_meta, dict) and ui_meta:
row["ui_meta"] = ui_meta
except Exception:
pass
out.append(row)
return _ok(rid, {"profiles": out})
except Exception as e:
Expand Down Expand Up @@ -382,6 +399,50 @@ def _(rid, params: dict) -> dict:

applied = {}

if isinstance(params.get("ui_meta"), dict):
# Client-agnostic UI metadata (avatar/pet/etc.), merged key-wise
# into profile.yaml's ui_meta block. A key set to None deletes it.
# Size-capped: this rides profiles.list on every roster paint, so
# large blobs (e.g. raw base64 images) are rejected — persist big
# assets elsewhere and store a reference.
try:
import json as _json

incoming = params["ui_meta"]
if len(_json.dumps(incoming)) > 65536:
applied["ui_meta"] = False
else:
import yaml as _yaml

meta_path = profile_dir / "profile.yaml"
existing = {}
if meta_path.is_file():
try:
with open(meta_path, "r", encoding="utf-8") as f:
loaded = _yaml.safe_load(f) or {}
if isinstance(loaded, dict):
existing = loaded
except Exception:
existing = {}
current = existing.get("ui_meta")
if not isinstance(current, dict):
current = {}
for key, value in incoming.items():
if value is None:
current.pop(key, None)
else:
current[key] = value
if current:
existing["ui_meta"] = current
else:
existing.pop("ui_meta", None)
from utils import atomic_yaml_write

atomic_yaml_write(meta_path, existing, sort_keys=False)
applied["ui_meta"] = True
except Exception:
applied["ui_meta"] = False

if isinstance(params.get("soul"), str):
try:
(profile_dir / "SOUL.md").write_text(params["soul"], encoding="utf-8")
Expand Down
Loading