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