From 5ecd1806a41896d13d668ef0ea35508c0d9a79d8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 2 Nov 2026 21:42:54 +0000 Subject: [PATCH 1/2] test(web): reproduce #29609, delete fallback lands on parked threads These tests reproduce pingdotgg/t3code#29609: getFallbackThreadIdAfterDelete picks a snoozed or settled thread instead of the next thread visible in the sidebar. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/Sidebar.logic.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 669c805492bcc..bc83b31b1a6ef 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -628,6 +628,39 @@ describe("getFallbackThreadIdAfterDelete", () => { expect(fallbackThreadId).toBe(ThreadId.make("thread-next")); }); + + it.each([ + [ + "snoozed", + { snoozedUntil: "2099-01-01T00:00:00.000Z", snoozedAt: "2026-03-09T10:06:00.000Z" }, + ], + ["settled", { settledOverride: "settled", settledAt: "2026-03-09T10:06:00.000Z" }], + ] as const)("skips a %s thread parked on its shelf", (_state, parked) => { + const fallbackThreadId = getFallbackThreadIdAfterDelete({ + threads: [ + makeThread({ + id: ThreadId.make("thread-visible"), + createdAt: "2026-03-09T10:01:00.000Z", + updatedAt: "2026-03-09T10:01:00.000Z", + }), + makeThread({ + id: ThreadId.make("thread-parked"), + createdAt: "2026-03-09T10:02:00.000Z", + updatedAt: "2026-03-09T10:06:00.000Z", + ...parked, + }), + makeThread({ + id: ThreadId.make("thread-active"), + createdAt: "2026-03-09T10:03:00.000Z", + updatedAt: "2026-03-09T10:03:00.000Z", + }), + ], + deletedThreadId: ThreadId.make("thread-active"), + sortOrder: "created_at", + }); + + expect(fallbackThreadId).toBe(ThreadId.make("thread-visible")); + }); }); describe("sortScopedProjectsForSidebar", () => { From ede2715b383381173ef24ae20af3e97d5563330f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 2 Nov 2026 21:45:52 +0000 Subject: [PATCH 2/2] fix(web): deleting a thread no longer lands on a snoozed or settled thread The delete fallback picked the most recent same-project thread, including threads parked on the Snoozed and Settled shelves. It now skips them with the sidebar's own predicates (settledOverride and effectiveSnoozed). Fixes pingdotgg/t3code#29609 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/Sidebar.logic.test.ts | 2 ++ apps/web/src/components/Sidebar.logic.ts | 11 ++++++++--- apps/web/src/hooks/useThreadActions.ts | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index bc83b31b1a6ef..3e2e8be2ce18d 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -624,6 +624,7 @@ describe("getFallbackThreadIdAfterDelete", () => { deletedThreadId: ThreadId.make("thread-active"), deletedThreadIds: new Set([ThreadId.make("thread-active"), ThreadId.make("thread-newest")]), sortOrder: "created_at", + now: "2026-03-09T10:10:00.000Z", }); expect(fallbackThreadId).toBe(ThreadId.make("thread-next")); @@ -657,6 +658,7 @@ describe("getFallbackThreadIdAfterDelete", () => { ], deletedThreadId: ThreadId.make("thread-active"), sortOrder: "created_at", + now: "2026-03-09T10:10:00.000Z", }); expect(fallbackThreadId).toBe(ThreadId.make("thread-visible")); diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 55cf694c2b79e..c20789e2a9160 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -1211,14 +1211,17 @@ 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; + const { deletedThreadId, deletedThreadIds, sortOrder, now, threads } = input; const deletedThread = threads.find((thread) => thread.id === deletedThreadId); if (!deletedThread) { return null; @@ -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 }), ), sortOrder, )[0]?.id ?? null diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts index e5eedffc16698..904b94ebfc5c5 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,