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,