Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/typography": "^0.5.16",
Expand All @@ -47,6 +48,7 @@
"rehype-external-links": "^3.0.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"swr": "^2.3.6",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
Expand Down
31 changes: 17 additions & 14 deletions ui/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Footer } from "@/components/Footer";
import { ThemeProvider } from "@/components/ThemeProvider";
import { Toaster } from "@/components/ui/sonner";
import { AppInitializer } from "@/components/AppInitializer";
import { SettingsProvider } from "@/contexts/SettingsContext";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -21,20 +22,22 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<TooltipProvider>
<AgentsProvider>
<html lang="en" className="">
<body className={`${geistSans.className} flex flex-col h-screen overflow-hidden`}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<AppInitializer>
<Header />
<main className="flex-1 overflow-y-scroll w-full mx-auto">{children}</main>
<Footer />
</AppInitializer>
<Toaster richColors/>
</ThemeProvider>
</body>
</html>
</AgentsProvider>
<SettingsProvider>
<AgentsProvider>
<html lang="en" suppressHydrationWarning>
<body className={`${geistSans.className} flex flex-col h-screen overflow-hidden`}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<AppInitializer>
<Header />
<main className="flex-1 overflow-y-scroll w-full mx-auto">{children}</main>
<Footer />
</AppInitializer>
<Toaster richColors/>
</ThemeProvider>
</body>
</html>
</AgentsProvider>
</SettingsProvider>
</TooltipProvider>
);
}
7 changes: 7 additions & 0 deletions ui/src/components/AgentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { ErrorState } from "./ErrorState";
import { Button } from "./ui/button";
import { LoadingState } from "./LoadingState";
import { useAgents } from "./AgentsProvider";
import { RefreshIndicator } from "./RefreshIndicator";
import { SettingsPanel } from "./SettingsPanel";
import { ClientOnly } from "./ClientOnly";

