diff --git a/apps/web/src/components/Sidebar.browser.test.tsx b/apps/web/src/components/Sidebar.browser.test.tsx
index 64bead88b35c9..f152cabf01851 100644
--- a/apps/web/src/components/Sidebar.browser.test.tsx
+++ b/apps/web/src/components/Sidebar.browser.test.tsx
@@ -1,3 +1,7 @@
+import appCss from "../index.css?inline";
+import type { EnvironmentServerConfig } from "@t3tools/client-runtime/connection";
+import * as shelfHooks from "../state/olderSettledShelf";
+import { createWorkspaceFocusController, workspaceAreaProps } from "../lib/workspaceFocus";
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
import {
DEFAULT_CLIENT_SETTINGS,
@@ -16,7 +20,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, useServerConfigs, useQuietThreadShells } from "../state/entities";
import { readEnvironmentScope, useEnvironmentScope } from "../state/session";
import { useOrchestrationCommand } from "../state/use-orchestration-command";
import { useTerminalUiStateStore } from "../terminalUiStateStore";
@@ -26,6 +30,10 @@ import { THREAD_JUMP_HINT_SHOW_DELAY_MS } from "./Sidebar.logic";
import { SortableThreadRow } from "./sidebar/SidebarThreadRow";
import { SidebarProvider } from "./ui/sidebar";
+vi.mock(import("../state/olderSettledShelf"), async (original) => {
+ const actual = await original();
+ return { ...actual, useOlderSettledShelf: vi.fn(actual.useOlderSettledShelf) };
+});
vi.mock("../state/entities", { spy: true });
vi.mock("../hooks/useHandleNewThread", { spy: true });
vi.mock("../hooks/useThreadRouteTarget", { spy: true });
@@ -135,6 +143,7 @@ describe("sidebar render isolation", () => {
environmentId: activeProject.environmentId,
threadId: threads[0]!.id,
};
+ threads.unshift(threadShell(activeProject.id, 100));
vi.mocked(useProjects).mockReturnValue([activeProject]);
vi.mocked(useQuietThreadShells).mockReturnValue(threads);
vi.mocked(useThreadRouteTarget).mockReturnValue({ kind: "server", threadRef: routeThreadRef });
@@ -247,3 +256,98 @@ describe("sidebar thread rename", () => {
},
);
});
+
+describe("sidebar settled pagination", () => {
+ it("keeps the viewport in place when showing more settled threads", async () => {
+ using stack = new DisposableStack();
+ const style = document.createElement("style");
+ style.textContent = appCss;
+ document.head.append(style);
+ stack.defer(() => style.remove());
+ const focusController = createWorkspaceFocusController(document);
+ stack.defer(() => focusController.dispose());
+ let loading = false;
+ const originalShelf = vi.mocked(shelfHooks.useOlderSettledShelf).getMockImplementation()!;
+ using _shelf = vi.spyOn(shelfHooks, "useOlderSettledShelf").mockImplementation((input) => ({
+ ...originalShelf(input),
+ loading,
+ retry: () => {
+ loading = true;
+ },
+ }));
+ const activeProject = project("active");
+ const threads = Array.from({ length: 70 }, (_, index) =>
+ makeThreadFixture({
+ environmentId: activeProject.environmentId,
+ projectId: activeProject.id,
+ id: ThreadId.make(`settled-${index}`),
+ title: `Settled thread ${index}`,
+ settledOverride: "settled",
+ settledAt: "2026-01-01T00:00:00.000Z",
+ }),
+ );
+ threads.unshift(threadShell(activeProject.id, 100));
+ vi.mocked(useProjects).mockReturnValue([activeProject]);
+ vi.mocked(useQuietThreadShells).mockReturnValue(threads);
+ vi.mocked(useThreadRouteTarget).mockReturnValue({
+ kind: "server",
+ threadRef: { environmentId: activeProject.environmentId, threadId: threads[0]!.id },
+ });
+ vi.mocked(useHandleNewThread).mockReturnValue({
+ activeDraftThread: null,
+ activeThread: null,
+ defaultProjectRef: {
+ environmentId: activeProject.environmentId,
+ projectId: activeProject.id,
+ },
+ defaultProjectFallbackReady: true,
+ handleNewThread: vi.fn().mockResolvedValue(null),
+ routeDraftId: null,
+ routeThreadRef: { environmentId: activeProject.environmentId, threadId: threads[0]!.id },
+ });
+ vi.mocked(useServerConfigs).mockReturnValue(
+ new Map([
+ [
+ activeProject.environmentId,
+ {
+ scratchWorkspaceRoot: null,
+ shellThreadFamilies: false,
+ } as unknown as EnvironmentServerConfig,
+ ],
+ ]),
+ );
+ await using screen = await render(
+
+
+ ,
+ );
+ const shelf = screen.container.querySelector(
+ '[data-sidebar-row="shelf:settled"]',
+ )!;
+ if (shelf.getAttribute("aria-expanded") === "false") await act(() => shelf.click());
+ const more = screen.getByRole("button", { name: "Show 25 more", exact: true });
+ await expect.element(more).toBeInTheDocument();
+ const moreButton = more.element() as HTMLButtonElement;
+ const viewport = moreButton.closest('[data-slot="scroll-area-viewport"]')!;
+ viewport.style.height = "300px";
+ viewport.style.maxHeight = "300px";
+ moreButton.scrollIntoView({ block: "nearest" });
+ const before = viewport.scrollTop;
+ expect(before).toBeGreaterThan(0);
+ moreButton.focus({ preventScroll: true });
+ await act(() => moreButton.click());
+ await expect
+ .element(screen.getByRole("button", { name: "Loading", exact: true }))
+ .toBeInTheDocument();
+ await expect
+ .element(screen.getByRole("button", { name: "Loading", exact: true }))
+ .toHaveFocus();
+ expect(viewport.scrollTop).toBe(before);
+ const loadedRows = screen.container.querySelectorAll("[data-thread-item]").length;
+ await act(() => moreButton.click());
+ expect(screen.container.querySelectorAll("[data-thread-item]")).toHaveLength(loadedRows);
+ });
+});
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
index 5c9203742578c..b7ec300204a05 100644
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -3644,12 +3644,13 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
type="button"
data-sidebar-row="settled:more"
data-sidebar-parent="shelf:settled"
- disabled={olderSettledShelf.loading}
+ aria-disabled={olderSettledShelf.loading}
onClick={() => {
+ if (olderSettledShelf.loading) return;
olderSettledShelf.retry();
showMoreSettled();
}}
- className="flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground disabled:cursor-default"
+ className="flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground aria-disabled:cursor-default"
>
{/* Plus means "add", so revealing more uses a chevron. */}