From df64a0ad060e0c77671851a21fead6957eb58a08 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 11:03:41 +0000 Subject: [PATCH 1/2] test(web): settled shelf keeps its length across project filter changes Fails on main: changing the sidebar project filter resets an expanded Settled shelf to its first 10 threads (#34291). AI-assisted; verified with the tests and tools listed above. Co-Authored-By: Claude Opus 5.5 --- .../src/components/Sidebar.browser.test.tsx | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.browser.test.tsx b/apps/web/src/components/Sidebar.browser.test.tsx index c1cae81000123..ed24a60e0cd3f 100644 --- a/apps/web/src/components/Sidebar.browser.test.tsx +++ b/apps/web/src/components/Sidebar.browser.test.tsx @@ -16,7 +16,7 @@ import { makeThreadFixture } from "../test-fixtures"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { __setClientSettingsForTests, getClientSettings } from "../hooks/useSettings"; import { useThreadRouteTarget } from "../hooks/useThreadRouteTarget"; -import { useProjects, useQuietThreadShells } from "../state/entities"; +import { useProjects, useQuietThreadShells, useServerConfigs } from "../state/entities"; import { useTerminalUiStateStore } from "../terminalUiStateStore"; import type { Project } from "../types"; import Sidebar from "./Sidebar"; @@ -112,6 +112,57 @@ describe("sidebar header new thread", () => { }); }); +describe("sidebar settled shelf", () => { + // regression: #34291 changing the project filter collapsed an expanded Settled shelf back to 10 + it("keeps its expanded length when the project filter changes", async () => { + using stack = new DisposableStack(); + localStorage.setItem("t3code:sidebar:settled-expanded", "true"); + stack.defer(() => localStorage.removeItem("t3code:sidebar:settled-expanded")); + const projectA = project("a"); + const projectB = project("b"); + const settled = (projectId: ProjectId, index: number) => ({ + ...threadShell(projectId, index), + settledOverride: "settled" as const, + }); + const threads = [ + ...Array.from({ length: 14 }, (_, index) => settled(projectA.id, index)), + ...Array.from({ length: 2 }, (_, index) => settled(projectB.id, 14 + index)), + ]; + vi.mocked(useProjects).mockReturnValue([projectA, projectB]); + vi.mocked(useQuietThreadShells).mockReturnValue(threads); + // Threads only count as settled once their environment's config has loaded. + vi.mocked(useServerConfigs).mockReturnValue( + new Map([[projectA.environmentId, { environment: { capabilities: {} } } as never]]), + ); + vi.mocked(useThreadRouteTarget).mockReturnValue(null); + vi.mocked(useHandleNewThread).mockReturnValue({ + activeDraftThread: null, + activeThread: null, + defaultProjectRef: { environmentId: projectA.environmentId, projectId: projectA.id }, + defaultProjectFallbackReady: true, + handleNewThread: vi.fn().mockResolvedValue(null), + routeDraftId: null, + routeThreadRef: null, + }); + await using screen = await render( + + + , + ); + + await userEvent.click(screen.getByRole("button", { name: "Show 6 more" })); + await expect.element(screen.getByText("Thread 15").first()).toBeInTheDocument(); + + await userEvent.click(screen.getByRole("combobox", { name: "Filter threads by project" })); + await userEvent.click(screen.getByRole("option", { name: "a" })); + await expect.element(screen.getByRole("button", { name: "Settled14" })).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /^Thread \d+, a$/ }).elements(), + "the Settled shelf fell back to its first page", + ).toHaveLength(14); + }); +}); + function threadShell(projectId: ProjectId, index: number): EnvironmentThreadShell { return makeThreadFixture({ environmentId: EnvironmentId.make("local"), From e31052a1b41bd1b30bc40b4d54ecb241ffab93aa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 19:18:53 +0000 Subject: [PATCH 2/2] fix(web): settled shelf keeps its expanded length across project filter changes The Settled shelf's visible count was keyed to the project filter and reset to the first 10 threads whenever the filter changed. Keep the count as plain state so it survives filter changes. Fixes pingdotgg/t3code#34291 AI-assisted; verified with the tests and tools listed above. --- apps/web/src/components/Sidebar.tsx | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 8fa58d8456f75..87e8ef18694b8 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -412,7 +412,6 @@ function routeThreadOnly( // Separate hooks cache maps independently of later Sidebar hooks and preserve dnd-kit inputs. function useSidebarThreads(input: { - readonly projectScopeKey: string | null; readonly scopedProjectKeys: ReadonlySet | null; readonly serverConfigs: ReadonlyMap; readonly connectedEnvironmentIds: readonly EnvironmentId[]; @@ -426,7 +425,6 @@ function useSidebarThreads(input: { readonly keepEmptyFolders: (group: SidebarProjectSnapshot) => boolean; }) { const { - projectScopeKey, scopedProjectKeys, serverConfigs, connectedEnvironmentIds, @@ -465,15 +463,7 @@ function useSidebarThreads(input: { // Shared with the old per-project sidebar, so folders keep their open state. const projectExpandedById = useUiStateStore((store) => store.projectExpandedById); const setProjectExpanded = useUiStateStore((store) => store.setProjectExpanded); - const [settledTail, setSettledTail] = useState({ - scopeKey: projectScopeKey, - count: SETTLED_TAIL_INITIAL_COUNT, - }); - if (settledTail.scopeKey !== projectScopeKey) { - setSettledTail({ scopeKey: projectScopeKey, count: SETTLED_TAIL_INITIAL_COUNT }); - } - const settledVisibleCount = - settledTail.scopeKey === projectScopeKey ? settledTail.count : SETTLED_TAIL_INITIAL_COUNT; + const [settledVisibleCount, setSettledVisibleCount] = useState(SETTLED_TAIL_INITIAL_COUNT); const partition = partitionSidebarThreads({ threads, @@ -575,11 +565,7 @@ function useSidebarThreads(input: { settledCount, olderSettledShelf, hiddenSettledCount: Math.max(0, settledCount - visibleSettledThreads.length), - showMoreSettled: () => - setSettledTail((current) => ({ - scopeKey: current.scopeKey, - count: current.count + SETTLED_TAIL_PAGE_COUNT, - })), + showMoreSettled: () => setSettledVisibleCount((count) => count + SETTLED_TAIL_PAGE_COUNT), settledShelfExpanded, toggleSettledShelf: () => setSettledShelfExpanded((value) => !value), snoozedShelfExpanded, @@ -1138,7 +1124,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve findThread: (key) => threads.find((candidate) => threadShellKey(candidate) === key), }); const projectScopeKeys = revealWidensScope ? [] : chosenProjectScopeKeys; - const projectScopeKey = projectScopeKeys.length === 0 ? null : projectScopeKeys.join("\n"); const toggleProjectScopeKey = (key: string) => setProjectScopeKeys( projectScopeKeys.includes(key) @@ -1336,7 +1321,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve pinnedKeysById, activeKeysById, } = useSidebarThreads({ - projectScopeKey, scopedProjectKeys, serverConfigs, connectedEnvironmentIds,