diff --git a/apps/desktop/src/lib/session-signatures.test.ts b/apps/desktop/src/lib/session-signatures.test.ts index aeecd19b61..0c4c280c8e 100644 --- a/apps/desktop/src/lib/session-signatures.test.ts +++ b/apps/desktop/src/lib/session-signatures.test.ts @@ -4,7 +4,8 @@ import type { SessionInfo } from '@/hermes' import { sameCronSignature, sessionMessagesSignature } from './session-signatures' -const session = (id: string, title: string | null): SessionInfo => ({ id, title }) as SessionInfo +const session = (id: string, title: string | null, extra: Partial = {}): SessionInfo => + ({ id, title, ...extra }) as SessionInfo describe('sameCronSignature', () => { it('is false when the lengths differ', () => { @@ -28,6 +29,28 @@ describe('sameCronSignature', () => { const b = [session('b', 't'), session('a', 't')] expect(sameCronSignature(a, b)).toBe(false) }) + + // A pin-only page must reach $sessions: session-pin-sync treats the row as + // authoritative and releases its write guard when a page confirms the value + // it wrote. Gating that page out froze the row and re-pinned what the user + // had just unpinned (#76919). + it('is false when only the pinned flag changed', () => { + const a = [session('a', 't', { pinned: true })] + const b = [session('a', 't', { pinned: false })] + expect(sameCronSignature(a, b)).toBe(false) + }) + + it('is false when only the archived flag changed', () => { + const a = [session('a', 't', { archived: false })] + const b = [session('a', 't', { archived: true })] + expect(sameCronSignature(a, b)).toBe(false) + }) + + it('is true when both flags match', () => { + const a = [session('a', 't', { archived: false, pinned: true })] + const b = [session('a', 't', { archived: false, pinned: true })] + expect(sameCronSignature(a, b)).toBe(true) + }) }) describe('sessionMessagesSignature', () => { diff --git a/apps/desktop/src/lib/session-signatures.ts b/apps/desktop/src/lib/session-signatures.ts index 4ef20e1fab..fac4725422 100644 --- a/apps/desktop/src/lib/session-signatures.ts +++ b/apps/desktop/src/lib/session-signatures.ts @@ -23,7 +23,14 @@ export function sameCronSignature(a: SessionInfo[], b: SessionInfo[]): boolean { session.preview === other.preview && session.message_count === other.message_count && session.last_active === other.last_active && - session.ended_at === other.ended_at + session.ended_at === other.ended_at && + // Row STATE, not just row content: session-pin-sync reconciles the + // sidebar's pins against `pinned` on the rows in this atom, so a page + // whose only delta is a flag has to swap in or the reconciler reads a + // frozen copy forever. An idle conversation never moves any of the + // fields above again, which is exactly when a pin gets toggled (#76919). + session.pinned === other.pinned && + session.archived === other.archived ) }) }