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
38 changes: 38 additions & 0 deletions apps/desktop/src/app/chat/sidebar/sessions-section-order.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, it } from 'vitest'

import type { SessionInfo } from '@/types/hermes'

import { sessionEntriesForSection } from './sessions-section-order'

const session = (id: string, lastActive: number): SessionInfo =>
({
ended_at: null,
id,
input_tokens: 0,
is_active: false,
last_active: lastActive,
message_count: 1,
model: null,
output_tokens: 0,
preview: null,
source: 'desktop',
started_at: lastActive,
title: id,
tool_call_count: 0
}) as SessionInfo

describe('sessionEntriesForSection', () => {
it('preserves the persisted user order for pinned sessions', () => {
const older = session('older', 1)
const newer = session('newer', 2)

expect(sessionEntriesForSection([older, newer], true).map(entry => entry.session.id)).toEqual(['older', 'newer'])
})

it('keeps the activity ordering used by regular session lists', () => {
const older = session('older', 1)
const newer = session('newer', 2)

expect(sessionEntriesForSection([older, newer], false).map(entry => entry.session.id)).toEqual(['newer', 'older'])
})
})
8 changes: 8 additions & 0 deletions apps/desktop/src/app/chat/sidebar/sessions-section-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { SessionInfo } from '@/hermes'
import { flattenSessionsWithBranches, type SidebarSessionEntry } from '@/lib/session-branch-tree'

export function sessionEntriesForSection(sessions: readonly SessionInfo[], pinned: boolean): SidebarSessionEntry[] {
// Pins are already in the user's persisted order. The branch flattener sorts
// top-level groups by activity, which would undo a successful drag reorder.
return pinned ? sessions.map(session => ({ session })) : flattenSessionsWithBranches(sessions)
}
3 changes: 2 additions & 1 deletion apps/desktop/src/app/chat/sidebar/sessions-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { ReorderableList, useSortableBindings } from './reorderable-list'
import { SidebarSessionSkeletons } from './section-states'
import { SidebarSessionRow } from './session-row'
import { sessionEntriesForSection } from './sessions-section-order'
import { VirtualSessionList } from './virtual-session-list'

export const VIRTUALIZE_THRESHOLD = 25
Expand Down Expand Up @@ -189,7 +190,7 @@ export function SidebarSessionsSection({
// The flat recents/pinned list is the only place sessions reorder by hand;
// grouped/tree views always sort by creation date and never drag.
const sessionsDraggable = sortable && !!onReorderSessions
const displayEntries = useMemo(() => flattenSessionsWithBranches(sessions), [sessions])
const displayEntries = useMemo(() => sessionEntriesForSection(sessions, pinned), [pinned, sessions])

const renderRow = (session: SessionInfo, draggable: boolean, branchStem?: string) => {
const rowProps = {
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/store/layout-pinned-order.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'

import { reorderedPinnedSessionIds } from './layout'

describe('reorderedPinnedSessionIds', () => {
it('persists an exact visible reorder', () => {
expect(reorderedPinnedSessionIds(['a', 'b', 'c'], ['c', 'a', 'b'])).toEqual(['c', 'a', 'b'])
})

it('reorders visible pins while preserving stale or unresolved durable pins', () => {
expect(reorderedPinnedSessionIds(['a', 'stale', 'b', 'c'], ['c', 'a', 'b'])).toEqual(['c', 'a', 'b', 'stale'])
})

it('ignores duplicate and unpinned ids from the rendered order', () => {
expect(reorderedPinnedSessionIds(['a', 'b'], ['b', 'b', 'not-pinned', 'a'])).toEqual(['b', 'a'])
})
})
16 changes: 13 additions & 3 deletions apps/desktop/src/store/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,22 @@ export function unpinSession(sessionId: string) {
// Replace the whole pinned order at once (drag-reorder hands back the new order
// rather than a single move). Keep only ids that are actually pinned so a stale
// row can't smuggle an unpinned id into the store.
export function reorderedPinnedSessionIds(prev: string[], ids: string[]): string[] {
const pinned = new Set(prev)
const seen = new Set<string>()
const visibleOrder = ids.filter(id => pinned.has(id) && !seen.has(id) && Boolean(seen.add(id)))

// The rendered list can contain fewer rows than the durable pin store after
// compression/lineage dedup or when an old pin is temporarily unresolved.
// Keep those hidden ids, but do not let them veto reordering the visible rows.
return [...visibleOrder, ...prev.filter(id => !seen.has(id))]
}

export function setPinnedSessionOrder(ids: string[]) {
const prev = $pinnedSessionIds.get()
const pinned = new Set(prev)
const next = ids.filter(id => pinned.has(id))
const next = reorderedPinnedSessionIds(prev, ids)

if (next.length === prev.length && !arraysEqual(prev, next)) {
if (!arraysEqual(prev, next)) {
$pinnedSessionIds.set(next)
}
}
Expand Down