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
85 changes: 85 additions & 0 deletions desktop/src/components/layout/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<ReturnType<typeof useUIStore.getState>>)
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(<Sidebar />)

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(<Sidebar />)

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(<Sidebar />)

expect(screen.queryByText('Hello memory agent')).not.toBeInTheDocument()
expect(screen.getByText('No sessions')).toBeInTheDocument()
})
})
12 changes: 11 additions & 1 deletion desktop/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
38 changes: 38 additions & 0 deletions desktop/src/components/plugins/PluginDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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<string | null>(null)
const [showUninstallDialog, setShowUninstallDialog] = useState(false)
Expand Down Expand Up @@ -309,6 +312,41 @@ export function PluginDetail() {
</p>
</section>

{selectedPlugin.id === 'claude-mem@thedotmack' && (
<section className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-5 py-4">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
aria-label="隐藏 observer 会话标签页"
checked={observerSessionsHidden}
onChange={(e) => void setObserverSessionsHidden(e.target.checked)}
className="sr-only"
/>
<div
className={`mt-0.5 flex h-5 w-9 shrink-0 items-center rounded-full p-0.5 transition-colors ${
observerSessionsHidden
? 'bg-[var(--color-brand)]'
: 'bg-[var(--color-border)]'
}`}
>
<div
className={`h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
observerSessionsHidden ? 'translate-x-4' : ''
}`}
/>
</div>
<div className="min-w-0">
<div className="text-sm font-medium text-[var(--color-text-primary)]">
隐藏 observer 会话标签页
</div>
<div className="text-xs text-[var(--color-text-tertiary)] mt-1 leading-5">
claude-mem 在后台静默观察主会话并生成进度摘要,这些观察会话不需要在侧边栏中显示
</div>
</div>
</label>
</section>
)}

{selectedPlugin.errors.length > 0 && (
<section className="rounded-2xl border border-[var(--color-error)]/20 bg-[var(--color-error)]/6 px-5 py-4">
<div className="flex items-center gap-2 mb-3">
Expand Down
105 changes: 105 additions & 0 deletions desktop/src/stores/settingsStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
})
})
14 changes: 14 additions & 0 deletions desktop/src/stores/settingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type SettingsStore = {
h5AccessError: string | null
responseLanguage: string
uiZoom: number
observerSessionsHidden: boolean
isLoading: boolean
error: string | null

Expand All @@ -70,6 +71,7 @@ type SettingsStore = {
}) => Promise<void>
setResponseLanguage: (language: string) => Promise<void>
setUiZoom: (zoom: number) => void
setObserverSessionsHidden: (hidden: boolean) => Promise<void>
}

const DEFAULT_H5_ACCESS_SETTINGS: H5AccessSettings = {
Expand All @@ -95,6 +97,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
h5AccessError: null,
responseLanguage: '',
uiZoom: readStoredAppZoomLevel(),
observerSessionsHidden: false,
isLoading: false,
error: null,

Expand Down Expand Up @@ -132,6 +135,7 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
h5Access: h5AccessResult.settings,
h5AccessError: h5AccessResult.error,
responseLanguage: typeof userSettings.language === 'string' ? userSettings.language : '',
observerSessionsHidden: userSettings.claudeMemObserverSessionsHidden === true,
isLoading: false,
error: null,
})
Expand Down Expand Up @@ -310,6 +314,16 @@ export const useSettingsStore = create<SettingsStore>((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 {
Expand Down
Loading