fix(desktop): keep recents sorted unless manually reordered (#45404)

This commit is contained in:
Gille 2026-06-12 23:38:10 -06:00 committed by GitHub
parent 7d183f6497
commit 1e755ff556
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 101 additions and 8 deletions

View File

@ -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)

View File

@ -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([])
})
})

View File

@ -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]
}

View File

@ -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'])
})
})

View File

@ -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.
}

View File

@ -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<number> = 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)