mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
perf: preload agent thread details (#1448)
* perf: preload agent thread details Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * perf: persist agent layout across thread switches Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
This commit is contained in:
parent
ab2e0d1da3
commit
9350361f0d
9 changed files with 152 additions and 150 deletions
|
|
@ -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 (
|
||||
<AgentsShell user={user} activeThreadId={thread.id}>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{hasMessages ? (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<MessageView
|
||||
messages={displayMessages}
|
||||
isStreaming={isStreaming}
|
||||
contentWidthClass="max-w-3xl"
|
||||
/>
|
||||
<div className="shrink-0 px-4 pb-4">
|
||||
<div className="mx-auto w-full max-w-3xl min-w-0">
|
||||
<AgentPromptBar
|
||||
placeholder="Add a follow up"
|
||||
compact
|
||||
busy={hasActiveRun}
|
||||
disabled={sendMessage.isPending}
|
||||
onSubmit={(content, images) =>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 px-6">
|
||||
<p className="text-sm text-[var(--ui-text-dim)]">
|
||||
This thread has no messages yet.
|
||||
</p>
|
||||
<div className="w-full max-w-3xl">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{hasMessages ? (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<MessageView
|
||||
messages={displayMessages}
|
||||
isStreaming={isStreaming}
|
||||
contentWidthClass="max-w-3xl"
|
||||
/>
|
||||
<div className="shrink-0 px-4 pb-4">
|
||||
<div className="mx-auto w-full max-w-3xl min-w-0">
|
||||
<AgentPromptBar
|
||||
placeholder="Send the first message"
|
||||
placeholder="Add a follow up"
|
||||
compact
|
||||
busy={hasActiveRun}
|
||||
disabled={sendMessage.isPending}
|
||||
|
|
@ -186,9 +154,36 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 px-6">
|
||||
<p className="text-sm text-[var(--ui-text-dim)]">
|
||||
This thread has no messages yet.
|
||||
</p>
|
||||
<div className="w-full max-w-3xl">
|
||||
<AgentPromptBar
|
||||
placeholder="Send the first message"
|
||||
compact
|
||||
busy={hasActiveRun}
|
||||
disabled={sendMessage.isPending}
|
||||
onSubmit={(content, images) =>
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AgentsShell>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SVGProps<SVGSVGElement>>
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="agents-ui flex h-svh items-center justify-center bg-[var(--ui-bg)] p-6">
|
||||
<Skeleton className="h-40 w-full max-w-md" />
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (!session.data) return <Navigate to="/login" />;
|
||||
if (!session.data) return <Navigate to="/login" />
|
||||
|
||||
return (
|
||||
<div className="agents-ui h-svh overflow-hidden">
|
||||
<AgentsShell user={session.data} activeThreadId={activeThreadId}>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
</AgentsShell>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <Navigate to="/login" />;
|
||||
const { threadId } = Route.useParams()
|
||||
const threadQuery = useAgentThread(threadId)
|
||||
|
||||
if (threadQuery.isLoading) {
|
||||
return (
|
||||
<main className="agents-ui flex h-svh items-center justify-center bg-[var(--ui-bg)] p-6">
|
||||
<main className="flex min-w-0 flex-1 items-center justify-center p-6">
|
||||
<Skeleton className="h-40 w-full max-w-md" />
|
||||
</main>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
if (threadQuery.isError || !threadQuery.data) {
|
||||
return <Navigate to="/agents" />;
|
||||
return <Navigate to="/agents" />
|
||||
}
|
||||
|
||||
return <AgentThreadView user={session.data} thread={threadQuery.data} />;
|
||||
return <AgentThreadView thread={threadQuery.data} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="mx-auto w-full max-w-3xl px-6 py-10">
|
||||
<Skeleton className="h-9 w-64" />
|
||||
<Skeleton className="mt-6 h-32 w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const schedule = schedulesQuery.data?.find((s) => s.id === scheduleId);
|
||||
if (schedule) {
|
||||
return <AutomationEditor mode="edit" schedule={schedule} />
|
||||
}
|
||||
|
||||
return (
|
||||
<AgentsShell user={session.data}>
|
||||
{schedulesQuery.isLoading ? (
|
||||
<div className="mx-auto w-full max-w-3xl px-6 py-10">
|
||||
<Skeleton className="h-9 w-64" />
|
||||
<Skeleton className="mt-6 h-32 w-full" />
|
||||
</div>
|
||||
) : schedule ? (
|
||||
<AutomationEditor mode="edit" schedule={schedule} />
|
||||
) : (
|
||||
<div className="mx-auto w-full max-w-3xl px-6 py-16 text-center">
|
||||
<p className="text-sm text-[var(--ui-text-muted)]">
|
||||
This automation could not be found.
|
||||
</p>
|
||||
<Link
|
||||
to="/agents/automations"
|
||||
className="mt-3 inline-block text-sm text-[var(--ui-accent)] hover:underline"
|
||||
>
|
||||
Back to Automations
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</AgentsShell>
|
||||
);
|
||||
<div className="mx-auto w-full max-w-3xl px-6 py-16 text-center">
|
||||
<p className="text-sm text-[var(--ui-text-muted)]">
|
||||
This automation could not be found.
|
||||
</p>
|
||||
<Link
|
||||
to="/agents/automations"
|
||||
className="mt-3 inline-block text-sm text-[var(--ui-accent)] hover:underline"
|
||||
>
|
||||
Back to Automations
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<AgentsShell user={session.data}>
|
||||
<AutomationsList />
|
||||
</AgentsShell>
|
||||
);
|
||||
return <AutomationsList />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<AgentsShell user={session.data}>
|
||||
<AutomationEditor mode="create" />
|
||||
</AgentsShell>
|
||||
);
|
||||
return <AutomationEditor mode="create" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<AgentsShell user={session.data}>
|
||||
<AgentsHome />
|
||||
</AgentsShell>
|
||||
);
|
||||
return <AgentsHome />
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue