From fd9fc50dd2ca89acb3e7b57ff3ebf3e80fd52af4 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 6 Aug 2026 21:11:10 -0500 Subject: [PATCH] fix(desktop): stop dropping pinned sessions past the page limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list endpoints deliberately back-fill pinned conversations past their LIMIT, then the client sliced the response back down to that same limit and threw them away — so only pins that happened to land inside the most recent page ever rendered, which reads as a cap on how many sessions you can pin. Keep the back-filled rows when trimming, and discount them from the "window came back full" test that drives Load more. Counting a back-fill as a loaded row invented a page that could never be fetched, leaving a Load more button that refetched the same rows forever. --- .../session/hooks/use-session-list-actions.ts | 8 +++-- apps/desktop/src/hermes.ts | 30 ++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-session-list-actions.ts b/apps/desktop/src/app/session/hooks/use-session-list-actions.ts index d7ab9ea0914c0..596a336f4e316 100644 --- a/apps/desktop/src/app/session/hooks/use-session-list-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-session-list-actions.ts @@ -262,8 +262,12 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg ...mergeSessionPage(prev.filter(inKey), result.sessions, keep) ]) - // A full window back means the profile still has more on disk. - const truncated = result.sessions.length >= loaded + SIDEBAR_SESSIONS_PAGE_SIZE + // A full window back means the profile still has more on disk — but pinned + // rows arrive as a back-fill PAST the limit, so counting them fakes a full + // page and the "Load more" never goes away (it re-fetches the same rows + // forever). Only unpinned rows count toward the window. + const unpinned = result.sessions.filter(s => !s.pinned).length + const truncated = unpinned >= loaded + SIDEBAR_SESSIONS_PAGE_SIZE setSessionProfilesTruncated(prev => ({ ...prev, [key]: truncated })) }, []) diff --git a/apps/desktop/src/hermes.ts b/apps/desktop/src/hermes.ts index f33d05226c106..b931fe8678f5e 100644 --- a/apps/desktop/src/hermes.ts +++ b/apps/desktop/src/hermes.ts @@ -370,6 +370,26 @@ export function pluginSocket(pluginId: string, path: string, onMessage: (data: u } } +/** + * Trim a page to its window WITHOUT discarding pinned rows. + * + * The list endpoints deliberately back-fill pinned conversations past their + * LIMIT — a pin means "always reachable", so an aged-out pinned chat is + * appended after the recency window. A plain `slice(0, limit)` throws exactly + * those rows away again, which is why pins silently stopped rendering past + * some count: the sidebar could only ever show the pins that happened to fall + * inside the most-recent page. + */ +function pageWindow(sessions: SessionInfo[], limit: number): SessionInfo[] { + if (sessions.length <= limit) { + return sessions + } + + const recent = sessions.slice(0, limit) + + return [...recent, ...sessions.slice(limit).filter(session => session.pinned)] +} + export async function listSessions( limit = 40, minMessages = 0, @@ -385,7 +405,7 @@ export async function listSessions( return { ...result, - sessions: result.sessions.slice(0, limit), + sessions: pageWindow(result.sessions, limit), offset: 0 } } @@ -426,7 +446,7 @@ export async function listAllProfileSessions( return { ...result, - sessions: result.sessions.slice(0, limit), + sessions: pageWindow(result.sessions, limit), offset: 0 } } @@ -445,14 +465,16 @@ export interface SidebarSessionSlice { /** Which profiles filled their per-profile window in a returned page. The * legacy per-slice endpoint doesn't report this, so derive it from the rows: - * a profile at (or over) the cap still has more on disk. */ + * a profile at (or over) the cap still has more on disk. Pinned rows are + * discounted — they're back-filled past the LIMIT, so counting them fakes a + * full page and leaves a "Load more" that can never resolve. */ function profilesTruncatedFrom(sessions: SessionInfo[], cap: number): Record { const counts = new Map() for (const session of sessions) { const key = session.profile || 'default' - counts.set(key, (counts.get(key) ?? 0) + 1) + counts.set(key, (counts.get(key) ?? 0) + (session.pinned ? 0 : 1)) } return Object.fromEntries([...counts].map(([name, count]) => [name, count >= cap]))