Merge pull request #72514 from NousResearch/bb/desktop-pinned-session-order

fix(desktop): keep pinned sidebar rows in user order
This commit is contained in:
brooklyn! 2026-07-27 01:45:07 -05:00 committed by GitHub
commit 9f0e62c5c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 5 deletions

View file

@ -205,7 +205,15 @@ 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])
// Pinned and manual-drag lists pass sessions already in the caller's order.
// Default recents stay dateGrouped and still re-sort roots by group recency
// so partition buckets stay truthful — but NEVER for pins, where a turn
// finishing was floating background tasks over the user's fixed ranking.
const preserveInputOrder = pinned || (sessionsDraggable && !dateGrouped)
const displayEntries = useMemo(
() => flattenSessionsWithBranches(sessions, { preserveOrder: preserveInputOrder }),
[sessions, preserveInputOrder]
)
const renderRow = (session: SessionInfo, draggable: boolean, branchStem?: string) => {
const rowProps = {

View file

@ -50,4 +50,34 @@ describe('flattenSessionsWithBranches', () => {
expect(flattenSessionsWithBranches([branch])).toEqual([{ session: branch }])
})
it('re-sorts roots by group recency by default (pinned-style jumps without preserveOrder)', () => {
// Stale important chat first in the caller's array; a recently-active
// background task second. Default path must lift the fresher root — that
// is what was scrambling the Pinned section before preserveOrder.
const important = session('important', { last_active: 10 })
const background = session('background', { last_active: 99 })
expect(flattenSessionsWithBranches([important, background]).map(e => e.session.id)).toEqual([
'background',
'important'
])
})
it("preserveOrder keeps the caller's root order even when activity is newer lower down", () => {
const important = session('important', { last_active: 10 })
const background = session('background', { last_active: 99 })
const branch = session('branch', { last_active: 50, parent_session_id: 'important' })
expect(
flattenSessionsWithBranches([important, background, branch], { preserveOrder: true }).map(e => ({
id: e.session.id,
stem: e.branchStem
}))
).toEqual([
{ id: 'important', stem: undefined },
{ id: 'branch', stem: '└─ ' },
{ id: 'background', stem: undefined }
])
})
})

View file

@ -5,10 +5,23 @@ export interface SidebarSessionEntry {
session: SessionInfo
}
export interface FlattenSessionsOptions {
/**
* Keep the input root order instead of re-sorting by group recency.
* Use for hand-ordered surfaces (pinned ids, manual recents drag) so a
* turn completing can't float a row. Branch children still nest under
* their parent; sibling branches stay ordered by their own recency.
*/
preserveOrder?: boolean
}
const recency = (session: SessionInfo): number => session.last_active || session.started_at || 0
/** Flat list with branch/fork sessions nested visually under their parent. */
export function flattenSessionsWithBranches(sessions: readonly SessionInfo[]): SidebarSessionEntry[] {
export function flattenSessionsWithBranches(
sessions: readonly SessionInfo[],
options: FlattenSessionsOptions = {}
): SidebarSessionEntry[] {
if (sessions.length < 2) {
return sessions.map(session => ({ session }))
}
@ -53,6 +66,7 @@ export function flattenSessionsWithBranches(sessions: readonly SessionInfo[]): S
// A group sorts by its freshest member, so activity on any branch lifts the
// whole parent→branches cluster together instead of stranding the parent at
// its own stale timestamp. Memoized — each subtree is folded at most once.
// Skipped when preserveOrder is set: the caller already chose positions.
const groupRecencyMemo = new Map<string, number>()
const groupRecency = (session: SessionInfo): number => {
@ -92,11 +106,15 @@ export function flattenSessionsWithBranches(sessions: readonly SessionInfo[]): S
children?.forEach((child, index) => emit(child, index === children.length - 1 ? '└─ ' : '├─ '))
}
sessions
const roots = sessions
.filter(session => !nestedIds.has(session.id))
.map((session, index) => ({ index, session }))
.sort((a, b) => groupRecency(b.session) - groupRecency(a.session) || a.index - b.index)
.forEach(({ session }) => emit(session))
if (!options.preserveOrder) {
roots.sort((a, b) => groupRecency(b.session) - groupRecency(a.session) || a.index - b.index)
}
roots.forEach(({ session }) => emit(session))
for (const session of sessions) {
if (!seen.has(session.id)) {