fix(desktop): keep recents sorted unless manually reordered (#45404)
This commit is contained in:
parent
7d183f6497
commit
1e755ff556
@ -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)
|
||||
|
||||
|
||||
21
apps/desktop/src/app/chat/sidebar/order.test.ts
Normal file
21
apps/desktop/src/app/chat/sidebar/order.test.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
17
apps/desktop/src/app/chat/sidebar/order.ts
Normal file
17
apps/desktop/src/app/chat/sidebar/order.ts
Normal 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]
|
||||
}
|
||||
25
apps/desktop/src/lib/storage.test.ts
Normal file
25
apps/desktop/src/lib/storage.test.ts
Normal 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'])
|
||||
})
|
||||
})
|
||||
@ -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.
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user