From 0a2e8edf9720fa877648eed5cd2be73668897dc5 Mon Sep 17 00:00:00 2001 From: Hyunwoo Jung Date: Mon, 15 Jun 2026 14:12:19 +0900 Subject: [PATCH] fix(app): keep the unread session indicator stable during polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `buildSessionListViewData` bakes each row's `hasUnread` flag into a snapshot, reading it from an *optional* `unreadSessionIds` set. `applySessions` passes the set, but several other reducers rebuild the snapshot without it — most importantly `applyMachines`, which fires on every machine heartbeat / `fetchMachines` poll. On each such rebuild `hasUnread` falls back to `undefined?.has(id) ?? false`, so every row loses its unread state until the next `applySessions` restores it. The result is a distracting flicker of the unread indicator (bold title on the bold-title builds, the status dot/text on main) on the polling cadence, with no user action involved. Make `unreadSessionIds` a required parameter on `buildSessionRowData` / `buildSessionListViewData` so any reducer that forgets it is a compile error, and thread `state.unreadSessionIds` through the call sites that dropped it (`updateSessionDraft`, `applyMachines`, `deleteMachine`, `deleteSession`). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- packages/happy-app/sources/sync/storage.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/happy-app/sources/sync/storage.ts b/packages/happy-app/sources/sync/storage.ts index 42d554b6b1..be735d0056 100644 --- a/packages/happy-app/sources/sync/storage.ts +++ b/packages/happy-app/sources/sync/storage.ts @@ -95,7 +95,7 @@ export interface SessionRowData { hasUnread: boolean; } -function buildSessionRowData(session: Session, unreadSessionIds?: Set): SessionRowData { +function buildSessionRowData(session: Session, unreadSessionIds: Set): SessionRowData { const isOnline = session.presence === "online"; const hasPermissions = !!(session.agentState?.requests && Object.keys(session.agentState.requests).length > 0); @@ -125,7 +125,7 @@ function buildSessionRowData(session: Session, unreadSessionIds?: Set): homeDir: session.metadata?.homeDir ?? null, completedTodosCount: session.todos?.filter(todo => todo.status === 'completed').length ?? 0, totalTodosCount: session.todos?.length ?? 0, - hasUnread: unreadSessionIds?.has(session.id) ?? false, + hasUnread: unreadSessionIds.has(session.id), }; } @@ -233,7 +233,7 @@ interface StorageState { // Helper function to build unified list view data from sessions and machines function buildSessionListViewData( sessions: Record, - unreadSessionIds?: Set, + unreadSessionIds: Set, ): SessionListViewItem[] { // Separate active and inactive sessions const activeSessions: Session[] = []; @@ -1007,7 +1007,7 @@ export const storage = create()((set, get) => { return { ...state, sessions: updatedSessions, - sessionListViewData: buildSessionListViewData(updatedSessions) + sessionListViewData: buildSessionListViewData(updatedSessions, state.unreadSessionIds) }; }), updateSessionPermissionMode: (sessionId: string, mode: string | null) => set((state) => { @@ -1150,7 +1150,8 @@ export const storage = create()((set, get) => { // Rebuild sessionListViewData to reflect machine changes const sessionListViewData = buildSessionListViewData( - state.sessions + state.sessions, + state.unreadSessionIds, ); return { @@ -1167,7 +1168,7 @@ export const storage = create()((set, get) => { return { ...state, machines: remaining, - sessionListViewData: buildSessionListViewData(state.sessions) + sessionListViewData: buildSessionListViewData(state.sessions, state.unreadSessionIds) }; }), // Artifact methods @@ -1241,7 +1242,7 @@ export const storage = create()((set, get) => { saveSessionEffortLevels(effortLevels); // Rebuild sessionListViewData without the deleted session - const sessionListViewData = buildSessionListViewData(remainingSessions); + const sessionListViewData = buildSessionListViewData(remainingSessions, state.unreadSessionIds); return { ...state,