Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

## [Unreleased]

### Added
- Settings now includes a configurable **Non-WebUI session limit** (`cli_session_limit`) for sidebar rows imported from CLI/gateway sources. The cap is persisted via `/api/settings`, applied to the state.db import path (`api/models.py`) and the final sidebar visibility cap (`api/routes.py`), while preserving the legacy fallback of 20 when the setting is missing or invalid.

## [v0.51.195] — 2026-06-01 — Release FO (stage-batch7 — hide attachment path markers in chat UI)

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4804,6 +4804,7 @@ def _get_session_agent_lock(session_id: str) -> threading.Lock:
"show_tps": False, # show tokens-per-second chip in assistant message headers
"fade_text_effect": False, # animate newly streamed words with a lightweight fade-in effect
"show_cli_sessions": False, # merge CLI sessions from state.db into the sidebar
"cli_session_limit": 20, # max non-WebUI sessions to keep visible in sidebar when enabled
"show_previous_messaging_sessions": False, # show older Telegram/Discord/etc. reset segments
"sync_to_insights": False, # mirror WebUI token usage to state.db for /insights
"check_for_updates": True, # check if webui/agent repos are behind upstream
Expand Down Expand Up @@ -4949,6 +4950,7 @@ def load_settings() -> dict:
"busy_input_mode": {"queue", "interrupt", "steer"},
}
_SETTINGS_INT_RANGES = {
"cli_session_limit": (1, 500),
"pinned_sessions_limit": (1, 99),
"inflight_state_max_sessions": (1, 25),
"inflight_state_max_messages": (1, 100),
Expand Down
17 changes: 16 additions & 1 deletion api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3300,6 +3300,21 @@ def _cli_sessions_cache_ttl_seconds() -> float:
return 5.0


def _cli_visible_session_limit() -> int:
"""Resolve non-WebUI sidebar session cap from settings.

Keeps legacy behavior (20) when the setting is missing/invalid.
"""
try:
raw = _cfg.load_settings().get('cli_session_limit', CLI_VISIBLE_SESSION_LIMIT)
value = int(raw)
except Exception:
value = CLI_VISIBLE_SESSION_LIMIT
if value < 1:
return CLI_VISIBLE_SESSION_LIMIT
return value


def _path_cache_key(path) -> str | None:
if path is None:
return None
Expand Down Expand Up @@ -3384,7 +3399,7 @@ def _cron_pid():

for row in read_importable_agent_session_rows(
db_path,
limit=CLI_VISIBLE_SESSION_LIMIT,
limit=_cli_visible_session_limit(),
log=logger,
exclude_sources=None,
):
Expand Down
18 changes: 17 additions & 1 deletion api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2662,6 +2662,19 @@ def _dedupe_cli_sidebar_sessions_for_api(cli: list[dict], represented_webui_ids:
CLI_VISIBLE_SESSION_CAP = 20


def _resolve_cli_session_cap(settings: dict | None = None) -> int:
"""Resolve non-WebUI sidebar cap from settings with safe fallback."""
try:
source = settings if isinstance(settings, dict) else load_settings()
raw = source.get("cli_session_limit", CLI_VISIBLE_SESSION_CAP)
value = int(raw)
except Exception:
value = CLI_VISIBLE_SESSION_CAP
if value < 1:
return CLI_VISIBLE_SESSION_CAP
return value


def _cap_recent_cli_sessions(sessions: list[dict], cli_cap: int = CLI_VISIBLE_SESSION_CAP) -> list[dict]:
"""Keep only the most recent CLI-visible sessions after filtering."""
if cli_cap <= 0:
Expand Down Expand Up @@ -4887,7 +4900,10 @@ def handle_get(handler, parsed) -> bool:
)
if show_cli_sessions:
diag.stage("cli_cap")
scoped = _cap_recent_cli_sessions(scoped, cli_cap=CLI_VISIBLE_SESSION_CAP)
scoped = _cap_recent_cli_sessions(
scoped,
cli_cap=_resolve_cli_session_cap(settings),
)
diag.stage("redact_sessions")
safe_merged = []
for s in scoped:
Expand Down
2 changes: 2 additions & 0 deletions static/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,7 @@ function applyBotName(){
window._showTps=!!s.show_tps;
window._fadeTextEffect=!!s.fade_text_effect;
window._showCliSessions=!!s.show_cli_sessions;
window._cliSessionLimit=parseInt(s.cli_session_limit||20,10)||20;
window._showPreviousMessagingSessions=!!s.show_previous_messaging_sessions;
window._soundEnabled=!!s.sound_enabled;
window._notificationsEnabled=!!s.notifications_enabled;
Expand Down Expand Up @@ -1721,6 +1722,7 @@ function applyBotName(){
window._showTps=false;
window._fadeTextEffect=false;
window._showCliSessions=false;
window._cliSessionLimit=20;
window._soundEnabled=false;
window._notificationsEnabled=false;
window._whatsNewSummaryEnabled=false;
Expand Down
5 changes: 5 additions & 0 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,11 @@ <h2 data-i18n="empty_title">What can I help with?</h2>
</label>
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_external_sessions">Show conversations from CLI, Telegram, Discord, Slack, and other channels in the session list. Click to import and continue.</div>
</div>
<div class="settings-field">
<label for="settingsCliSessionLimit">Non-WebUI session limit</label>
<input type="number" id="settingsCliSessionLimit" min="1" max="500" step="1" inputmode="numeric" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px">
<div style="font-size:11px;color:var(--muted);margin-top:4px">Maximum number of non-WebUI sessions (CLI/Telegram/Discord/etc.) shown in the sidebar when enabled. Default is 20.</div>
</div>
<div class="settings-field">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="settingsShowPreviousMessagingSessions" style="width:15px;height:15px;accent-color:var(--accent)">
Expand Down
10 changes: 10 additions & 0 deletions static/panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -5906,6 +5906,8 @@ function _preferencesPayloadFromUi(){
if(apiRedactCb) payload.api_redact_enabled=apiRedactCb.checked;
const showCliCb=$('settingsShowCliSessions');
if(showCliCb) payload.show_cli_sessions=showCliCb.checked;
const cliLimitField=$('settingsCliSessionLimit');
if(cliLimitField) payload.cli_session_limit=parseInt(cliLimitField.value,10);
const showPreviousMessagingCb=$('settingsShowPreviousMessagingSessions');
if(showPreviousMessagingCb) payload.show_previous_messaging_sessions=showPreviousMessagingCb.checked;
const syncCb=$('settingsSyncInsights');
Expand Down Expand Up @@ -6201,6 +6203,12 @@ async function loadSettingsPanel(){
if(apiRedactCb){apiRedactCb.checked=settings.api_redact_enabled!==false;apiRedactCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
const showCliCb=$('settingsShowCliSessions');
if(showCliCb){showCliCb.checked=!!settings.show_cli_sessions;showCliCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
const cliLimitField=$('settingsCliSessionLimit');
if(cliLimitField){
cliLimitField.value=parseInt(settings.cli_session_limit||20,10)||20;
cliLimitField.addEventListener('change',_schedulePreferencesAutosave,{once:false});
cliLimitField.addEventListener('input',_schedulePreferencesAutosave,{once:false});
}
const showPreviousMessagingCb=$('settingsShowPreviousMessagingSessions');
if(showPreviousMessagingCb){showPreviousMessagingCb.checked=!!settings.show_previous_messaging_sessions;showPreviousMessagingCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
const syncCb=$('settingsSyncInsights');
Expand Down Expand Up @@ -7432,6 +7440,7 @@ async function saveSettings(andClose){
const showTps=!!($('settingsShowTps')||{}).checked;
const fadeTextEffect=!!($('settingsFadeTextEffect')||{}).checked;
const showCliSessions=!!($('settingsShowCliSessions')||{}).checked;
const cliSessionLimit=parseInt(($('settingsCliSessionLimit')||{}).value,10)||20;
const showPreviousMessagingSessions=!!($('settingsShowPreviousMessagingSessions')||{}).checked;
const pinnedSessionsLimit=parseInt(($('settingsPinnedSessionsLimit')||{}).value,10)||3;
const pw=($('settingsPassword')||{}).value;
Expand All @@ -7457,6 +7466,7 @@ async function saveSettings(andClose){
body.simplified_tool_calling=!!($('settingsSimplifiedToolCalling')||{}).checked;
body.api_redact_enabled=!!($('settingsApiRedact')||{}).checked;
body.show_cli_sessions=showCliSessions;
body.cli_session_limit=cliSessionLimit;
body.show_previous_messaging_sessions=showPreviousMessagingSessions;
body.pinned_sessions_limit=pinnedSessionsLimit;
body.sync_to_insights=!!($('settingsSyncInsights')||{}).checked;
Expand Down
66 changes: 66 additions & 0 deletions tests/test_configurable_cli_session_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Regression checks for configurable non-WebUI sidebar session limits."""

import json
import pathlib
import urllib.error
import urllib.request

from tests._pytest_port import BASE

ROOT = pathlib.Path(__file__).resolve().parent.parent
CONFIG_PY = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
MODELS_PY = (ROOT / "api" / "models.py").read_text(encoding="utf-8")
ROUTES_PY = (ROOT / "api" / "routes.py").read_text(encoding="utf-8")
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")


def post(path, body=None):
data = json.dumps(body or {}).encode()
req = urllib.request.Request(
BASE + path,
data=data,
headers={"Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=10) as r:
return json.loads(r.read()), r.status
except urllib.error.HTTPError as e:
return json.loads(e.read()), e.code


def test_cli_session_limit_setting_is_exposed_and_wired():
assert '"cli_session_limit": 20' in CONFIG_PY
assert '"cli_session_limit": (1, 500)' in CONFIG_PY
assert "def _cli_visible_session_limit()" in MODELS_PY
assert "limit=_cli_visible_session_limit()" in MODELS_PY
assert "def _resolve_cli_session_cap(" in ROUTES_PY
assert "cli_cap=_resolve_cli_session_cap(settings)" in ROUTES_PY
assert 'id="settingsCliSessionLimit"' in INDEX_HTML
assert 'min="1"' in INDEX_HTML
assert 'max="500"' in INDEX_HTML
assert "payload.cli_session_limit=parseInt(cliLimitField.value,10)" in PANELS_JS
assert "settings.cli_session_limit" in PANELS_JS
assert "window._cliSessionLimit=parseInt(s.cli_session_limit||20,10)||20" in BOOT_JS


def test_settings_api_persists_cli_session_limit_and_rejects_invalid_values():
try:
d, status = post("/api/settings", {"cli_session_limit": 200})
assert status == 200
assert d["cli_session_limit"] == 200

d, status = post("/api/settings", {"cli_session_limit": "150"})
assert status == 200
assert d["cli_session_limit"] == 150

d, status = post("/api/settings", {"cli_session_limit": 0})
assert status == 200
assert d["cli_session_limit"] == 150

d, status = post("/api/settings", {"cli_session_limit": 9999})
assert status == 200
assert d["cli_session_limit"] == 150
finally:
post("/api/settings", {"cli_session_limit": 20})