diff --git a/api/config.py b/api/config.py
index 77127bf7..24ef46a8 100644
--- a/api/config.py
+++ b/api/config.py
@@ -4324,6 +4324,7 @@ _SETTINGS_DEFAULTS = {
"font_size": "default", # small | default | large | xlarge
"session_jump_buttons": False, # show Start/End transcript jump pills
"session_endless_scroll": False, # auto-load older transcript pages while scrolling upward
+ "pinned_sessions_limit": 3, # maximum active pinned sessions shown in the sidebar
"hidden_tabs": [], # sidebar tab panel names hidden by user (e.g. ["tasks","kanban"]); chat and settings are always visible
"language": "en", # UI locale code; must match a key in static/i18n.js LOCALES
"bot_name": os.getenv(
@@ -4452,6 +4453,9 @@ _SETTINGS_ENUM_VALUES = {
"auto_title_refresh_every": {"0", "5", "10", "20"},
"busy_input_mode": {"queue", "interrupt", "steer"},
}
+_SETTINGS_INT_RANGES = {
+ "pinned_sessions_limit": (1, 99),
+}
_SETTINGS_BOOL_KEYS = {
"onboarding_completed",
"show_token_usage",
@@ -4512,6 +4516,15 @@ def save_settings(settings: dict) -> dict:
# Validate enum-constrained keys
if k in _SETTINGS_ENUM_VALUES and v not in _SETTINGS_ENUM_VALUES[k]:
continue
+ # Validate bounded integer settings.
+ if k in _SETTINGS_INT_RANGES:
+ try:
+ v = int(v)
+ except (TypeError, ValueError):
+ continue
+ min_value, max_value = _SETTINGS_INT_RANGES[k]
+ if v < min_value or v > max_value:
+ continue
# Validate language codes (BCP-47-like: 'en', 'zh', 'fr', 'zh-CN')
if k == "language" and (
not isinstance(v, str) or not _SETTINGS_LANG_RE.match(v)
diff --git a/api/routes.py b/api/routes.py
index c7f287ac..54c6261b 100644
--- a/api/routes.py
+++ b/api/routes.py
@@ -5767,8 +5767,9 @@ def handle_post(handler, parsed) -> bool:
if getattr(existing, "pinned", False) and not getattr(existing, "archived", False)
)
pinned_ids.discard(body["session_id"])
- if len(pinned_ids) >= 3:
- return bad(handler, "Up to 3 sessions can be pinned. Unpin one before pinning another.", 400)
+ pinned_sessions_limit = int(load_settings().get("pinned_sessions_limit", 3) or 3)
+ if len(pinned_ids) >= pinned_sessions_limit:
+ return bad(handler, f"Up to {pinned_sessions_limit} sessions can be pinned. Unpin one before pinning another.", 400)
# Mark in-memory pin state under LOCK so concurrent pin
# requests see the increment immediately, even before
# save() finishes flushing to disk.
diff --git a/static/boot.js b/static/boot.js
index 61688c49..9b48b910 100644
--- a/static/boot.js
+++ b/static/boot.js
@@ -1451,6 +1451,7 @@ function applyBotName(){
window._showThinking=s.show_thinking!==false;
window._simplifiedToolCalling=s.simplified_tool_calling!==false;
window._sidebarDensity=(s.sidebar_density==='detailed'?'detailed':'compact');
+ window._pinnedSessionsLimit=parseInt(s.pinned_sessions_limit||3,10)||3;
window._busyInputMode=(s.busy_input_mode||'queue');
window._sessionEndlessScrollEnabled=!!s.session_endless_scroll;
window._botName=s.bot_name||'Hermes';
@@ -1539,6 +1540,7 @@ function applyBotName(){
window._simplifiedToolCalling=true;
window._sessionJumpButtonsEnabled=false;
window._sidebarDensity='compact';
+ window._pinnedSessionsLimit=3;
window._busyInputMode='queue';
window._sessionEndlessScrollEnabled=false;
window._botName='Hermes';
diff --git a/static/index.html b/static/index.html
index bb8e86c4..1659189f 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1126,6 +1126,11 @@
Controls how much metadata the session list shows in the left sidebar.
+
+
+
+
Maximum number of active conversations that can be pinned in the sidebar. Default is 3.
+