From 1e755ff5568a4afac0309261952a55752556c6e7 Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:38:10 -0600 Subject: [PATCH] fix(desktop): keep recents sorted unless manually reordered (#45404) --- apps/desktop/src/app/chat/sidebar/index.tsx | 31 ++++++++++++++----- .../src/app/chat/sidebar/order.test.ts | 21 +++++++++++++ apps/desktop/src/app/chat/sidebar/order.ts | 17 ++++++++++ apps/desktop/src/lib/storage.test.ts | 25 +++++++++++++++ apps/desktop/src/lib/storage.ts | 6 +++- apps/desktop/src/store/layout.ts | 9 ++++++ 6 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/src/app/chat/sidebar/order.test.ts create mode 100644 apps/desktop/src/app/chat/sidebar/order.ts create mode 100644 apps/desktop/src/lib/storage.test.ts diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index 6d65c4e59a..7f46367344 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -55,6 +55,7 @@ import { $sidebarPinsOpen, $sidebarRecentsOpen, $sidebarSessionOrderIds, + $sidebarSessionOrderManual, $sidebarWorkspaceOrderIds, $sidebarWorkspaceParentOrderIds, pinSession, @@ -65,6 +66,7 @@ import { setSidebarPinsOpen, setSidebarRecentsOpen, setSidebarSessionOrderIds, + setSidebarSessionOrderManual, setSidebarWorkspaceOrderIds, setSidebarWorkspaceParentOrderIds, SIDEBAR_SESSIONS_PAGE_SIZE, @@ -99,6 +101,7 @@ import type { SidebarNavItem } from '../../types' import { SidebarCronJobsSection } from './cron-jobs-section' import { SidebarLoadMoreRow } from './load-more-row' +import { resolveManualSessionOrderIds } from './order' import { ProfileRail } from './profile-switcher' import { SidebarSessionRow } from './session-row' import { VirtualSessionList } from './virtual-session-list' @@ -354,6 +357,7 @@ export function ChatSidebar({ // otherwise be stuck in the grouped view with no way out. const showAllProfiles = multiProfile && profileScope === ALL_PROFILES const agentOrderIds = useStore($sidebarSessionOrderIds) + const agentOrderManual = useStore($sidebarSessionOrderManual) const workspaceOrderIds = useStore($sidebarWorkspaceOrderIds) const workspaceParentOrderIds = useStore($sidebarWorkspaceParentOrderIds) const [searchQuery, setSearchQuery] = useState('') @@ -517,19 +521,29 @@ export function ChatSidebar({ ) useEffect(() => { - const next = reconcileOrderIds( + const next = resolveManualSessionOrderIds( unpinnedAgentSessions.map(s => s.id), - agentOrderIds + agentOrderIds, + agentOrderManual ) - if (!sameIds(next, agentOrderIds)) { + if (!next.length && agentOrderManual) { + setSidebarSessionOrderManual(false) + } + + if (!next.length && agentOrderIds.length) { + setSidebarSessionOrderIds([]) + return + } + + if (next.length && !sameIds(next, agentOrderIds)) { setSidebarSessionOrderIds(next) } - }, [agentOrderIds, unpinnedAgentSessions]) + }, [agentOrderIds, agentOrderManual, unpinnedAgentSessions]) const agentSessions = useMemo( - () => orderByIds(unpinnedAgentSessions, s => s.id, agentOrderIds), - [unpinnedAgentSessions, agentOrderIds] + () => (agentOrderManual ? orderByIds(unpinnedAgentSessions, s => s.id, agentOrderIds) : unpinnedAgentSessions), + [unpinnedAgentSessions, agentOrderIds, agentOrderManual] ) // Recents are local-only: messaging-platform sessions are fetched as their @@ -752,7 +766,10 @@ export function ChatSidebar({ // Each reorderable list reports its OWN new id order; persisting is a direct, // typed write — no id-prefix sniffing to figure out which level moved. - const reorderSessions = (ids: string[]) => setSidebarSessionOrderIds(ids) + const reorderSessions = (ids: string[]) => { + setSidebarSessionOrderManual(true) + setSidebarSessionOrderIds(ids) + } const reorderParents = (ids: string[]) => setSidebarWorkspaceParentOrderIds(ids) diff --git a/apps/desktop/src/app/chat/sidebar/order.test.ts b/apps/desktop/src/app/chat/sidebar/order.test.ts new file mode 100644 index 0000000000..f65b08e260 --- /dev/null +++ b/apps/desktop/src/app/chat/sidebar/order.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' + +import { resolveManualSessionOrderIds } from './order' + +describe('resolveManualSessionOrderIds', () => { + it('clears legacy auto-seeded order until the user manually reorders sessions', () => { + expect(resolveManualSessionOrderIds(['newest', 'older'], ['older', 'newest'], false)).toEqual([]) + }) + + it('keeps a manual order and surfaces newly seen sessions first', () => { + expect(resolveManualSessionOrderIds(['newest', 'older', 'oldest'], ['oldest', 'older'], true)).toEqual([ + 'newest', + 'oldest', + 'older' + ]) + }) + + it('clears manual order when none of the saved ids still exist', () => { + expect(resolveManualSessionOrderIds(['newest'], ['gone'], true)).toEqual([]) + }) +}) diff --git a/apps/desktop/src/app/chat/sidebar/order.ts b/apps/desktop/src/app/chat/sidebar/order.ts new file mode 100644 index 0000000000..abe5de7c47 --- /dev/null +++ b/apps/desktop/src/app/chat/sidebar/order.ts @@ -0,0 +1,17 @@ +export function resolveManualSessionOrderIds(currentIds: string[], orderIds: string[], manual: boolean): string[] { + if (!manual || !currentIds.length || !orderIds.length) { + return [] + } + + const current = new Set(currentIds) + const retained = orderIds.filter(id => current.has(id)) + + if (!retained.length) { + return [] + } + + const retainedSet = new Set(retained) + const fresh = currentIds.filter(id => !retainedSet.has(id)) + + return [...fresh, ...retained] +} diff --git a/apps/desktop/src/lib/storage.test.ts b/apps/desktop/src/lib/storage.test.ts new file mode 100644 index 0000000000..fa74102e0e --- /dev/null +++ b/apps/desktop/src/lib/storage.test.ts @@ -0,0 +1,25 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import { persistStringArray, storedStringArray } from './storage' + +describe('string array storage', () => { + beforeEach(() => { + window.localStorage.clear() + }) + + it('removes the key for an empty array', () => { + window.localStorage.setItem('test.order', JSON.stringify(['a'])) + + persistStringArray('test.order', []) + + expect(window.localStorage.getItem('test.order')).toBeNull() + expect(storedStringArray('test.order')).toEqual([]) + }) + + it('persists non-empty arrays', () => { + persistStringArray('test.order', ['a', 'b']) + + expect(window.localStorage.getItem('test.order')).toBe(JSON.stringify(['a', 'b'])) + expect(storedStringArray('test.order')).toEqual(['a', 'b']) + }) +}) diff --git a/apps/desktop/src/lib/storage.ts b/apps/desktop/src/lib/storage.ts index 9f82ae4b8a..b04d903858 100644 --- a/apps/desktop/src/lib/storage.ts +++ b/apps/desktop/src/lib/storage.ts @@ -58,7 +58,11 @@ export function storedStringArray(key: string): string[] { export function persistStringArray(key: string, value: string[]) { try { - window.localStorage.setItem(key, JSON.stringify(value)) + if (value.length === 0) { + window.localStorage.removeItem(key) + } else { + window.localStorage.setItem(key, JSON.stringify(value)) + } } catch { // Pins are a local preference; restricted storage should not break chat. } diff --git a/apps/desktop/src/store/layout.ts b/apps/desktop/src/store/layout.ts index 46cdf0ede2..77ce4635b2 100644 --- a/apps/desktop/src/store/layout.ts +++ b/apps/desktop/src/store/layout.ts @@ -25,6 +25,7 @@ const SIDEBAR_AGENTS_GROUPED_STORAGE_KEY = 'hermes.desktop.agentsGroupedByWorksp const SIDEBAR_CRON_OPEN_STORAGE_KEY = 'hermes.desktop.sidebarCronOpen' const SIDEBAR_MESSAGING_OPEN_STORAGE_KEY = 'hermes.desktop.sidebarMessagingOpen' const SIDEBAR_SESSION_ORDER_STORAGE_KEY = 'hermes.desktop.sessionOrder' +const SIDEBAR_SESSION_ORDER_MANUAL_STORAGE_KEY = 'hermes.desktop.sessionOrder.manual' const SIDEBAR_WORKSPACE_ORDER_STORAGE_KEY = 'hermes.desktop.workspaceOrder' const SIDEBAR_WORKSPACE_PARENT_ORDER_STORAGE_KEY = 'hermes.desktop.workspaceParentOrder' const PANES_FLIPPED_STORAGE_KEY = 'hermes.desktop.panesFlipped' @@ -58,6 +59,7 @@ export const $sidebarWidth: ReadableAtom = computed($paneStates, states export const $pinnedSessionIds = atom(storedStringArray(SIDEBAR_PINNED_STORAGE_KEY)) export const $sidebarSessionOrderIds = atom(storedStringArray(SIDEBAR_SESSION_ORDER_STORAGE_KEY)) +export const $sidebarSessionOrderManual = atom(storedBoolean(SIDEBAR_SESSION_ORDER_MANUAL_STORAGE_KEY, false)) export const $sidebarWorkspaceOrderIds = atom(storedStringArray(SIDEBAR_WORKSPACE_ORDER_STORAGE_KEY)) // Order of the top-level repo "parent" groups in the worktree tree (worktrees // within a parent reuse $sidebarWorkspaceOrderIds). @@ -88,6 +90,7 @@ $pinnedSessionIds.subscribe(ids => persistStringArray(SIDEBAR_PINNED_STORAGE_KEY $sidebarCronOpen.subscribe(open => persistBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, open)) $sidebarMessagingOpenIds.subscribe(ids => persistStringArray(SIDEBAR_MESSAGING_OPEN_STORAGE_KEY, [...ids])) $sidebarSessionOrderIds.subscribe(ids => persistStringArray(SIDEBAR_SESSION_ORDER_STORAGE_KEY, [...ids])) +$sidebarSessionOrderManual.subscribe(manual => persistBoolean(SIDEBAR_SESSION_ORDER_MANUAL_STORAGE_KEY, manual)) $sidebarWorkspaceOrderIds.subscribe(ids => persistStringArray(SIDEBAR_WORKSPACE_ORDER_STORAGE_KEY, [...ids])) $sidebarWorkspaceParentOrderIds.subscribe(ids => persistStringArray(SIDEBAR_WORKSPACE_PARENT_ORDER_STORAGE_KEY, [...ids]) @@ -170,6 +173,12 @@ export function setSidebarSessionOrderIds(ids: string[]) { } } +export function setSidebarSessionOrderManual(manual: boolean) { + if ($sidebarSessionOrderManual.get() !== manual) { + $sidebarSessionOrderManual.set(manual) + } +} + export function setSidebarWorkspaceOrderIds(ids: string[]) { if (!arraysEqual($sidebarWorkspaceOrderIds.get(), ids)) { $sidebarWorkspaceOrderIds.set(ids)