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
2 changes: 2 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4128,6 +4128,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
"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
"whats_new_summary_enabled": False, # show an LLM-written What's New summary before diff links
Expand Down Expand Up @@ -4263,6 +4264,7 @@ def load_settings() -> dict:
"show_tps",
"fade_text_effect",
"show_cli_sessions",
"show_previous_messaging_sessions",
"sync_to_insights",
"check_for_updates",
"whats_new_summary_enabled",
Expand Down
16 changes: 14 additions & 2 deletions api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2158,8 +2158,15 @@ def _messaging_source_key(session: dict) -> str | None:
return _messaging_session_identity(session, raw)


def _keep_latest_messaging_session_per_source(sessions: list[dict]) -> list[dict]:
def _keep_latest_messaging_session_per_source(
sessions: list[dict],
*,
show_previous_messaging_sessions: bool = False,
) -> list[dict]:
"""Keep only the newest sidebar row per messaging session identity."""
if show_previous_messaging_sessions:
return sorted(sessions, key=_session_sort_timestamp, reverse=True)

gateway_metadata = _load_gateway_session_identity_map()
active_gateway_session_ids = {str(sid) for sid in gateway_metadata.keys() if sid}
active_gateway_sources = {
Expand Down Expand Up @@ -3943,7 +3950,12 @@ def handle_get(handler, parsed) -> bool:
if _profiles_match(s.get("profile"), active_profile)]
other_profile_count = len(merged) - len(scoped)
diag.stage("messaging_dedupe")
scoped = _keep_latest_messaging_session_per_source(scoped)
scoped = _keep_latest_messaging_session_per_source(
scoped,
show_previous_messaging_sessions=bool(
settings.get("show_previous_messaging_sessions")
),
)
if show_cli_sessions:
diag.stage("cli_cap")
scoped = _cap_recent_cli_sessions(scoped, cli_cap=CLI_VISIBLE_SESSION_CAP)
Expand Down
1 change: 1 addition & 0 deletions static/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,7 @@ function applyBotName(){
window._showTps=!!s.show_tps;
window._fadeTextEffect=!!s.fade_text_effect;
window._showCliSessions=!!s.show_cli_sessions;
window._showPreviousMessagingSessions=!!s.show_previous_messaging_sessions;
window._soundEnabled=!!s.sound_enabled;
window._notificationsEnabled=!!s.notifications_enabled;
// Persist default workspace so the blank new-chat page can show it
Expand Down
55 changes: 55 additions & 0 deletions static/i18n.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,13 @@ <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 style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="settingsShowPreviousMessagingSessions" style="width:15px;height:15px;accent-color:var(--accent)">
<span data-i18n="settings_label_previous_messaging_sessions">Show previous messaging sessions</span>
</label>
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_previous_messaging_sessions">Show older Discord, Telegram, Slack, and Weixin sessions that were replaced by reset or compression.</div>
</div>
<div class="settings-field">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="settingsSyncInsights" style="width:15px;height:15px;accent-color:var(--accent)">
Expand Down
7 changes: 7 additions & 0 deletions static/panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -5272,6 +5272,8 @@ function _preferencesPayloadFromUi(){
if(apiRedactCb) payload.api_redact_enabled=apiRedactCb.checked;
const showCliCb=$('settingsShowCliSessions');
if(showCliCb) payload.show_cli_sessions=showCliCb.checked;
const showPreviousMessagingCb=$('settingsShowPreviousMessagingSessions');
if(showPreviousMessagingCb) payload.show_previous_messaging_sessions=showPreviousMessagingCb.checked;
const syncCb=$('settingsSyncInsights');
if(syncCb) payload.sync_to_insights=syncCb.checked;
const updateCb=$('settingsCheckUpdates');
Expand Down Expand Up @@ -5524,6 +5526,8 @@ 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 showPreviousMessagingCb=$('settingsShowPreviousMessagingSessions');
if(showPreviousMessagingCb){showPreviousMessagingCb.checked=!!settings.show_previous_messaging_sessions;showPreviousMessagingCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
const syncCb=$('settingsSyncInsights');
if(syncCb){syncCb.checked=!!settings.sync_to_insights;syncCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
const updateCb=$('settingsCheckUpdates');
Expand Down Expand Up @@ -6287,6 +6291,7 @@ function _applySavedSettingsUi(saved, body, opts){
window._showTps=showTps;
window._fadeTextEffect=!!fadeTextEffect;
window._showCliSessions=showCliSessions;
window._showPreviousMessagingSessions=!!body.show_previous_messaging_sessions;
window._soundEnabled=body.sound_enabled;
window._notificationsEnabled=body.notifications_enabled;
window._whatsNewSummaryEnabled=!!body.whats_new_summary_enabled;
Expand Down Expand Up @@ -6382,6 +6387,7 @@ async function saveSettings(andClose){
const showTps=!!($('settingsShowTps')||{}).checked;
const fadeTextEffect=!!($('settingsFadeTextEffect')||{}).checked;
const showCliSessions=!!($('settingsShowCliSessions')||{}).checked;
const showPreviousMessagingSessions=!!($('settingsShowPreviousMessagingSessions')||{}).checked;
const pw=($('settingsPassword')||{}).value;
const theme=($('settingsTheme')||{}).value||'dark';
const skin=($('settingsSkin')||{}).value||'default';
Expand All @@ -6405,6 +6411,7 @@ async function saveSettings(andClose){
body.simplified_tool_calling=!!($('settingsSimplifiedToolCalling')||{}).checked;
body.api_redact_enabled=!!($('settingsApiRedact')||{}).checked;
body.show_cli_sessions=showCliSessions;
body.show_previous_messaging_sessions=showPreviousMessagingSessions;
body.sync_to_insights=!!($('settingsSyncInsights')||{}).checked;
body.check_for_updates=!!($('settingsCheckUpdates')||{}).checked;
body.whats_new_summary_enabled=!!($('settingsWhatsNewSummary')||{}).checked;
Expand Down
17 changes: 17 additions & 0 deletions static/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1773,6 +1773,23 @@ function _openSessionActionMenu(session, anchorEl){
}catch(err){showToast(t('session_archive_failed')+err.message);}
}
));
if(isExternalSession && !session.archived){
menu.appendChild(_buildSessionAction(
t('session_hide_external'),
t('session_hide_external_desc'),
ICONS.archive,
async()=>{
closeSessionActionMenu();
try{
await api('/api/session/archive',{method:'POST',body:JSON.stringify({session_id:session.session_id,archived:true})});
session.archived=true;
if(S.session&&S.session.session_id===session.session_id) S.session.archived=true;
await renderSessionList();
showToast(t('session_hidden'));
}catch(err){showToast(t('session_archive_failed')+err.message);}
}
));
}
if(!isExternalSession){
_appendSessionDuplicateAction(menu, session);
}
Expand Down
9 changes: 5 additions & 4 deletions tests/test_1003_preferences_autosave.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Mirrors the structure of test_1003_appearance_autosave.py to verify the
preferences-panel autosave pattern is wired correctly:

- All 14 preference fields use _schedulePreferencesAutosave (not _markSettingsDirty)
- All 15 preference fields use _schedulePreferencesAutosave (not _markSettingsDirty)
- Password field MUST still call _markSettingsDirty (security: never autosave)
- _preferencesPayloadFromUi covers all 14 fields
- _setPreferencesAutosaveStatus uses the shared i18n keys
Expand Down Expand Up @@ -41,6 +41,7 @@ def _load_settings_panel_block() -> str:
("settingsShowTps", "show_tps"),
("settingsSimplifiedToolCalling", "simplified_tool_calling"),
("settingsShowCliSessions", "show_cli_sessions"),
("settingsShowPreviousMessagingSessions", "show_previous_messaging_sessions"),
("settingsSyncInsights", "sync_to_insights"),
("settingsCheckUpdates", "check_for_updates"),
("settingsSoundEnabled", "sound_enabled"),
Expand All @@ -52,8 +53,8 @@ def _load_settings_panel_block() -> str:
]


def test_all_14_preference_fields_have_autosave_payload_entries():
"""_preferencesPayloadFromUi must include all 14 preference fields."""
def test_all_15_preference_fields_have_autosave_payload_entries():
"""_preferencesPayloadFromUi must include all 15 preference fields."""
block = _function_block(PANELS_JS, "_preferencesPayloadFromUi")
for dom_id, field in PREFERENCE_FIELDS_AUTOSAVE:
assert f"$('{dom_id}')" in block, \
Expand All @@ -63,7 +64,7 @@ def test_all_14_preference_fields_have_autosave_payload_entries():


def test_preference_fields_use_schedule_autosave_not_mark_dirty():
"""All 13 listener attachments (excluding bot_name's debounce wrapper) must
"""All 14 listener attachments (excluding bot_name's debounce wrapper) must
use _schedulePreferencesAutosave. bot_name uses a wrapper but still
eventually calls _schedulePreferencesAutosave."""
panel = _load_settings_panel_block()
Expand Down
11 changes: 6 additions & 5 deletions tests/test_1466_sidebar_cancel_clarify.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ def test_running_sidebar_sessions_get_stop_action(self):
"""Running sessions need a context-menu cancel action even when not active pane."""
# Window bumped from 3200 → 4400 in #1764 to accommodate the new
# Rename action item, then to 5200 in #2111 for response-aware archive
# toast handling inside _openSessionActionMenu before the stop/delete
# actions.
# toast handling, then to 6400 in #2294 for the new "Hide from list"
# action prepended for external sessions.
# The `session.active_stream_id` / cancelSessionStream / delete checks
# are positional further down in the function, so growing the prefix
# required growing this read window.
body = _function_body(SESSIONS_JS, "_openSessionActionMenu", 5200)
body = _function_body(SESSIONS_JS, "_openSessionActionMenu", 6400)
assert "session.active_stream_id" in body, (
"sidebar action menu must detect per-session active_stream_id instead of S.activeStreamId"
)
Expand Down Expand Up @@ -75,8 +75,9 @@ def test_cli_session_helper_identifies_cli_origin(self):
def test_cli_sessions_hide_duplicate_and_delete_in_action_menu(self):
"""Session action menu should hide duplicate/delete for CLI-origin sessions."""
# Window bumped 3600 → 4800 in #1764 (Rename action prepended), then
# to 5200 in #2111 for response-aware archive toast handling.
body = _function_body(SESSIONS_JS, "_openSessionActionMenu", 5200)
# to 5200 in #2111 for response-aware archive toast handling, then
# to 6400 in #2294 for the "Hide from list" action on external sessions.
body = _function_body(SESSIONS_JS, "_openSessionActionMenu", 6400)
assert "const isCliSession = _isCliSession(session);" in body
assert "const isExternalSession = isMessagingSession || isCliSession;" in body
assert "if(!isExternalSession)" in body
Expand Down
55 changes: 55 additions & 0 deletions tests/test_gateway_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,61 @@ def test_sessions_js_treats_email_as_messaging_source():
assert "email: 'Email'" in src[src.find("_MESSAGING_SOURCE_LABELS"):src.find("function _isMessagingSession")]


def test_previous_messaging_setting_keeps_reset_history(monkeypatch):
"""The previous-messaging toggle exposes older reset segments."""
import api.routes as routes

monkeypatch.setattr(
routes,
"_load_gateway_session_identity_map",
lambda: {
"discord_active": {
"raw_source": "discord",
"platform": "discord",
"user_id": "user-1",
}
},
)

rows = [
{
"session_id": "discord_active",
"title": "Current Discord chat",
"source_tag": "discord",
"raw_source": "discord",
"session_source": "messaging",
"source_label": "Discord",
"user_id": "user-1",
"message_count": 3,
"updated_at": 200.0,
},
{
"session_id": "discord_previous_history",
"title": "Previous Discord chat",
"source_tag": "discord",
"raw_source": "discord",
"session_source": "messaging",
"source_label": "Discord",
"user_id": "user-1",
"message_count": 7,
"updated_at": 100.0,
"end_reason": "session_reset",
},
]

hidden = routes._keep_latest_messaging_session_per_source(rows)
visible = routes._keep_latest_messaging_session_per_source(
rows,
show_previous_messaging_sessions=True,
)

assert [row["session_id"] for row in hidden] == ["discord_active"]
assert [row["session_id"] for row in visible] == [
"discord_active",
"discord_previous_history",
]


def test_cross_source_parent_child_is_not_collapsed_into_root_metadata(cleanup_test_sessions):
"""A WebUI continuation from a messaging parent must keep WebUI metadata.

Expand Down
6 changes: 5 additions & 1 deletion tests/test_issue1611_session_profile_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,11 @@ def test_keep_latest_messaging_runs_after_profile_filter():
block = src[handler_idx:next_handler]

filter_idx = block.find('_profiles_match(s.get("profile"), active_profile)')
dedupe_idx = block.find('_keep_latest_messaging_session_per_source(scoped)')
# The dedupe call can be either single-line `(scoped)` or multi-line
# `(\n scoped,\n show_previous_messaging_sessions=…,\n)`; match the
# function name + the first arg position rather than coupling to the call
# shape. (#2294 added the keyword-arg form.)
dedupe_idx = block.find('_keep_latest_messaging_session_per_source(')
assert filter_idx > 0, "Profile filter not found in /api/sessions handler"
assert dedupe_idx > 0, "Messaging dedupe must run on the scoped list"
assert filter_idx < dedupe_idx, (
Expand Down
Loading