diff --git a/README.md b/README.md index 3848361b..f3da8a38 100644 --- a/README.md +++ b/README.md @@ -477,6 +477,8 @@ Relative paths resolve against the shell's current directory (not agentchattr's Server and wrappers share the same `AGENTCHATTR_*` env vars and the same flag names, so a launcher/profile can run multiple isolated instances by passing matching values to each process. If no flags or env vars are set, `config.toml` is used exactly as before — zero change for existing setups. +With several instances open at once, set **Settings → This server** to give each one a short name. It appears beside the title in the header and in the browser tab title, so you can tell which project a tab belongs to. It is empty by default, which looks exactly as it does now. + ### API agents (local models) Connect any local model with an OpenAI-compatible API (Ollama, llama-server, LM Studio, vLLM, etc.) to the chat room. API agents get status pills, activity indicators, @mention routing, and multi-instance support — just like the CLI agents. diff --git a/app.py b/app.py index 60a6434e..2455384d 100644 --- a/app.py +++ b/app.py @@ -49,6 +49,9 @@ # Room settings (persisted to data/settings.json) room_settings: dict = { "title": "agentchattr", + # Shown beside the title, so several servers can be told apart without + # renaming the product itself. "" hides it. + "subtitle": "", "username": "user", "font": "sans", "channels": ["general"], @@ -124,6 +127,30 @@ def _settings_path() -> Path: return Path(data_dir) / "settings.json" +ROOM_SUBTITLE_MAX = 40 + + +def normalize_room_subtitle(value): + """Clean a room subtitle, or return None if it is not usable. + + Collapses any internal whitespace so a pasted newline cannot break the + header across two lines, and caps the length so a long name cannot crowd + out the rest of the header. + """ + if not isinstance(value, str): + return None + return " ".join(value.split())[:ROOM_SUBTITLE_MAX] + + +def apply_room_subtitle(new: dict): + """Apply a subtitle from a settings payload, if present and usable.""" + if "subtitle" not in new: + return + cleaned = normalize_room_subtitle(new["subtitle"]) + if cleaned is not None: + room_settings["subtitle"] = cleaned + + def _load_settings(): global room_settings p = _settings_path() @@ -133,6 +160,9 @@ def _load_settings(): room_settings.update(saved) except Exception: pass + # settings.json is hand-editable, so the file bypasses every check the + # live update path applies. Re-normalise on load rather than trust it. + room_settings["subtitle"] = normalize_room_subtitle(room_settings.get("subtitle")) or "" # Ensure "general" always exists and is first if "channels" not in room_settings or not room_settings["channels"]: room_settings["channels"] = ["general"] @@ -1265,6 +1295,7 @@ async def websocket_endpoint(websocket: WebSocket): new = event.get("data", {}) if "title" in new and isinstance(new["title"], str): room_settings["title"] = new["title"].strip() or "agentchattr" + apply_room_subtitle(new) if "username" in new and isinstance(new["username"], str): room_settings["username"] = new["username"].strip() or "user" if "font" in new and new["font"] in ("mono", "serif", "sans"): diff --git a/static/chat.js b/static/chat.js index 338653c4..9b048868 100644 --- a/static/chat.js +++ b/static/chat.js @@ -1804,7 +1804,14 @@ let pendingChannelSwitch = null; function applySettings(data) { if (data.title) { document.getElementById('room-title').textContent = data.title; - document.title = data.title; + // Tab title carries both names, so several servers are distinguishable + document.title = data.subtitle ? data.title + ' - ' + data.subtitle : data.title; + } + if (data.subtitle !== undefined) { + const subEl = document.getElementById('room-subtitle'); + if (subEl) subEl.textContent = data.subtitle; + const subInput = document.getElementById('setting-subtitle'); + if (subInput) subInput.value = data.subtitle; } if (data.username) { username = data.username; @@ -1929,6 +1936,7 @@ function clearChat() { function saveSettings() { const newUsername = document.getElementById('setting-username').value.trim(); + const newSubtitle = document.getElementById('setting-subtitle').value.trim(); const newFont = document.getElementById('setting-font').value; const newHops = document.getElementById('setting-hops').value; const histVal = document.getElementById('setting-history').value; @@ -1941,6 +1949,7 @@ function saveSettings() { type: 'update_settings', data: { username: newUsername || 'user', + subtitle: newSubtitle, font: newFont, max_agent_hops: parseInt(newHops) || 4, history_limit: newHistory, @@ -1953,7 +1962,7 @@ function saveSettings() { function setupSettingsKeys() { // Auto-save on blur/Enter for text/number fields - for (const id of ['setting-username', 'setting-hops']) { + for (const id of ['setting-username', 'setting-subtitle', 'setting-hops']) { const el = document.getElementById(id); el.addEventListener('blur', () => saveSettings()); el.addEventListener('keydown', (e) => { diff --git a/static/index.html b/static/index.html index f015bf49..de5b69aa 100644 --- a/static/index.html +++ b/static/index.html @@ -4,7 +4,7 @@ agentchattr - + @@ -63,6 +63,10 @@

agentchattr

+
+ + +