export default function AgentList() {
const { agents , loading, error } = useAgents();
Expand All @@ -24,7 +27,11 @@ export default function AgentList() {
<div className="flex justify-between items-center mb-8">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold">Agents</h1>
<RefreshIndicator />
</div>
<ClientOnly>
<SettingsPanel />
</ClientOnly>
</div>

{agents?.length === 0 ? (
Expand Down
137 changes: 70 additions & 67 deletions ui/src/components/AgentsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react";
import React, { createContext, useContext, ReactNode, useCallback, useMemo } from "react";
import { getAgent as getAgentAction, createAgent, getAgents } from "@/app/actions/agents";
import { getTools } from "@/app/actions/tools";
import { getModels } from "@/app/actions/models";
import type { Agent, Tool, AgentResponse, BaseResponse, ModelConfig, ToolsResponse, AgentType, EnvVar } from "@/types";
import { getModelConfigs } from "@/app/actions/modelConfigs";
import { isResourceNameValid } from "@/lib/utils";
import { useSettings } from "@/contexts/SettingsContext";
import useSWR from "swr";

interface ValidationErrors {
name?: string;
Expand Down Expand Up @@ -53,6 +55,7 @@ interface AgentsContextType {
updateAgent: (agentData: AgentFormData) => Promise<BaseResponse<Agent>>;
getAgent: (name: string, namespace: string) => Promise<AgentResponse | null>;
validateAgentData: (data: Partial<AgentFormData>) => ValidationErrors;
isRefreshing: boolean;
}

const AgentsContext = createContext<AgentsContextType | undefined>(undefined);
Expand All @@ -70,60 +73,68 @@ interface AgentsProviderProps {
}

export function AgentsProvider({ children }: AgentsProviderProps) {
const [agents, setAgents] = useState<AgentResponse[]>([]);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const [tools, setTools] = useState<ToolsResponse[]>([]);
const [models, setModels] = useState<ModelConfig[]>([]);

const fetchAgents = useCallback(async () => {
try {
setLoading(true);
const agentsResult = await getAgents();

if (!agentsResult.data || agentsResult.error) {
throw new Error(agentsResult.error || "Failed to fetch agents");
}

setAgents(agentsResult.data);
setError("");
} catch (err) {
setError(err instanceof Error ? err.message : "An unexpected error occurred");
} finally {
setLoading(false);
}
const { autoRefreshEnabled, autoRefreshInterval } = useSettings();

// Use existing server action fetchers (they include sorting logic)
const agentsFetcher = useCallback(async () => {
const result = await getAgents();
if (result.error) throw new Error(result.error);
return result.data || [];
}, []);

const fetchModels = useCallback(async () => {
try {
const response = await getModelConfigs();
if (!response.data || response.error) {
throw new Error(response.error || "Failed to fetch models");
}

setModels(response.data);
setError("");
} catch (err) {
console.error("Error fetching models:", err);
setError(err instanceof Error ? err.message : "An unexpected error occurred");
} finally {
setLoading(false);
}
const toolsFetcher = useCallback(async () => {
// getTools returns ToolsResponse[] directly, throws on error
return await getTools();
}, []);

const fetchTools = useCallback(async () => {
try {
setLoading(true);
const response = await getTools();
setTools(response);
setError("");
} catch (err) {
console.error("Error fetching tools:", err);
setError(err instanceof Error ? err.message : "An unexpected error occurred");
} finally {
setLoading(false);
}
const modelsFetcher = useCallback(async () => {
// Note: models in AgentsProvider appears unused - model selection uses getModelConfigs() directly
// Return empty array to satisfy type
return [];
}, []);

// Memoize SWR config so it's stable and reactive
const swrConfig = useMemo(() => ({
refreshInterval: autoRefreshEnabled ? autoRefreshInterval : 0,
revalidateOnFocus: true,
revalidateOnReconnect: true,
dedupingInterval: 0, // Disable deduping to ensure every request goes through
revalidateIfStale: true, // Always revalidate if data is stale
refreshWhenHidden: false, // Don't refresh when tab is hidden
refreshWhenOffline: false, // Don't refresh when offline
}), [autoRefreshEnabled, autoRefreshInterval]);

// SWR hooks using server actions (already includes alphabetical sorting)
const { data: agents = [], error: agentsError, isLoading: agentsLoading, mutate: mutateAgents } = useSWR<AgentResponse[]>(
'agents-list',
agentsFetcher,
swrConfig
);

const { data: tools = [], error: toolsError, isLoading: toolsLoading } = useSWR<ToolsResponse[]>(
'tools-list',
toolsFetcher,
// Tools change less frequently, so less aggressive refresh
{ revalidateOnFocus: false, refreshInterval: undefined }
);

const { data: models = [], error: modelsError, isLoading: modelsLoading } = useSWR<ModelConfig[]>(
'models-list',
modelsFetcher,
// Models change even less frequently
{ revalidateOnFocus: false, refreshInterval: undefined }
);

// Combine loading states
const loading = agentsLoading || toolsLoading || modelsLoading;
const error = agentsError?.message || toolsError?.message || modelsError?.message || "";
const isRefreshing = agentsLoading && agents.length > 0; // Only consider it "refreshing" if we have data already

// Manual refresh functions using SWR's mutate
const refreshAgents = useCallback(async () => {
await mutateAgents();
}, [mutateAgents]);


// Validation logic moved from the component
const validateAgentData = useCallback((data: Partial<AgentFormData>): ValidationErrors => {
Expand Down Expand Up @@ -169,11 +180,10 @@ export function AgentsProvider({ children }: AgentsProviderProps) {
// Get agent by ID function
const getAgent = useCallback(async (name: string, namespace: string): Promise<AgentResponse | null> => {
try {
// Fetch all agents
// Fetch single agent
const agentResult = await getAgentAction(name, namespace);
if (!agentResult.data || agentResult.error) {
console.error("Failed to get agent:", agentResult.error);
setError("Failed to get agent");
return null;
}

Expand All @@ -186,12 +196,11 @@ export function AgentsProvider({ children }: AgentsProviderProps) {
return agent;
} catch (error) {
console.error("Error getting agent by name and namespace:", error);
setError(error instanceof Error ? error.message : "Failed to get agent");
return null;
}
}, []);

// Agent creation logic moved from the component
// Agent creation logic
const createNewAgent = useCallback(async (agentData: AgentFormData) => {
try {
const errors = validateAgentData(agentData);
Expand All @@ -203,7 +212,7 @@ export function AgentsProvider({ children }: AgentsProviderProps) {

if (!result.error) {
// Refresh agents to get the newly created one
await fetchAgents();
await mutateAgents();
}

return result;
Expand All @@ -214,7 +223,7 @@ export function AgentsProvider({ children }: AgentsProviderProps) {
error: error instanceof Error ? error.message : "Failed to create agent",
};
}
}, [fetchAgents, validateAgentData]);
}, [mutateAgents, validateAgentData]);

// Update existing agent
const updateAgent = useCallback(async (agentData: AgentFormData): Promise<BaseResponse<Agent>> => {
Expand All @@ -231,7 +240,7 @@ export function AgentsProvider({ children }: AgentsProviderProps) {

if (!result.error) {
// Refresh agents to get the updated one
await fetchAgents();
await mutateAgents();
}

return result;
Expand All @@ -242,26 +251,20 @@ export function AgentsProvider({ children }: AgentsProviderProps) {
error: error instanceof Error ? error.message : "Failed to update agent",
};
}
}, [fetchAgents, validateAgentData]);

// Initial fetches
useEffect(() => {
fetchAgents();
fetchTools();
fetchModels();
}, [fetchAgents, fetchTools, fetchModels]);
}, [mutateAgents, validateAgentData]);

const value = {
agents,
models,
loading,
error,
tools,
refreshAgents: fetchAgents,
refreshAgents,
createNewAgent,
updateAgent,
getAgent,
validateAgentData,
isRefreshing,
};

return <AgentsContext.Provider value={value}>{children}</AgentsContext.Provider>;
Expand Down
Loading
Loading