From 9350361f0db06c631f44f3cda7acc3573f8ced4a Mon Sep 17 00:00:00 2001 From: Ramon Nogueira Date: Mon, 8 Jun 2026 17:07:55 -0400 Subject: [PATCH] perf: preload agent thread details (#1448) * perf: preload agent thread details Co-authored-by: open-swe[bot] * perf: persist agent layout across thread switches Co-authored-by: open-swe[bot] --------- Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com> Co-authored-by: open-swe[bot] Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com> --- ui/src/components/agents/AgentThreadView.tsx | 91 +++++++++---------- ui/src/components/agents/AgentsSidebar.tsx | 7 +- ui/src/lib/agents/queries.ts | 24 +++++ ui/src/routes/agents.tsx | 36 +++++--- ui/src/routes/agents/$threadId.tsx | 27 +++--- .../routes/agents/automations/$scheduleId.tsx | 66 +++++++------- ui/src/routes/agents/automations/index.tsx | 17 +--- ui/src/routes/agents/automations/new.tsx | 17 +--- ui/src/routes/agents/index.tsx | 17 +--- 9 files changed, 152 insertions(+), 150 deletions(-) diff --git a/ui/src/components/agents/AgentThreadView.tsx b/ui/src/components/agents/AgentThreadView.tsx index 855a5afe..805f20c3 100644 --- a/ui/src/components/agents/AgentThreadView.tsx +++ b/ui/src/components/agents/AgentThreadView.tsx @@ -1,12 +1,10 @@ import { useEffect, useMemo, useState } from "react" import { useQueryClient } from "@tanstack/react-query" -import type { SessionUser } from "@/lib/api" import type { PendingPrompt } from "@/lib/agents/pendingPrompts" import type { AgentThread, Message } from "@/lib/agents/types" import type { ModelSelection } from "@/lib/agents/useModelOptions" import { AgentPromptBar } from "@/components/agents/AgentPromptBar" -import { AgentsShell } from "@/components/agents/AgentsSidebar" import { MessageView } from "@/components/agents/ported" import { agentThreadKeys, @@ -21,7 +19,6 @@ import { useAgentThreadStream } from "@/lib/agents/useThreadStream" import { useModelOptions } from "@/lib/agents/useModelOptions" interface AgentThreadViewProps { - user: SessionUser thread: AgentThread } @@ -58,7 +55,7 @@ function isPendingPromptConfirmed( }) } -export function AgentThreadView({ user, thread }: AgentThreadViewProps) { +export function AgentThreadView({ thread }: AgentThreadViewProps) { const queryClient = useQueryClient() const sendMessage = useSendAgentMessage(thread.id) const cancelThread = useCancelAgentThread(thread.id) @@ -125,48 +122,19 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) { const isStreaming = hasActiveRun || pendingPrompts.length > 0 return ( - -
-
- {hasMessages ? ( -
- -
-
- - sendMessage.mutate({ - content, - images, - model_id: activeSelection?.modelId ?? null, - effort: activeSelection?.effort ?? null, - }) - } - onStop={() => cancelThread.mutate()} - stopping={cancelThread.isPending} - models={models} - selection={activeSelection} - onSelectionChange={setSelection} - /> -
-
-
- ) : ( -
-

- This thread has no messages yet. -

-
+
+
+ {hasMessages ? ( +
+ +
+
- )} -
+
+ ) : ( +
+

+ This thread has no messages yet. +

+
+ + sendMessage.mutate({ + content, + images, + model_id: activeSelection?.modelId ?? null, + effort: activeSelection?.effort ?? null, + }) + } + onStop={() => cancelThread.mutate()} + stopping={cancelThread.isPending} + models={models} + selection={activeSelection} + onSelectionChange={setSelection} + /> +
+
+ )}
- +
) } diff --git a/ui/src/components/agents/AgentsSidebar.tsx b/ui/src/components/agents/AgentsSidebar.tsx index 58e9dc35..f46acdaa 100644 --- a/ui/src/components/agents/AgentsSidebar.tsx +++ b/ui/src/components/agents/AgentsSidebar.tsx @@ -26,7 +26,11 @@ import { useSidebarLayout, } from "@/components/sidebar-layout" import { groupThreads } from "@/lib/agents/api" -import { useAgentThreads, useDeleteAgentThread } from "@/lib/agents/queries" +import { + useAgentThreads, + useDeleteAgentThread, + usePrefetchAgentThreadDetails, +} from "@/lib/agents/queries" import { cn } from "@/lib/utils" type SourceIcon = ComponentType> @@ -52,6 +56,7 @@ const NAV = [ export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) { const threadsQuery = useAgentThreads() const threads = threadsQuery.data ?? [] + usePrefetchAgentThreadDetails(threads, activeThreadId) const groups = groupThreads(threads) const layout = useSidebarLayout() diff --git a/ui/src/lib/agents/queries.ts b/ui/src/lib/agents/queries.ts index 6246ddbf..ec9307c3 100644 --- a/ui/src/lib/agents/queries.ts +++ b/ui/src/lib/agents/queries.ts @@ -1,5 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useNavigate } from "@tanstack/react-router" +import { useEffect } from "react" import { agentsApi } from "./api" import { addPendingPrompt } from "./pendingPrompts" @@ -15,6 +16,29 @@ export const agentScheduleKeys = { all: ["agent-schedules"] as const, } +const PREFETCH_THREAD_DETAIL_LIMIT = 12 + +export function usePrefetchAgentThreadDetails( + threads: Array<{ id: string }>, + activeThreadId?: string +) { + const queryClient = useQueryClient() + + useEffect(() => { + const threadIds = threads + .map((thread) => thread.id) + .filter((threadId) => threadId !== activeThreadId) + .slice(0, PREFETCH_THREAD_DETAIL_LIMIT) + + threadIds.forEach((threadId) => { + void queryClient.prefetchQuery({ + queryKey: agentThreadKeys.detail(threadId), + queryFn: () => agentsApi.getThread(threadId), + }) + }) + }, [activeThreadId, queryClient, threads]) +} + export function useAgentThreads() { return useQuery({ queryKey: agentThreadKeys.all, diff --git a/ui/src/routes/agents.tsx b/ui/src/routes/agents.tsx index 97bc06b6..c85412bf 100644 --- a/ui/src/routes/agents.tsx +++ b/ui/src/routes/agents.tsx @@ -1,32 +1,46 @@ -import { Navigate, Outlet, createFileRoute } from "@tanstack/react-router"; +import { + Navigate, + Outlet, + createFileRoute, + useRouterState, +} from "@tanstack/react-router" -import { Skeleton } from "@/components/ui/skeleton"; -import agentsCss from "@/styles/agents.css?url"; -import { useSession } from "@/lib/session"; +import { AgentsShell } from "@/components/agents/AgentsSidebar" +import { Skeleton } from "@/components/ui/skeleton" +import agentsCss from "@/styles/agents.css?url" +import { useSession } from "@/lib/session" export const Route = createFileRoute("/agents")({ head: () => ({ links: [{ rel: "stylesheet", href: agentsCss }], }), component: AgentsLayout, -}); +}) function AgentsLayout() { - const session = useSession(); + const session = useSession() + const pathname = useRouterState({ + select: (state) => state.location.pathname, + }) + const [, section, threadId] = pathname.split("/") + const activeThreadId = + section === "agents" && threadId && threadId !== "automations" + ? threadId + : undefined if (session.isLoading) { return (
- ); + ) } - if (!session.data) return ; + if (!session.data) return return ( -
+ -
- ); + + ) } diff --git a/ui/src/routes/agents/$threadId.tsx b/ui/src/routes/agents/$threadId.tsx index 572e2039..1acaa6e4 100644 --- a/ui/src/routes/agents/$threadId.tsx +++ b/ui/src/routes/agents/$threadId.tsx @@ -1,33 +1,28 @@ -import { Navigate, createFileRoute } from "@tanstack/react-router"; +import { Navigate, createFileRoute } from "@tanstack/react-router" -import { AgentThreadView } from "@/components/agents/AgentThreadView"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useAgentThread } from "@/lib/agents/queries"; -import { useSession } from "@/lib/session"; +import { AgentThreadView } from "@/components/agents/AgentThreadView" +import { Skeleton } from "@/components/ui/skeleton" +import { useAgentThread } from "@/lib/agents/queries" export const Route = createFileRoute("/agents/$threadId")({ component: AgentThreadPage, -}); +}) function AgentThreadPage() { - const { threadId } = Route.useParams(); - const session = useSession(); - const threadQuery = useAgentThread(threadId); - - if (session.isLoading) return null; - if (!session.data) return ; + const { threadId } = Route.useParams() + const threadQuery = useAgentThread(threadId) if (threadQuery.isLoading) { return ( -
+
- ); + ) } if (threadQuery.isError || !threadQuery.data) { - return ; + return } - return ; + return } diff --git a/ui/src/routes/agents/automations/$scheduleId.tsx b/ui/src/routes/agents/automations/$scheduleId.tsx index f014e6e2..5e49173d 100644 --- a/ui/src/routes/agents/automations/$scheduleId.tsx +++ b/ui/src/routes/agents/automations/$scheduleId.tsx @@ -1,46 +1,42 @@ -import { Link, createFileRoute } from "@tanstack/react-router"; +import { Link, createFileRoute } from "@tanstack/react-router" -import { AutomationEditor } from "@/components/agents/AutomationEditor"; -import { AgentsShell } from "@/components/agents/AgentsSidebar"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useAgentSchedules } from "@/lib/agents/queries"; -import { useSession } from "@/lib/session"; +import { AutomationEditor } from "@/components/agents/AutomationEditor" +import { Skeleton } from "@/components/ui/skeleton" +import { useAgentSchedules } from "@/lib/agents/queries" export const Route = createFileRoute("/agents/automations/$scheduleId")({ component: EditAutomationPage, -}); +}) function EditAutomationPage() { - const session = useSession(); - const { scheduleId } = Route.useParams(); - const schedulesQuery = useAgentSchedules(); + const { scheduleId } = Route.useParams() + const schedulesQuery = useAgentSchedules() + const schedule = schedulesQuery.data?.find((s) => s.id === scheduleId) - if (!session.data) return null; + if (schedulesQuery.isLoading) { + return ( +
+ + +
+ ) + } - const schedule = schedulesQuery.data?.find((s) => s.id === scheduleId); + if (schedule) { + return + } return ( - - {schedulesQuery.isLoading ? ( -
- - -
- ) : schedule ? ( - - ) : ( -
-

- This automation could not be found. -

- - Back to Automations - -
- )} -
- ); +
+

+ This automation could not be found. +

+ + Back to Automations + +
+ ) } diff --git a/ui/src/routes/agents/automations/index.tsx b/ui/src/routes/agents/automations/index.tsx index a5bb2654..1a7ae507 100644 --- a/ui/src/routes/agents/automations/index.tsx +++ b/ui/src/routes/agents/automations/index.tsx @@ -1,20 +1,11 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router" -import { AutomationsList } from "@/components/agents/AutomationsList"; -import { AgentsShell } from "@/components/agents/AgentsSidebar"; -import { useSession } from "@/lib/session"; +import { AutomationsList } from "@/components/agents/AutomationsList" export const Route = createFileRoute("/agents/automations/")({ component: AutomationsIndexPage, -}); +}) function AutomationsIndexPage() { - const session = useSession(); - if (!session.data) return null; - - return ( - - - - ); + return } diff --git a/ui/src/routes/agents/automations/new.tsx b/ui/src/routes/agents/automations/new.tsx index 1807c494..45c7fc7e 100644 --- a/ui/src/routes/agents/automations/new.tsx +++ b/ui/src/routes/agents/automations/new.tsx @@ -1,20 +1,11 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router" -import { AutomationEditor } from "@/components/agents/AutomationEditor"; -import { AgentsShell } from "@/components/agents/AgentsSidebar"; -import { useSession } from "@/lib/session"; +import { AutomationEditor } from "@/components/agents/AutomationEditor" export const Route = createFileRoute("/agents/automations/new")({ component: NewAutomationPage, -}); +}) function NewAutomationPage() { - const session = useSession(); - if (!session.data) return null; - - return ( - - - - ); + return } diff --git a/ui/src/routes/agents/index.tsx b/ui/src/routes/agents/index.tsx index 9efd1526..ebe18c66 100644 --- a/ui/src/routes/agents/index.tsx +++ b/ui/src/routes/agents/index.tsx @@ -1,20 +1,11 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute } from "@tanstack/react-router" -import { AgentsHome } from "@/components/agents/AgentsHome"; -import { AgentsShell } from "@/components/agents/AgentsSidebar"; -import { useSession } from "@/lib/session"; +import { AgentsHome } from "@/components/agents/AgentsHome" export const Route = createFileRoute("/agents/")({ component: AgentsIndexPage, -}); +}) function AgentsIndexPage() { - const session = useSession(); - if (!session.data) return null; - - return ( - - - - ); + return }