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"), 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,