mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 11:22:10 +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 { useEffect, useMemo, useState } from "react"
|
||||||
import { useQueryClient } from "@tanstack/react-query"
|
import { useQueryClient } from "@tanstack/react-query"
|
||||||
|
|
||||||
import type { SessionUser } from "@/lib/api"
|
|
||||||
import type { PendingPrompt } from "@/lib/agents/pendingPrompts"
|
import type { PendingPrompt } from "@/lib/agents/pendingPrompts"
|
||||||
import type { AgentThread, Message } from "@/lib/agents/types"
|
import type { AgentThread, Message } from "@/lib/agents/types"
|
||||||
import type { ModelSelection } from "@/lib/agents/useModelOptions"
|
import type { ModelSelection } from "@/lib/agents/useModelOptions"
|
||||||
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
|
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
|
||||||
import { AgentsShell } from "@/components/agents/AgentsSidebar"
|
|
||||||
import { MessageView } from "@/components/agents/ported"
|
import { MessageView } from "@/components/agents/ported"
|
||||||
import {
|
import {
|
||||||
agentThreadKeys,
|
agentThreadKeys,
|
||||||
|
|
@ -21,7 +19,6 @@ import { useAgentThreadStream } from "@/lib/agents/useThreadStream"
|
||||||
import { useModelOptions } from "@/lib/agents/useModelOptions"
|
import { useModelOptions } from "@/lib/agents/useModelOptions"
|
||||||
|
|
||||||
interface AgentThreadViewProps {
|
interface AgentThreadViewProps {
|
||||||
user: SessionUser
|
|
||||||
thread: AgentThread
|
thread: AgentThread
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +55,7 @@ function isPendingPromptConfirmed(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
export function AgentThreadView({ thread }: AgentThreadViewProps) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const sendMessage = useSendAgentMessage(thread.id)
|
const sendMessage = useSendAgentMessage(thread.id)
|
||||||
const cancelThread = useCancelAgentThread(thread.id)
|
const cancelThread = useCancelAgentThread(thread.id)
|
||||||
|
|
@ -125,48 +122,19 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
const isStreaming = hasActiveRun || pendingPrompts.length > 0
|
const isStreaming = hasActiveRun || pendingPrompts.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgentsShell user={user} activeThreadId={thread.id}>
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
{hasMessages ? (
|
||||||
{hasMessages ? (
|
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
<MessageView
|
||||||
<MessageView
|
messages={displayMessages}
|
||||||
messages={displayMessages}
|
isStreaming={isStreaming}
|
||||||
isStreaming={isStreaming}
|
contentWidthClass="max-w-3xl"
|
||||||
contentWidthClass="max-w-3xl"
|
/>
|
||||||
/>
|
<div className="shrink-0 px-4 pb-4">
|
||||||
<div className="shrink-0 px-4 pb-4">
|
<div className="mx-auto w-full max-w-3xl min-w-0">
|
||||||
<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">
|
|
||||||
<AgentPromptBar
|
<AgentPromptBar
|
||||||
placeholder="Send the first message"
|
placeholder="Add a follow up"
|
||||||
compact
|
compact
|
||||||
busy={hasActiveRun}
|
busy={hasActiveRun}
|
||||||
disabled={sendMessage.isPending}
|
disabled={sendMessage.isPending}
|
||||||
|
|
@ -186,9 +154,36 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</AgentsShell>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ import {
|
||||||
useSidebarLayout,
|
useSidebarLayout,
|
||||||
} from "@/components/sidebar-layout"
|
} from "@/components/sidebar-layout"
|
||||||
import { groupThreads } from "@/lib/agents/api"
|
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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
type SourceIcon = ComponentType<SVGProps<SVGSVGElement>>
|
type SourceIcon = ComponentType<SVGProps<SVGSVGElement>>
|
||||||
|
|
@ -52,6 +56,7 @@ const NAV = [
|
||||||
export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
||||||
const threadsQuery = useAgentThreads()
|
const threadsQuery = useAgentThreads()
|
||||||
const threads = threadsQuery.data ?? []
|
const threads = threadsQuery.data ?? []
|
||||||
|
usePrefetchAgentThreadDetails(threads, activeThreadId)
|
||||||
const groups = groupThreads(threads)
|
const groups = groupThreads(threads)
|
||||||
const layout = useSidebarLayout()
|
const layout = useSidebarLayout()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { useNavigate } from "@tanstack/react-router"
|
import { useNavigate } from "@tanstack/react-router"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
import { agentsApi } from "./api"
|
import { agentsApi } from "./api"
|
||||||
import { addPendingPrompt } from "./pendingPrompts"
|
import { addPendingPrompt } from "./pendingPrompts"
|
||||||
|
|
@ -15,6 +16,29 @@ export const agentScheduleKeys = {
|
||||||
all: ["agent-schedules"] as const,
|
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() {
|
export function useAgentThreads() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: agentThreadKeys.all,
|
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 { AgentsShell } from "@/components/agents/AgentsSidebar"
|
||||||
import agentsCss from "@/styles/agents.css?url";
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { useSession } from "@/lib/session";
|
import agentsCss from "@/styles/agents.css?url"
|
||||||
|
import { useSession } from "@/lib/session"
|
||||||
|
|
||||||
export const Route = createFileRoute("/agents")({
|
export const Route = createFileRoute("/agents")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
links: [{ rel: "stylesheet", href: agentsCss }],
|
links: [{ rel: "stylesheet", href: agentsCss }],
|
||||||
}),
|
}),
|
||||||
component: AgentsLayout,
|
component: AgentsLayout,
|
||||||
});
|
})
|
||||||
|
|
||||||
function 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) {
|
if (session.isLoading) {
|
||||||
return (
|
return (
|
||||||
<main className="agents-ui flex h-svh items-center justify-center bg-[var(--ui-bg)] p-6">
|
<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" />
|
<Skeleton className="h-40 w-full max-w-md" />
|
||||||
</main>
|
</main>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session.data) return <Navigate to="/login" />;
|
if (!session.data) return <Navigate to="/login" />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="agents-ui h-svh overflow-hidden">
|
<AgentsShell user={session.data} activeThreadId={activeThreadId}>
|
||||||
<Outlet />
|
<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 { AgentThreadView } from "@/components/agents/AgentThreadView"
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { useAgentThread } from "@/lib/agents/queries";
|
import { useAgentThread } from "@/lib/agents/queries"
|
||||||
import { useSession } from "@/lib/session";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/agents/$threadId")({
|
export const Route = createFileRoute("/agents/$threadId")({
|
||||||
component: AgentThreadPage,
|
component: AgentThreadPage,
|
||||||
});
|
})
|
||||||
|
|
||||||
function AgentThreadPage() {
|
function AgentThreadPage() {
|
||||||
const { threadId } = Route.useParams();
|
const { threadId } = Route.useParams()
|
||||||
const session = useSession();
|
const threadQuery = useAgentThread(threadId)
|
||||||
const threadQuery = useAgentThread(threadId);
|
|
||||||
|
|
||||||
if (session.isLoading) return null;
|
|
||||||
if (!session.data) return <Navigate to="/login" />;
|
|
||||||
|
|
||||||
if (threadQuery.isLoading) {
|
if (threadQuery.isLoading) {
|
||||||
return (
|
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" />
|
<Skeleton className="h-40 w-full max-w-md" />
|
||||||
</main>
|
</main>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (threadQuery.isError || !threadQuery.data) {
|
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 { AutomationEditor } from "@/components/agents/AutomationEditor"
|
||||||
import { AgentsShell } from "@/components/agents/AgentsSidebar";
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { useAgentSchedules } from "@/lib/agents/queries"
|
||||||
import { useAgentSchedules } from "@/lib/agents/queries";
|
|
||||||
import { useSession } from "@/lib/session";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/agents/automations/$scheduleId")({
|
export const Route = createFileRoute("/agents/automations/$scheduleId")({
|
||||||
component: EditAutomationPage,
|
component: EditAutomationPage,
|
||||||
});
|
})
|
||||||
|
|
||||||
function EditAutomationPage() {
|
function EditAutomationPage() {
|
||||||
const session = useSession();
|
const { scheduleId } = Route.useParams()
|
||||||
const { scheduleId } = Route.useParams();
|
const schedulesQuery = useAgentSchedules()
|
||||||
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 (
|
return (
|
||||||
<AgentsShell user={session.data}>
|
<div className="mx-auto w-full max-w-3xl px-6 py-16 text-center">
|
||||||
{schedulesQuery.isLoading ? (
|
<p className="text-sm text-[var(--ui-text-muted)]">
|
||||||
<div className="mx-auto w-full max-w-3xl px-6 py-10">
|
This automation could not be found.
|
||||||
<Skeleton className="h-9 w-64" />
|
</p>
|
||||||
<Skeleton className="mt-6 h-32 w-full" />
|
<Link
|
||||||
</div>
|
to="/agents/automations"
|
||||||
) : schedule ? (
|
className="mt-3 inline-block text-sm text-[var(--ui-accent)] hover:underline"
|
||||||
<AutomationEditor mode="edit" schedule={schedule} />
|
>
|
||||||
) : (
|
Back to Automations
|
||||||
<div className="mx-auto w-full max-w-3xl px-6 py-16 text-center">
|
</Link>
|
||||||
<p className="text-sm text-[var(--ui-text-muted)]">
|
</div>
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,11 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
import { AutomationsList } from "@/components/agents/AutomationsList";
|
import { AutomationsList } from "@/components/agents/AutomationsList"
|
||||||
import { AgentsShell } from "@/components/agents/AgentsSidebar";
|
|
||||||
import { useSession } from "@/lib/session";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/agents/automations/")({
|
export const Route = createFileRoute("/agents/automations/")({
|
||||||
component: AutomationsIndexPage,
|
component: AutomationsIndexPage,
|
||||||
});
|
})
|
||||||
|
|
||||||
function AutomationsIndexPage() {
|
function AutomationsIndexPage() {
|
||||||
const session = useSession();
|
return <AutomationsList />
|
||||||
if (!session.data) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgentsShell user={session.data}>
|
|
||||||
<AutomationsList />
|
|
||||||
</AgentsShell>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,11 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
import { AutomationEditor } from "@/components/agents/AutomationEditor";
|
import { AutomationEditor } from "@/components/agents/AutomationEditor"
|
||||||
import { AgentsShell } from "@/components/agents/AgentsSidebar";
|
|
||||||
import { useSession } from "@/lib/session";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/agents/automations/new")({
|
export const Route = createFileRoute("/agents/automations/new")({
|
||||||
component: NewAutomationPage,
|
component: NewAutomationPage,
|
||||||
});
|
})
|
||||||
|
|
||||||
function NewAutomationPage() {
|
function NewAutomationPage() {
|
||||||
const session = useSession();
|
return <AutomationEditor mode="create" />
|
||||||
if (!session.data) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgentsShell user={session.data}>
|
|
||||||
<AutomationEditor mode="create" />
|
|
||||||
</AgentsShell>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,11 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
import { AgentsHome } from "@/components/agents/AgentsHome";
|
import { AgentsHome } from "@/components/agents/AgentsHome"
|
||||||
import { AgentsShell } from "@/components/agents/AgentsSidebar";
|
|
||||||
import { useSession } from "@/lib/session";
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/agents/")({
|
export const Route = createFileRoute("/agents/")({
|
||||||
component: AgentsIndexPage,
|
component: AgentsIndexPage,
|
||||||
});
|
})
|
||||||
|
|
||||||
function AgentsIndexPage() {
|
function AgentsIndexPage() {
|
||||||
const session = useSession();
|
return <AgentsHome />
|
||||||
if (!session.data) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgentsShell user={session.data}>
|
|
||||||
<AgentsHome />
|
|
||||||
</AgentsShell>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue