diff --git a/desktop/src/components/layout/Sidebar.test.tsx b/desktop/src/components/layout/Sidebar.test.tsx index 473c41a16..9a35c8a7a 100644 --- a/desktop/src/components/layout/Sidebar.test.tsx +++ b/desktop/src/components/layout/Sidebar.test.tsx @@ -102,6 +102,7 @@ vi.mock('../../i18n', () => ({ import { Sidebar } from './Sidebar' import { useChatStore } from '../../stores/chatStore' import { useSessionStore } from '../../stores/sessionStore' +import { useSettingsStore } from '../../stores/settingsStore' import { useTabStore } from '../../stores/tabStore' import { useUIStore } from '../../stores/uiStore' import type { SessionListItem } from '../../types/session' @@ -1046,3 +1047,87 @@ describe('Sidebar', () => { }) }) }) + +describe('Sidebar observer session filtering', () => { + const fetchSessions = vi.fn() + + beforeEach(() => { + fetchSessions.mockReset() + useTabStore.setState({ tabs: [], activeTabId: null }) + useSessionStore.setState({ + sessions: [], + activeSessionId: null, + isLoading: false, + error: null, + selectedProjects: [], + availableProjects: [], + isBatchMode: false, + selectedSessionIds: new Set(), + fetchSessions, + createSession: vi.fn(), + deleteSession: vi.fn(), + deleteSessions: vi.fn(), + }) + useUIStore.setState({ + sidebarOpen: true, + addToast: vi.fn(), + } as Partial>) + useSettingsStore.setState({ observerSessionsHidden: false }) + }) + + afterEach(() => { + cleanup() + }) + + const observerSession = { + id: 'observer-1', + title: 'Hello memory agent', + createdAt: new Date().toISOString(), + modifiedAt: new Date().toISOString(), + messageCount: 5, + projectPath: '-Users-test--claude-mem-observer-sessions', + workDir: '/Users/test/.claude-mem/observer-sessions', + workDirExists: true, + } + + const normalSession = { + id: 'normal-1', + title: 'Bug fix in auth', + createdAt: new Date().toISOString(), + modifiedAt: new Date().toISOString(), + messageCount: 3, + projectPath: '-Users-test-my-project', + workDir: '/Users/test/my-project', + workDirExists: true, + } + + it('shows observer sessions when toggle is off', () => { + useSettingsStore.setState({ observerSessionsHidden: false }) + useSessionStore.setState({ sessions: [normalSession, observerSession] }) + + render() + + expect(screen.getByText('Hello memory agent')).toBeInTheDocument() + expect(screen.getByText('Bug fix in auth')).toBeInTheDocument() + }) + + it('hides observer sessions when toggle is on', () => { + useSettingsStore.setState({ observerSessionsHidden: true }) + useSessionStore.setState({ sessions: [normalSession, observerSession] }) + + render() + + expect(screen.queryByText('Hello memory agent')).not.toBeInTheDocument() + expect(screen.getByText('Bug fix in auth')).toBeInTheDocument() + }) + + it('shows no sessions message when all sessions are filtered out', () => { + useSettingsStore.setState({ observerSessionsHidden: true }) + useSessionStore.setState({ sessions: [observerSession] }) + + render() + + expect(screen.queryByText('Hello memory agent')).not.toBeInTheDocument() + expect(screen.getByText('No sessions')).toBeInTheDocument() + }) +}) diff --git a/desktop/src/components/layout/Sidebar.tsx b/desktop/src/components/layout/Sidebar.tsx index a4593ddfd..ee50296aa 100644 --- a/desktop/src/components/layout/Sidebar.tsx +++ b/desktop/src/components/layout/Sidebar.tsx @@ -9,6 +9,7 @@ import { useTabStore, SETTINGS_TAB_ID, SCHEDULED_TAB_ID } from '../../stores/tab import { useChatStore } from '../../stores/chatStore' import { useOpenTargetStore } from '../../stores/openTargetStore' import { desktopUiPreferencesApi, type SidebarProjectPreferences } from '../../api/desktopUiPreferences' +import { useSettingsStore } from '../../stores/settingsStore' const isTauri = typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window) const isWindows = typeof navigator !== 'undefined' && /Win/.test(navigator.platform) @@ -61,6 +62,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) { const activeTabId = useTabStore((s) => s.activeTabId) const closeTab = useTabStore((s) => s.closeTab) const disconnectSession = useChatStore((s) => s.disconnectSession) + const observerSessionsHidden = useSettingsStore((s) => s.observerSessionsHidden) const [searchQuery, setSearchQuery] = useState('') const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null) const [projectContextMenu, setProjectContextMenu] = useState<{ key: string; x: number; y: number } | null>(null) @@ -105,12 +107,20 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) { const filteredSessions = useMemo(() => { let result = sessions + if (observerSessionsHidden) { + result = result.filter((s) => { + const p = (s.projectPath || '').toLowerCase() + const w = (s.workDir || '').toLowerCase() + return !p.includes('claude-mem') && !p.includes('claude_mem') && + !w.includes('/.claude-mem/observer-sessions') + }) + } if (searchQuery) { const q = searchQuery.toLowerCase() result = result.filter((s) => s.title.toLowerCase().includes(q)) } return result - }, [sessions, searchQuery]) + }, [sessions, searchQuery, observerSessionsHidden]) const projectGroups = useMemo(() => groupByProject(filteredSessions, projectSortBy), [filteredSessions, projectSortBy]) const orderedProjectGroups = useMemo( diff --git a/desktop/src/components/plugins/PluginDetail.tsx b/desktop/src/components/plugins/PluginDetail.tsx index abe9f5d66..4216033e3 100644 --- a/desktop/src/components/plugins/PluginDetail.tsx +++ b/desktop/src/components/plugins/PluginDetail.tsx @@ -10,6 +10,7 @@ import { SETTINGS_TAB_ID, useTabStore } from '../../stores/tabStore' import { useSkillStore } from '../../stores/skillStore' import { useAgentStore } from '../../stores/agentStore' import { useMcpStore } from '../../stores/mcpStore' +import { useSettingsStore } from '../../stores/settingsStore' const CAPABILITY_ORDER: PluginCapabilityKey[] = [ 'lspServers', @@ -35,6 +36,8 @@ export function PluginDetail() { const selectAgent = useAgentStore((s) => s.selectAgent) const fetchServers = useMcpStore((s) => s.fetchServers) const selectServer = useMcpStore((s) => s.selectServer) + const observerSessionsHidden = useSettingsStore((s) => s.observerSessionsHidden) + const setObserverSessionsHidden = useSettingsStore((s) => s.setObserverSessionsHidden) const t = useTranslation() const [actionKey, setActionKey] = useState(null) const [showUninstallDialog, setShowUninstallDialog] = useState(false) @@ -309,6 +312,41 @@ export function PluginDetail() {

+ {selectedPlugin.id === 'claude-mem@thedotmack' && ( +
+
+ )} + {selectedPlugin.errors.length > 0 && (
diff --git a/desktop/src/stores/settingsStore.test.ts b/desktop/src/stores/settingsStore.test.ts index 8c45f1725..fdac50ab3 100644 --- a/desktop/src/stores/settingsStore.test.ts +++ b/desktop/src/stores/settingsStore.test.ts @@ -597,3 +597,108 @@ describe('settingsStore H5 access behavior', () => { expect('h5AccessGeneratedToken' in useSettingsStore.getState()).toBe(false) }) }) + +describe('settingsStore observer sessions hidden', () => { + beforeEach(() => { + vi.resetModules() + vi.clearAllMocks() + }) + + it('defaults to false (observer sessions visible)', async () => { + const { useSettingsStore } = await import('./settingsStore') + + expect(useSettingsStore.getState().observerSessionsHidden).toBe(false) + }) + + it('hydrates claudeMemObserverSessionsHidden from user settings', async () => { + vi.doMock('../api/settings', () => ({ + settingsApi: { + getUser: vi.fn().mockResolvedValue({ claudeMemObserverSessionsHidden: true }), + updateUser: vi.fn(), + getPermissionMode: vi.fn().mockResolvedValue({ mode: 'default' }), + setPermissionMode: vi.fn(), + getCliLauncherStatus: vi.fn(), + }, + })) + vi.doMock('../api/models', () => ({ + modelsApi: { + list: vi.fn().mockResolvedValue({ models: [] }), + getCurrent: vi.fn().mockResolvedValue({ model: null }), + setCurrent: vi.fn(), + getEffort: vi.fn().mockResolvedValue({ level: 'medium' }), + setEffort: vi.fn(), + }, + })) + vi.doMock('../api/h5Access', () => ({ + h5AccessApi: { + get: vi.fn().mockResolvedValue({ + settings: { + enabled: false, + tokenPreview: null, + allowedOrigins: [], + publicBaseUrl: null, + }, + }), + enable: vi.fn(), + disable: vi.fn(), + regenerate: vi.fn(), + update: vi.fn(), + }, + })) + + const { useSettingsStore } = await import('./settingsStore') + + await useSettingsStore.getState().fetchAll() + + expect(useSettingsStore.getState().observerSessionsHidden).toBe(true) + }) + + it('persists toggle state to user settings', async () => { + const updateUser = vi.fn().mockResolvedValue({ ok: true }) + + vi.doMock('../api/settings', () => ({ + settingsApi: { + getUser: vi.fn(), + updateUser, + getPermissionMode: vi.fn(), + setPermissionMode: vi.fn(), + getCliLauncherStatus: vi.fn(), + }, + })) + vi.doMock('../api/models', () => ({ + modelsApi: { + list: vi.fn(), + getCurrent: vi.fn(), + setCurrent: vi.fn(), + getEffort: vi.fn(), + setEffort: vi.fn(), + }, + })) + vi.doMock('../api/h5Access', () => ({ + h5AccessApi: { + get: vi.fn().mockResolvedValue({ + settings: { + enabled: false, + tokenPreview: null, + allowedOrigins: [], + publicBaseUrl: null, + }, + }), + enable: vi.fn(), + disable: vi.fn(), + regenerate: vi.fn(), + update: vi.fn(), + }, + })) + + const { useSettingsStore } = await import('./settingsStore') + + await useSettingsStore.getState().setObserverSessionsHidden(true) + expect(useSettingsStore.getState().observerSessionsHidden).toBe(true) + expect(updateUser).toHaveBeenCalledWith({ claudeMemObserverSessionsHidden: true }) + + await useSettingsStore.getState().setObserverSessionsHidden(false) + expect(useSettingsStore.getState().observerSessionsHidden).toBe(false) + expect(updateUser).toHaveBeenCalledWith({ claudeMemObserverSessionsHidden: undefined }) + }) +}) diff --git a/desktop/src/stores/settingsStore.ts b/desktop/src/stores/settingsStore.ts index 51e628ab6..71fafcca1 100644 --- a/desktop/src/stores/settingsStore.ts +++ b/desktop/src/stores/settingsStore.ts @@ -47,6 +47,7 @@ type SettingsStore = { h5AccessError: string | null responseLanguage: string uiZoom: number + observerSessionsHidden: boolean isLoading: boolean error: string | null @@ -70,6 +71,7 @@ type SettingsStore = { }) => Promise setResponseLanguage: (language: string) => Promise setUiZoom: (zoom: number) => void + setObserverSessionsHidden: (hidden: boolean) => Promise } const DEFAULT_H5_ACCESS_SETTINGS: H5AccessSettings = { @@ -95,6 +97,7 @@ export const useSettingsStore = create((set, get) => ({ h5AccessError: null, responseLanguage: '', uiZoom: readStoredAppZoomLevel(), + observerSessionsHidden: false, isLoading: false, error: null, @@ -132,6 +135,7 @@ export const useSettingsStore = create((set, get) => ({ h5Access: h5AccessResult.settings, h5AccessError: h5AccessResult.error, responseLanguage: typeof userSettings.language === 'string' ? userSettings.language : '', + observerSessionsHidden: userSettings.claudeMemObserverSessionsHidden === true, isLoading: false, error: null, }) @@ -310,6 +314,16 @@ export const useSettingsStore = create((set, get) => ({ set({ responseLanguage: prev }) } }, + + setObserverSessionsHidden: async (hidden) => { + const prev = get().observerSessionsHidden + set({ observerSessionsHidden: hidden }) + try { + await settingsApi.updateUser({ claudeMemObserverSessionsHidden: hidden || undefined }) + } catch { + set({ observerSessionsHidden: prev }) + } + }, })) function normalizeWebSearchSettings(settings: WebSearchSettings | undefined): WebSearchSettings {