From c6d41304732122a3cdf79c40c52790075728145a Mon Sep 17 00:00:00 2001 From: louquillio Date: Thu, 16 Jul 2026 20:55:55 -0600 Subject: [PATCH] feat: add session source filter to Settings screen The Android session list shows all Hermes sessions including scheduled tasks and tool calls, making it noisy for mobile users who only want chat sessions. Add a checkbox-style source filter in Settings. Each session origin type gets a checkbox in plain English ("Scheduled tasks", "Signal messages", etc.): unchecked sources are filtered client-side by Session.source. Key design choices: - Client-side filtering (no backend API dependency) - Preference key scoped by connection ID (multi-gateway safe) - Unknown source types always shown - All sources shown by default (backward compatible) --- lib/core/screens/session_list_screen.dart | 9 ++- lib/core/screens/settings_screen.dart | 91 ++++++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/lib/core/screens/session_list_screen.dart b/lib/core/screens/session_list_screen.dart index 1fabe8e..8436175 100644 --- a/lib/core/screens/session_list_screen.dart +++ b/lib/core/screens/session_list_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../services/connection_manager.dart'; import 'chat_screen.dart'; import 'settings_screen.dart'; @@ -54,8 +55,14 @@ class _SessionListScreenState extends State { try { final sessions = await _client.getSessions(); if (!mounted) return; + final prefs = await SharedPreferences.getInstance(); + final key = 'excluded_session_sources_${widget.connection.id}'; + final excluded = prefs.getStringList(key) ?? []; + final filtered = + sessions.where((s) => !excluded.contains(s.source)).toList(); + if (!mounted) return; setState(() { - _sessions = sessions; + _sessions = filtered; _loading = false; }); } catch (e) { diff --git a/lib/core/screens/settings_screen.dart b/lib/core/screens/settings_screen.dart index 900b256..02e5ecc 100644 --- a/lib/core/screens/settings_screen.dart +++ b/lib/core/screens/settings_screen.dart @@ -322,6 +322,11 @@ class _SettingsScreenState extends State { _VoicePicker(), const SizedBox(height: 16), + // ---- Section: Session Sources ---- + _buildSectionHeader('Session Sources'), + _SessionSourcesFilter(connectionId: widget.connection.id), + const SizedBox(height: 16), + // ---- Section: Connection ---- _buildSectionHeader('Connection'), Card( @@ -692,4 +697,88 @@ class _VoicePickerState extends State<_VoicePicker> { onChanged: _set, ); } -} \ No newline at end of file +} + +/// Checkbox list of session sources. Unchecked sources are filtered +/// client-side from the fetched session list by `Session.source`. +class _SessionSourcesFilter extends StatefulWidget { + final String connectionId; + const _SessionSourcesFilter({required this.connectionId}); + + @override + State<_SessionSourcesFilter> createState() => _SessionSourcesFilterState(); +} + +class _SessionSourcesFilterState extends State<_SessionSourcesFilter> { + /// Known session source types. Hermes Gateway persists `session.source` for + /// every session. Sources not in this list are always shown (whitelisted). + static const Map _knownSources = { + 'acp': 'Autonomous agents', + 'api_server': 'External API clients', + 'cli': 'Command-line chats', + 'cron': 'Scheduled tasks', + 'desktop': 'Desktop app', + 'discord': 'Discord chats', + 'gateway': 'Gateway API access', + 'mobile': 'Phone or tablet', + 'signal': 'Signal messages', + 'slack': 'Slack chats', + 'telegram': 'Telegram messages', + 'tool': 'Developer tool calls', + 'tui': 'Terminal sessions', + 'whatsapp': 'WhatsApp messages', + }; + + Set _excluded = {}; + + String get _prefsKey => + 'excluded_session_sources_${widget.connectionId}'; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _excluded = + prefs.getStringList(_prefsKey)?.toSet() ?? {}; + }); + } + + Future _toggle(String source, bool enabled) async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + if (enabled) { + _excluded.remove(source); + } else { + _excluded.add(source); + } + }); + await prefs.setStringList(_prefsKey, _excluded.toList()); + } + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: _knownSources.entries.map((entry) { + final source = entry.key; + final label = entry.value; + final isVisible = !_excluded.contains(source); + return CheckboxListTile( + title: Text(label), + subtitle: Text(source, + style: const TextStyle(fontSize: 12, color: Colors.grey)), + value: isVisible, + onChanged: (val) => _toggle(source, val ?? true), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + ); + }).toList(), + ), + ); + } +}