diff --git a/apps/app/src/views/thread-detail/SplitThreadArea.test.tsx b/apps/app/src/views/thread-detail/SplitThreadArea.test.tsx index c2bc3d7f9e..b7b181380e 100644 --- a/apps/app/src/views/thread-detail/SplitThreadArea.test.tsx +++ b/apps/app/src/views/thread-detail/SplitThreadArea.test.tsx @@ -12,7 +12,7 @@ import { import { createStore, Provider as JotaiProvider } from "jotai"; import { useContext, useMemo, useState, type ReactNode } from "react"; import { MemoryRouter, useLocation, useNavigate } from "react-router-dom"; -import { QueryClientProvider } from "@tanstack/react-query"; +import { QueryClientProvider, useQuery } from "@tanstack/react-query"; import { PERSONAL_PROJECT_ID } from "@bb/domain"; import { TooltipProvider } from "@bb/shared-ui/tooltip"; import type { BbDesktopInfo } from "@bb/desktop-contract"; @@ -74,6 +74,10 @@ const commandHandlers = vi.hoisted(() => new Map boolean>()); const paneContextRenders = vi.hoisted( () => new Map>(), ); +const timelineProbe = vi.hoisted(() => ({ + active: false, + calls: new Map(), +})); interface ShortcutPresentationFixture { ariaKeyshortcuts: string; label: string; @@ -291,10 +295,21 @@ vi.mock("./ThreadDetailView", () => ({ ThreadDetailView: ({ projectId = "proj_personal", threadId = "thr-a", + timelineEnabled = true, }: { projectId: string; threadId: string; + timelineEnabled?: boolean; }) => { + useQuery({ + queryKey: ["timeline-probe", threadId], + queryFn: () => { + const next = (timelineProbe.calls.get(threadId) ?? 0) + 1; + timelineProbe.calls.set(threadId, next); + return next; + }, + enabled: timelineProbe.active && timelineEnabled, + }); const pane = useContext(PaneContext); paneContextRenders.set(threadId, [ ...(paneContextRenders.get(threadId) ?? []), @@ -705,6 +720,8 @@ beforeEach(() => { panelGroupLayoutState.layout = [100, 0]; commandHandlers.clear(); paneContextRenders.clear(); + timelineProbe.active = false; + timelineProbe.calls.clear(); commandPresentationState.isModifierHeld = false; commandPresentationState.shortcut = null; threadStore.set("thr-a", { archivedAt: null, deletedAt: null }); @@ -859,6 +876,30 @@ describe("SplitThreadArea", () => { }); }); + it("pauses a hidden pane timeline and refreshes it when restored", async () => { + timelineProbe.active = true; + renderSplitArea({ + path: threadPath("thr-a"), + layout: twoPaneLayout("pane-1"), + }); + + await waitFor(() => { + expect(timelineProbe.calls.get("thr-a")).toBe(1); + expect(timelineProbe.calls.get("thr-b")).toBe(1); + }); + + fireEvent.click(screen.getByTestId("maximize-thr-a")); + await act(async () => { + await queryClient.invalidateQueries({ + queryKey: ["timeline-probe", "thr-b"], + }); + }); + expect(timelineProbe.calls.get("thr-b")).toBe(1); + + fireEvent.click(screen.getByTestId("maximize-thr-a")); + await waitFor(() => expect(timelineProbe.calls.get("thr-b")).toBe(2)); + }); + it("temporarily replaces panel full screen with a clean thread full screen", async () => { panelFullScreenState.isMainCollapsed = true; renderSplitArea({ @@ -2279,8 +2320,8 @@ describe("SplitThreadArea", () => { ); await waitFor(() => expect( - screen.getAllByRole("button", { name: "Close pane" })[0] - ?.parentElement?.nextElementSibling, + screen.getAllByRole("button", { name: "Close pane" })[0]?.parentElement + ?.nextElementSibling, ).toBeNull(), ); }); diff --git a/apps/app/src/views/thread-detail/SplitThreadArea.tsx b/apps/app/src/views/thread-detail/SplitThreadArea.tsx index 6ccb1de581..12bc664245 100644 --- a/apps/app/src/views/thread-detail/SplitThreadArea.tsx +++ b/apps/app/src/views/thread-detail/SplitThreadArea.tsx @@ -633,6 +633,7 @@ function SplitThreadAreaContent({ routeContent }: SplitThreadAreaProps) { paneId={firstPane.paneId} isFocused isSplitPane={false} + timelineEnabled secondaryPanelRegistry={null} reservesWindowPanelToggle={false} onClosePane={ @@ -818,6 +819,7 @@ function SplitTree(props: SplitTreeProps) { paneId={node.paneId} isFocused={isFocused} isSplitPane + timelineEnabled={!isHiddenByMaximize} secondaryPanelRegistry={props.secondaryPanelRegistry} reservesWindowPanelToggle={isMaximized || (isTopRow && isRightEdge)} onClosePane={props.onClosePane} @@ -894,6 +896,7 @@ interface WorkspacePaneContentProps { paneId: string; isFocused: boolean; isSplitPane: boolean; + timelineEnabled: boolean; secondaryPanelRegistry: PaneSecondaryPanelRegistry | null; reservesWindowPanelToggle: boolean; onClosePane: ((paneId: string) => void) | null; @@ -912,6 +915,7 @@ const WorkspacePaneContent = memo(function WorkspacePaneContent({ paneId, isFocused, isSplitPane, + timelineEnabled, secondaryPanelRegistry, reservesWindowPanelToggle, onClosePane, @@ -1018,6 +1022,7 @@ const WorkspacePaneContent = memo(function WorkspacePaneContent({ surface="pane" projectId={content.projectId} threadId={content.threadId} + timelineEnabled={timelineEnabled} /> ); diff --git a/apps/app/src/views/thread-detail/ThreadDetailView.tsx b/apps/app/src/views/thread-detail/ThreadDetailView.tsx index 6e01a6b41e..2ab5b1bc25 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailView.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailView.tsx @@ -383,6 +383,7 @@ interface ThreadDetailViewPageProps { interface ThreadDetailViewPaneProps extends ThreadRoutePathArgs { surface: "pane"; + timelineEnabled: boolean; } type ThreadDetailViewProps = @@ -501,7 +502,11 @@ function RoutedThreadDetailView({ return ( - + ); } @@ -513,8 +518,10 @@ export function ThreadDetailView(props: ThreadDetailViewProps) { return ; } -function ThreadDetailViewInternal(props: ThreadRoutePathArgs) { - const { projectId, threadId } = props; +function ThreadDetailViewInternal( + props: ThreadRoutePathArgs & { timelineEnabled: boolean }, +) { + const { projectId, threadId, timelineEnabled } = props; const { isFocused, navigateInPane, onRequestClose, isBoundedPane } = usePaneContext(); const navigate = useImmediateRouteNavigate(); @@ -866,6 +873,7 @@ function ThreadDetailViewInternal(props: ThreadRoutePathArgs) { timelineRows, } = useThreadTimelineController({ threadId, + enabled: timelineEnabled, }); const sendMessage = useSendThreadMessage(); const editMessage = useEditThreadMessage();