From 46b7c3e2c094c4db882537dcdf5fd39ac39ab6a4 Mon Sep 17 00:00:00 2001 From: Lakshmi Tanmay Date: Tue, 3 Nov 2026 03:32:19 +0530 Subject: [PATCH] fix(web): deleting a thread skips snoozed and settled fallbacks --- apps/web/src/components/Sidebar.logic.test.ts | 55 +++++++++++++++++++ apps/web/src/components/Sidebar.logic.ts | 9 ++- apps/web/src/hooks/useThreadActions.ts | 1 + 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 669c805492bc..810bab00d46e 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -624,10 +624,65 @@ describe("getFallbackThreadIdAfterDelete", () => { deletedThreadId: ThreadId.make("thread-active"), deletedThreadIds: new Set([ThreadId.make("thread-active"), ThreadId.make("thread-newest")]), sortOrder: "created_at", + now: "2026-03-09T11:00:00.000Z", }); expect(fallbackThreadId).toBe(ThreadId.make("thread-next")); }); + + it.each([ + { + state: "snoozed", + overrides: { snoozedUntil: "2026-03-09T14:00:00.000Z" }, + eligible: false, + }, + { + state: "settled", + overrides: { settledOverride: "settled" as const }, + eligible: false, + }, + { + state: "expired snooze", + overrides: { snoozedUntil: "2026-03-09T10:30:00.000Z" }, + eligible: true, + }, + { + state: "snoozed but waiting on input", + overrides: { snoozedUntil: "2026-03-09T14:00:00.000Z", hasPendingUserInput: true }, + eligible: true, + }, + ])("respects $state visibility when choosing a fallback", ({ overrides, eligible }) => { + const deletedThread = makeThread({ id: ThreadId.make("deleted") }); + const candidate = makeThread({ + id: ThreadId.make("candidate"), + createdAt: "2026-03-09T10:10:00.000Z", + ...overrides, + }); + const olderVisibleThread = makeThread({ id: ThreadId.make("older-visible") }); + const otherProjectThread = makeThread({ + id: ThreadId.make("other-project"), + projectId: ProjectId.make("project-2"), + createdAt: "2026-03-09T10:15:00.000Z", + }); + const input = { + deletedThreadId: deletedThread.id, + sortOrder: "created_at" as const, + now: "2026-03-09T11:00:00.000Z", + }; + + expect( + getFallbackThreadIdAfterDelete({ + ...input, + threads: [deletedThread, candidate, olderVisibleThread, otherProjectThread], + }), + ).toBe(eligible ? candidate.id : olderVisibleThread.id); + expect( + getFallbackThreadIdAfterDelete({ + ...input, + threads: [deletedThread, candidate, otherProjectThread], + }), + ).toBe(eligible ? candidate.id : null); + }); }); describe("sortScopedProjectsForSidebar", () => { diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 55cf694c2b79..3395e9685d9b 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -1211,11 +1211,14 @@ export function resolveThreadStatusPill(input: { } export function getFallbackThreadIdAfterDelete< - T extends Pick & ThreadSortInput, + T extends Pick & + ThreadSortInput & + ThreadSnoozeShell, >(input: { threads: readonly T[]; deletedThreadId: T["id"]; sortOrder: SidebarThreadSortOrder; + now: string; deletedThreadIds?: ReadonlySet; }): T["id"] | null { const { deletedThreadId, deletedThreadIds, sortOrder, threads } = input; @@ -1230,7 +1233,9 @@ export function getFallbackThreadIdAfterDelete< (thread) => thread.projectId === deletedThread.projectId && thread.id !== deletedThreadId && - !deletedThreadIds?.has(thread.id), + !deletedThreadIds?.has(thread.id) && + thread.settledOverride !== "settled" && + !effectiveSnoozed(thread, { now: input.now }), ), sortOrder, )[0]?.id ?? null diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts index e5eedffc1669..904b94ebfc5c 100644 --- a/apps/web/src/hooks/useThreadActions.ts +++ b/apps/web/src/hooks/useThreadActions.ts @@ -554,6 +554,7 @@ export function useThreadActions() { deletedThreadId: threadRef.threadId, deletedThreadIds, sortOrder: sidebarThreadSortOrder, + now: new Date().toISOString(), }); const deleteResult = await deleteThreadMutation({ environmentId: threadRef.environmentId,