From 5fd14f506e595d8a29df30e1580c9359479bc6e8 Mon Sep 17 00:00:00 2001 From: Guitaraholic Date: Wed, 2 Sep 2026 19:23:42 +0100 Subject: [PATCH] Read cached thread in AppLayout while bootstrap is in flight. #6 painted ThreadDetailView from threadQueryKey. AppLayout still disabled useThread until bootstrap settled, so document title and the favicon attention dot waited on the include GET. useThread now takes the bootstrap snapshot and applies the same mount policy, so layout chrome uses the create/list cache immediately. --- apps/app/src/components/layout/AppLayout.tsx | 18 ++++-------- .../src/hooks/queries/thread-queries.test.tsx | 12 +------- apps/app/src/hooks/queries/thread-queries.ts | 29 ++++++++++++++----- .../views/thread-detail/ThreadDetailView.tsx | 14 ++------- 4 files changed, 31 insertions(+), 42 deletions(-) diff --git a/apps/app/src/components/layout/AppLayout.tsx b/apps/app/src/components/layout/AppLayout.tsx index 1375802d48..00783cf55a 100644 --- a/apps/app/src/components/layout/AppLayout.tsx +++ b/apps/app/src/components/layout/AppLayout.tsx @@ -37,7 +37,6 @@ import { AppPageHeader, HEADER_ICON_BUTTON_CLASS } from "./AppPageHeader"; import { stripProjectThreads } from "@/hooks/queries/project-queries"; import { useSidebarNavigation } from "@/hooks/queries/sidebar-navigation-query"; import { - didThreadDetailBootstrapRefreshAfterMount, getLatestPendingInteraction, useThread, useThreadDetailBootstrap, @@ -515,8 +514,6 @@ export function AppLayout({ children }: AppLayoutProps) { enabled: isThreadView && Boolean(threadId), timelinePrefetch: isThreadView && Boolean(threadId), }); - const hasThreadDetailBootstrapSettled = - threadDetailBootstrapQuery.isSuccess || threadDetailBootstrapQuery.isError; const [isSidebarResizing, setIsSidebarResizing] = useState(false); const startXRef = useRef(0); const startWidthRef = useRef(0); @@ -540,15 +537,12 @@ export function AppLayout({ children }: AppLayoutProps) { : null; const projectName = projectId ? project?.name : undefined; const projectLabel = projectName ?? (projectId ? projectId : undefined); - const { data: thread } = useThread(threadId ?? "", { - enabled: - Boolean(threadId) && (!isThreadView || hasThreadDetailBootstrapSettled), - refetchOnMount: - isThreadView && - didThreadDetailBootstrapRefreshAfterMount(threadDetailBootstrapQuery) - ? false - : "always", - }); + const { data: thread } = useThread( + threadId ?? "", + isThreadView + ? { bootstrap: threadDetailBootstrapQuery } + : { enabled: Boolean(threadId), refetchOnMount: "always" }, + ); const threadDisplayTitle = thread ? getThreadDisplayTitle(thread) : threadId diff --git a/apps/app/src/hooks/queries/thread-queries.test.tsx b/apps/app/src/hooks/queries/thread-queries.test.tsx index 7b8399ccfd..662a3f0dd2 100644 --- a/apps/app/src/hooks/queries/thread-queries.test.tsx +++ b/apps/app/src/hooks/queries/thread-queries.test.tsx @@ -1,7 +1,6 @@ // @vitest-environment jsdom import { act, cleanup, renderHook, waitFor } from "@testing-library/react"; -import { useQueryClient } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { PendingInteraction, ThreadListEntry } from "@bb/domain"; import type { @@ -29,7 +28,6 @@ import { COMPACT_THREAD_TIMELINE_SEGMENT_LIMIT, didThreadDetailBootstrapRefreshAfterMount, isPendingInteractionStateUnknown, - resolveThreadDetailQueryMount, useArchivedThreads, useChildThreads, useThread, @@ -417,16 +415,8 @@ describe("useThreadDetailBootstrap", () => { }); function useMountedThreadQuery(threadId: string) { - const queryClient = useQueryClient(); const bootstrap = useThreadDetailBootstrap(threadId); - return useThread( - threadId, - resolveThreadDetailQueryMount({ - bootstrap, - queryClient, - threadId, - }), - ); + return useThread(threadId, { bootstrap }); } describe("useArchivedThreads", () => { diff --git a/apps/app/src/hooks/queries/thread-queries.ts b/apps/app/src/hooks/queries/thread-queries.ts index 3437c4e790..b8a314c1dc 100644 --- a/apps/app/src/hooks/queries/thread-queries.ts +++ b/apps/app/src/hooks/queries/thread-queries.ts @@ -86,7 +86,15 @@ import { import { ARCHIVED_THREADS_PAGE_SIZE } from "./archived-threads-page-size"; import { ingestThreadDetailBootstrap } from "../cache-owners/thread-detail-cache-owner"; +interface ThreadDetailBootstrapMountSnapshot { + dataUpdatedAt: number; + isError: boolean; + isFetchedAfterMount: boolean; + isSuccess: boolean; +} + interface QueryOptions { + bootstrap?: ThreadDetailBootstrapMountSnapshot; enabled?: boolean; refetchOnMount?: boolean | "always"; staleTime?: number; @@ -118,12 +126,7 @@ export function didThreadDetailBootstrapRefreshAfterMount(query: { } export function resolveThreadDetailQueryMount(args: { - bootstrap: { - dataUpdatedAt: number; - isError: boolean; - isFetchedAfterMount: boolean; - isSuccess: boolean; - }; + bootstrap: ThreadDetailBootstrapMountSnapshot; queryClient: QueryClient; threadId: string; }): { enabled: boolean; refetchOnMount: boolean | "always" } { @@ -637,7 +640,16 @@ export function useThreadSearch({ export function useThread(id: string, options?: QueryOptions) { const queryClient = useQueryClient(); - const enabled = (options?.enabled ?? true) && Boolean(id); + const bootstrapMount = + options?.bootstrap === undefined + ? null + : resolveThreadDetailQueryMount({ + bootstrap: options.bootstrap, + queryClient, + threadId: id, + }); + const enabled = + (bootstrapMount?.enabled ?? options?.enabled ?? true) && Boolean(id); useThreadDetailRealtimeSubscription(id, { enabled }); return useQuery({ @@ -649,7 +661,8 @@ export function useThread(id: string, options?: QueryOptions) { }), enabled, staleTime: THREAD_DETAIL_STALE_TIME_MS, - refetchOnMount: options?.refetchOnMount ?? true, + refetchOnMount: + bootstrapMount?.refetchOnMount ?? options?.refetchOnMount ?? true, retry: shouldRetryTransientReadQuery, retryDelay: TRANSIENT_READ_RETRY_DELAY_MS, placeholderData: (previousData, previousQuery) => diff --git a/apps/app/src/views/thread-detail/ThreadDetailView.tsx b/apps/app/src/views/thread-detail/ThreadDetailView.tsx index 3c42eafdef..ac508d09c8 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailView.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailView.tsx @@ -6,7 +6,6 @@ import { useState, type ReactNode, } from "react"; -import { useQueryClient } from "@tanstack/react-query"; import { nanoid } from "nanoid"; import { useSystemProviderInfo } from "@/hooks/queries/system-queries"; import { useNavigate } from "react-router-dom"; @@ -69,7 +68,6 @@ import { import { getLatestPendingInteraction, isPendingInteractionStateUnknown, - resolveThreadDetailQueryMount, useChildThreads, useProjectThreadSubset, useThread, @@ -515,7 +513,6 @@ function ThreadDetailViewInternal(props: ThreadRoutePathArgs) { const { isFocused, navigateInPane, onRequestClose, isBoundedPane } = usePaneContext(); const navigate = useNavigate(); - const queryClient = useQueryClient(); useFixedPanelTabsStorageMaintenance(); const systemConfigQuery = useSystemConfig(); const threadDetailBootstrapQuery = useThreadDetailBootstrap(threadId); @@ -526,14 +523,9 @@ function ThreadDetailViewInternal(props: ThreadRoutePathArgs) { isFetching, isLoadingError, error, - } = useThread( - threadId, - resolveThreadDetailQueryMount({ - bootstrap: threadDetailBootstrapQuery, - queryClient, - threadId, - }), - ); + } = useThread(threadId, { + bootstrap: threadDetailBootstrapQuery, + }); const environmentQuery = useEnvironment(thread?.environmentId, { enabled: hasThreadDetailBootstrapSettled, staleTime: 5_000,