Add previous messaging session controls

This commit is contained in:
junjunjunbong 2026-05-15 15:17:40 +09:00 committed by Nathan Esquenazi
parent e6be01c4dd
commit 3a53592107
9 changed files with 118 additions and 6 deletions

View file

@ -4128,6 +4128,7 @@ _SETTINGS_DEFAULTS = {
"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
@ -4263,6 +4264,7 @@ _SETTINGS_BOOL_KEYS = {
"show_tps",
"fade_text_effect",
"show_cli_sessions",
"show_previous_messaging_sessions",
"sync_to_insights",
"check_for_updates",
"whats_new_summary_enabled",

View file

@ -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 = {
@ -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)

View file

@ -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

View file

@ -430,12 +430,15 @@ const LOCALES = {
session_move_project_desc_has: 'Change the project for this conversation',
session_move_project_desc_none: 'Assign a project to this conversation',
session_archive: 'Archive conversation',
session_hide_external: 'Hide from list',
session_restore: 'Restore conversation',
session_archive_desc: 'Hide this conversation until archived is shown',
session_archive_worktree_desc: 'Hide this conversation; keep its worktree on disk',
session_hide_external_desc: 'Hide this imported session from WebUI without deleting the source history.',
session_restore_desc: 'Bring this conversation back into the main list',
session_archived: 'Session archived',
session_archived_worktree: 'Session archived. Worktree remains on disk.',
session_hidden: 'Session hidden from list',
session_restored: 'Session restored',
session_archive_failed: 'Archive failed: ',
session_duplicate: 'Duplicate conversation',
@ -546,6 +549,7 @@ const LOCALES = {
settings_label_sidebar_density: 'Sidebar density',
cmd_reasoning: 'Toggle thinking visibility (show/hide), set effort level, or check current status',
settings_label_external_sessions: 'Show non-WebUI sessions',
settings_label_previous_messaging_sessions: 'Show previous messaging sessions',
settings_label_sync_insights: 'Sync to insights',
settings_label_check_updates: 'Check for updates',
settings_label_whats_new_summary: "Summarize What's New with AI",
@ -784,6 +788,7 @@ const LOCALES = {
settings_auto_title_refresh_20: 'Every 20 exchanges',
settings_desc_auto_title_refresh: 'Automatically re-generates the session title based on the latest exchange, keeping it relevant as the conversation evolves. Requires an LLM title generation model to be configured.',
settings_desc_external_sessions: 'Show conversations from CLI, Telegram, Discord, Slack, and other channels in the session list. Click to import and continue.',
settings_desc_previous_messaging_sessions: 'Show older Discord, Telegram, Slack, and Weixin sessions that were replaced by reset or compression.',
settings_desc_sync_insights: 'Mirrors WebUI token usage to state.db so hermes /insights includes browser session data. Off by default.',
settings_desc_check_updates: 'Show a banner when newer versions of the WebUI or Agent are available. Runs a background git fetch periodically.',
settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",
@ -10858,12 +10863,15 @@ const LOCALES = {
session_move_project_desc_has: 'Change the project for this conversation',
session_move_project_desc_none: 'Assign a project to this conversation',
session_archive: 'Archive conversation',
session_hide_external: '목록에서 숨기기',
session_restore: 'Restore conversation',
session_archive_desc: 'Hide this conversation until archived is shown',
session_archive_worktree_desc: '이 대화를 숨기고 worktree는 디스크에 유지합니다',
session_hide_external_desc: '원본 기록은 삭제하지 않고 WebUI 목록에서만 숨깁니다.',
session_restore_desc: 'Bring this conversation back into the main list',
session_archived: 'Session archived',
session_archived_worktree: '세션이 보관되었습니다. Worktree는 디스크에 남아 있습니다.',
session_hidden: '목록에서 숨겼습니다',
session_restored: 'Session restored',
session_archive_failed: 'Archive failed: ',
session_duplicate: 'Duplicate conversation',
@ -10974,6 +10982,7 @@ const LOCALES = {
settings_label_sidebar_density: '사이드바 밀도',
cmd_reasoning: 'Toggle thinking visibility (show/hide), set effort level, or check current status',
settings_label_external_sessions: '외부 세션 표시',
settings_label_previous_messaging_sessions: '이전 메시징 세션 표시',
settings_label_sync_insights: 'Insights에 동기화',
settings_label_check_updates: '업데이트 확인',
settings_label_whats_new_summary: "Summarize What's New with AI",
@ -11161,6 +11170,7 @@ const LOCALES = {
settings_auto_title_refresh_20: 'Every 20 exchanges',
settings_desc_auto_title_refresh: '최신 대화를 바탕으로 세션 제목을 자동으로 다시 생성해 대화가 진행되어도 제목을 관련 있게 유지합니다. LLM 제목 생성 모델 설정이 필요합니다.',
settings_desc_external_sessions: 'CLI, Telegram, Discord, Slack 및 기타 채널의 대화를 세션 목록에 표시합니다. 클릭하여 가져오고 계속하세요.',
settings_desc_previous_messaging_sessions: 'reset 또는 compression으로 교체된 이전 Discord, Telegram, Slack, Weixin 세션을 표시합니다.',
settings_desc_sync_insights: 'WebUI 토큰 사용량을 state.db에 반영하여 hermes /insights에 브라우저 세션 데이터가 포함되도록 합니다. 기본값은 꺼짐입니다.',
settings_desc_check_updates: 'WebUI 또는 Agent의 새 버전이 있으면 배너를 표시합니다. 백그라운드에서 주기적으로 git fetch를 실행합니다.',
settings_desc_whats_new_summary: "Changes the What's New action from opening the raw diff first to generating a short, human-readable summary. The regular diff comparison stays available after the summary.",

View file

@ -1129,6 +1129,13 @@
</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)">

View file

@ -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');
@ -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');
@ -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;
@ -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';
@ -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;

View file

@ -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);
}

View file

@ -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
@ -41,6 +41,7 @@ PREFERENCE_FIELDS_AUTOSAVE = [
("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"),
@ -52,8 +53,8 @@ PREFERENCE_FIELDS_AUTOSAVE = [
]
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, \
@ -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()

View file

@ -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.