From 29ba68859afc9c42a114dbb149150741ddc2ebb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E7=B6=A0BG?= Date: Wed, 10 Jun 2026 00:08:00 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20fix(desktop):=20filter=20mes?= =?UTF-8?q?saging=20sidebar=20by=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/app/chat/sidebar/index.tsx | 12 +++-- .../app/chat/sidebar/profile-scope.test.ts | 47 +++++++++++++++++++ .../src/app/chat/sidebar/profile-scope.ts | 16 +++++++ apps/desktop/src/app/desktop-controller.tsx | 35 ++++++++++---- 4 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/src/app/chat/sidebar/profile-scope.test.ts create mode 100644 apps/desktop/src/app/chat/sidebar/profile-scope.ts diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index 6770234d853a..6393f32ade10 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -93,6 +93,7 @@ import { SidebarPanelLabel } from '../../shell/sidebar-label' import type { SidebarNavItem } from '../../types' import { SidebarCronJobsSection } from './cron-jobs-section' +import { filterSessionsByProfileScope } from './profile-scope' import { ProfileRail } from './profile-switcher' import { SidebarSessionRow } from './session-row' import { VirtualSessionList } from './virtual-session-list' @@ -508,6 +509,11 @@ export function ChatSidebar({ [agentSessions, s.noWorkspace, workspaceOrderIds] ) + const visibleMessagingSessions = useMemo( + () => filterSessionsByProfileScope(messagingSessions, profileScope, showAllProfiles), + [messagingSessions, profileScope, showAllProfiles] + ) + const loadMoreForProfileGroup = useCallback( (profile: string) => { if (!onLoadMoreProfileSessions) { @@ -543,13 +549,13 @@ export function ChatSidebar({ // within a platform by recency. Per-platform totals (when a "load more" has // resolved them) drive the count + whether more remain on disk. const messagingGroups = useMemo(() => { - if (!messagingSessions.length) { + if (!visibleMessagingSessions.length) { return [] } const bySource = new Map() - for (const session of messagingSessions) { + for (const session of visibleMessagingSessions) { const sourceId = normalizeSessionSource(session.source) if (!sourceId) { @@ -579,7 +585,7 @@ export function ChatSidebar({ } }) .sort((a, b) => sessionTime(b.sessions[0]) - sessionTime(a.sessions[0])) - }, [messagingSessions, messagingPlatformTotals, messagingTruncated]) + }, [visibleMessagingSessions, messagingPlatformTotals, messagingTruncated]) // ALL-profiles view: one collapsible group per profile, color on the header // (not on every row). Default profile floats to the top, the rest alpha. diff --git a/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts b/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts new file mode 100644 index 000000000000..8f7236e225df --- /dev/null +++ b/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' + +import type { SessionInfo } from '@/types/hermes' + +import { filterSessionsByProfileScope } from './profile-scope' + +const session = (id: string, profile?: string): SessionInfo => ({ + archived: false, + cwd: null, + ended_at: null, + id, + input_tokens: 0, + is_active: false, + last_active: 1, + message_count: 1, + model: null, + output_tokens: 0, + preview: null, + profile, + source: 'feishu', + started_at: 1, + title: null, + tool_call_count: 0 +}) + +describe('filterSessionsByProfileScope', () => { + it('keeps only messaging rows from the selected profile', () => { + const rows = [session('default-row', 'default'), session('research-row', 'research')] + + expect(filterSessionsByProfileScope(rows, 'research', false).map(s => s.id)).toEqual(['research-row']) + }) + + it('treats missing profiles as default', () => { + const rows = [session('legacy-row'), session('research-row', 'research')] + + expect(filterSessionsByProfileScope(rows, 'default', false).map(s => s.id)).toEqual(['legacy-row']) + }) + + it('keeps every messaging row in All profiles mode', () => { + const rows = [session('default-row', 'default'), session('research-row', 'research')] + + expect(filterSessionsByProfileScope(rows, 'research', true).map(s => s.id)).toEqual([ + 'default-row', + 'research-row' + ]) + }) +}) diff --git a/apps/desktop/src/app/chat/sidebar/profile-scope.ts b/apps/desktop/src/app/chat/sidebar/profile-scope.ts new file mode 100644 index 000000000000..10a2b34dc08a --- /dev/null +++ b/apps/desktop/src/app/chat/sidebar/profile-scope.ts @@ -0,0 +1,16 @@ +import type { SessionInfo } from '@/hermes' +import { normalizeProfileKey } from '@/store/profile' + +export function filterSessionsByProfileScope( + sessions: SessionInfo[], + profileScope: string, + showAllProfiles: boolean +): SessionInfo[] { + if (showAllProfiles) { + return sessions + } + + const scope = normalizeProfileKey(profileScope) + + return sessions.filter(session => normalizeProfileKey(session.profile) === scope) +} diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 4444f524a06f..fb46c8e63f82 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -50,9 +50,9 @@ import { $currentCwd, $freshDraftReady, $gatewayState, + $messagingSessions, $selectedStoredSessionId, $sessions, - $messagingSessions, $workingSessionIds, CRON_SECTION_LIMIT, getRecentlySettledSessionIds, @@ -305,8 +305,12 @@ export function DesktopController() { // competes with local chats for the recents page budget. One combined fetch // seeds every platform; the sidebar splits the rows per source. const refreshMessagingSessions = useCallback(async () => { + setMessagingPlatformTotals({}) + try { - const result = await listAllProfileSessions(MESSAGING_SECTION_LIMIT, 1, 'exclude', 'recent', 'all', { + const sessionProfile = profileScope === ALL_PROFILES ? 'all' : profileScope + + const result = await listAllProfileSessions(MESSAGING_SECTION_LIMIT, 1, 'exclude', 'recent', sessionProfile, { excludeSources: MESSAGING_EXCLUDED_SOURCES }) @@ -321,20 +325,33 @@ export function DesktopController() { } catch { // Non-fatal: the messaging sections just stay empty/stale. } - }, []) + }, [profileScope]) // Page a single platform's section independently (mirrors the per-profile // pager): fetch that source's next window and merge it back in place, leaving // every other platform's rows untouched. Resolves the platform's exact total. const loadMoreMessagingForPlatform = useCallback(async (platform: string) => { - const inPlatform = (s: SessionInfo) => normalizeSessionSource(s.source) === platform + const sessionProfile = profileScope === ALL_PROFILES ? 'all' : profileScope + + const inProfileScope = (s: SessionInfo) => + sessionProfile === 'all' || normalizeProfileKey(s.profile) === sessionProfile + + const inPlatform = (s: SessionInfo) => normalizeSessionSource(s.source) === platform && inProfileScope(s) + const loaded = $messagingSessions.get().filter(inPlatform).length - const result = await listAllProfileSessions(loaded + SIDEBAR_SESSIONS_PAGE_SIZE, 1, 'exclude', 'recent', 'all', { - source: platform - }) + const result = await listAllProfileSessions( + loaded + SIDEBAR_SESSIONS_PAGE_SIZE, + 1, + 'exclude', + 'recent', + sessionProfile, + { + source: platform + } + ) - const incoming = result.sessions.filter(s => normalizeSessionSource(s.source) === platform) + const incoming = result.sessions.filter(inPlatform) setMessagingSessions(prev => [ ...prev.filter(s => !inPlatform(s)), @@ -343,7 +360,7 @@ export function DesktopController() { const total = result.total ?? incoming.length setMessagingPlatformTotals(prev => ({ ...prev, [platform]: Math.max(total, incoming.length) })) - }, []) + }, [profileScope]) // Cron *jobs* drive the sidebar "Cron jobs" section. Jobs are created // synchronously (agent tool call or the cron UI), so refreshing here right From c35a935ef251387d400f4521094f759174020fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E7=B6=A0BG?= Date: Tue, 14 Jul 2026 21:47:50 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20fix(desktop):=20preserve=20m?= =?UTF-8?q?essaging=20rows=20in=20all-profile=20scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/app/chat/sidebar/index.tsx | 4 ++-- .../desktop/src/app/chat/sidebar/profile-scope.test.ts | 9 +++++---- apps/desktop/src/app/chat/sidebar/profile-scope.ts | 10 +++------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index 00665cd56e29..0a6df7adad87 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -794,8 +794,8 @@ export function ChatSidebar({ ) const visibleMessagingSessions = useMemo( - () => filterSessionsByProfileScope(messagingSessions, profileScope, showAllProfiles), - [messagingSessions, profileScope, showAllProfiles] + () => filterSessionsByProfileScope(messagingSessions, profileScope), + [messagingSessions, profileScope] ) // Reveal another batch of a platform's rows; fetch from the backend too if we diff --git a/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts b/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts index 8f7236e225df..ec4345eab561 100644 --- a/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts +++ b/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' +import { ALL_PROFILES } from '@/store/profile' import type { SessionInfo } from '@/types/hermes' import { filterSessionsByProfileScope } from './profile-scope' @@ -27,19 +28,19 @@ describe('filterSessionsByProfileScope', () => { it('keeps only messaging rows from the selected profile', () => { const rows = [session('default-row', 'default'), session('research-row', 'research')] - expect(filterSessionsByProfileScope(rows, 'research', false).map(s => s.id)).toEqual(['research-row']) + expect(filterSessionsByProfileScope(rows, 'research').map(s => s.id)).toEqual(['research-row']) }) it('treats missing profiles as default', () => { const rows = [session('legacy-row'), session('research-row', 'research')] - expect(filterSessionsByProfileScope(rows, 'default', false).map(s => s.id)).toEqual(['legacy-row']) + expect(filterSessionsByProfileScope(rows, 'default').map(s => s.id)).toEqual(['legacy-row']) }) - it('keeps every messaging row in All profiles mode', () => { + it('keeps every messaging row when persisted All profiles mode outlives the profile switcher', () => { const rows = [session('default-row', 'default'), session('research-row', 'research')] - expect(filterSessionsByProfileScope(rows, 'research', true).map(s => s.id)).toEqual([ + expect(filterSessionsByProfileScope(rows, ALL_PROFILES).map(s => s.id)).toEqual([ 'default-row', 'research-row' ]) diff --git a/apps/desktop/src/app/chat/sidebar/profile-scope.ts b/apps/desktop/src/app/chat/sidebar/profile-scope.ts index 937c5fa17b16..1e59fe37436b 100644 --- a/apps/desktop/src/app/chat/sidebar/profile-scope.ts +++ b/apps/desktop/src/app/chat/sidebar/profile-scope.ts @@ -1,12 +1,8 @@ -import { normalizeProfileKey } from '@/store/profile' +import { ALL_PROFILES, normalizeProfileKey } from '@/store/profile' import type { SessionInfo } from '@/types/hermes' -export function filterSessionsByProfileScope( - sessions: SessionInfo[], - profileScope: string, - showAllProfiles: boolean -): SessionInfo[] { - if (showAllProfiles) { +export function filterSessionsByProfileScope(sessions: SessionInfo[], profileScope: string): SessionInfo[] { + if (profileScope === ALL_PROFILES) { return sessions } From c3efe9ba740a8534f1418c6e3450f52fcd98e519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E7=B6=A0BG?= Date: Sat, 18 Jul 2026 20:03:43 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20fix(desktop):=20ignore=20sta?= =?UTF-8?q?le=20messaging=20refresh=20callbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/use-session-list-actions.test.tsx | 104 ++++++++++++++++++ .../session/hooks/use-session-list-actions.ts | 8 +- 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/app/session/hooks/use-session-list-actions.test.tsx diff --git a/apps/desktop/src/app/session/hooks/use-session-list-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-session-list-actions.test.tsx new file mode 100644 index 000000000000..84e6e55a8b53 --- /dev/null +++ b/apps/desktop/src/app/session/hooks/use-session-list-actions.test.tsx @@ -0,0 +1,104 @@ +import { act, cleanup, renderHook } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { listAllProfileSessions, type SessionInfo } from '@/hermes' +import { + $messagingPlatformTotals, + $messagingSessions, + setMessagingPlatformTotals, + setMessagingSessions, + setMessagingTruncated +} from '@/store/session' + +import { useSessionListActions } from './use-session-list-actions' + +vi.mock('@/hermes', async importOriginal => ({ + ...(await importOriginal>()), + getCronJobs: vi.fn(), + listAllProfileSessions: vi.fn() +})) + +interface Deferred { + promise: Promise + resolve: (value: T) => void +} + +function deferred(): Deferred { + let resolve!: (value: T) => void + + const promise = new Promise(done => { + resolve = done + }) + + return { promise, resolve } +} + +function session(id: string, profile: string): SessionInfo { + return { + archived: false, + cwd: null, + ended_at: null, + id, + input_tokens: 0, + is_active: false, + last_active: 1, + message_count: 1, + model: null, + output_tokens: 0, + preview: null, + profile, + source: 'telegram', + started_at: 1, + title: null, + tool_call_count: 0 + } +} + +describe('useSessionListActions', () => { + beforeEach(() => { + vi.clearAllMocks() + setMessagingSessions([]) + setMessagingPlatformTotals({ telegram: 3 }) + setMessagingTruncated(false) + }) + + afterEach(() => { + cleanup() + setMessagingSessions([]) + setMessagingPlatformTotals({}) + setMessagingTruncated(false) + }) + + it('ignores a stale scope callback before it can invalidate the current messaging request', async () => { + const alpha = deferred>>() + const beta = deferred>>() + const alphaRow = session('alpha-row', 'alpha') + const betaRow = session('beta-row', 'beta') + + vi.mocked(listAllProfileSessions).mockImplementation((_limit, _min, _archived, _order, profile) => + profile === 'alpha' ? alpha.promise : beta.promise + ) + + const { rerender, result } = renderHook(({ profileScope }) => useSessionListActions({ profileScope }), { + initialProps: { profileScope: 'alpha' } + }) + + const staleAlphaRefresh = result.current.refreshMessagingSessions + + rerender({ profileScope: 'beta' }) + + const betaRefresh = result.current.refreshMessagingSessions() + const staleAlphaRequest = staleAlphaRefresh() + + await act(async () => { + beta.resolve({ limit: 100, offset: 0, sessions: [betaRow], total: 1 }) + await betaRefresh + alpha.resolve({ limit: 100, offset: 0, sessions: [alphaRow], total: 1 }) + await staleAlphaRequest + }) + + expect(vi.mocked(listAllProfileSessions).mock.calls.map(call => call[4])).toEqual(['beta']) + expect($messagingSessions.get()).toEqual([betaRow]) + expect($messagingPlatformTotals.get()).toEqual({}) + }) +}) diff --git a/apps/desktop/src/app/session/hooks/use-session-list-actions.ts b/apps/desktop/src/app/session/hooks/use-session-list-actions.ts index ac087b83c3f4..f06afb1c3bbd 100644 --- a/apps/desktop/src/app/session/hooks/use-session-list-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-session-list-actions.ts @@ -104,9 +104,14 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg // competes with local chats for the recents page budget. One combined fetch // seeds every platform; the sidebar splits the rows per source. const refreshMessagingSessions = useCallback(async () => { + const sessionProfile = sessionProfileForScope(profileScope) + + if (sessionProfileForScope(profileScopeRef.current) !== sessionProfile) { + return + } + const requestId = refreshMessagingSessionsRequestRef.current + 1 refreshMessagingSessionsRequestRef.current = requestId - const sessionProfile = sessionProfileForScope(profileScope) setMessagingPlatformTotals({}) @@ -143,6 +148,7 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg const inProfileScope = (s: SessionInfo) => sessionProfile === 'all' || normalizeProfileKey(s.profile) === sessionProfile + const inPlatform = (s: SessionInfo) => normalizeSessionSource(s.source) === platform && inProfileScope(s) const loaded = $messagingSessions.get().filter(inPlatform).length