/** * DevUI App - Minimal orchestrator for agent/workflow interactions * Features: Entity selection, layout management, debug coordination */ import { useEffect, useCallback, useState } from "react"; import { AppHeader, DebugPanel, SettingsModal, DeploymentModal } from "@/components/layout"; import { GalleryView } from "@/components/features/gallery"; import { AgentView } from "@/components/features/agent"; import { WorkflowView } from "@/components/features/workflow"; import { Toast, ToastContainer } from "@/components/ui/toast"; import { apiClient } from "@/services/api"; import { PanelRightOpen, ChevronLeft, ChevronDown, ServerOff, Rocket, Lock } from "lucide-react"; import type { AgentInfo, WorkflowInfo, ExtendedResponseStreamEvent, } from "@/types"; import { Button } from "./components/ui/button"; import { Input } from "./components/ui/input"; import { useDevUIStore } from "@/stores"; export default function App() { // Local state for auth handling const [authRequired, setAuthRequired] = useState(false); const [authToken, setAuthToken] = useState(""); const [isTestingToken, setIsTestingToken] = useState(false); const [authError, setAuthError] = useState(""); // Entity state from Zustand const agents = useDevUIStore((state) => state.agents); const workflows = useDevUIStore((state) => state.workflows); const entities = useDevUIStore((state) => state.entities); const selectedAgent = useDevUIStore((state) => state.selectedAgent); const azureDeploymentEnabled = useDevUIStore((state) => state.azureDeploymentEnabled); const isLoadingEntities = useDevUIStore((state) => state.isLoadingEntities); const entityError = useDevUIStore((state) => state.entityError); // OpenAI proxy mode const oaiMode = useDevUIStore((state) => state.oaiMode); // UI mode const uiMode = useDevUIStore((state) => state.uiMode); // Entity actions const setAgents = useDevUIStore((state) => state.setAgents); const setWorkflows = useDevUIStore((state) => state.setWorkflows); const setEntities = useDevUIStore((state) => state.setEntities); const selectEntity = useDevUIStore((state) => state.selectEntity); const updateAgent = useDevUIStore((state) => state.updateAgent); const updateWorkflow = useDevUIStore((state) => state.updateWorkflow); const setIsLoadingEntities = useDevUIStore((state) => state.setIsLoadingEntities); const setEntityError = useDevUIStore((state) => state.setEntityError); // UI state from Zustand const showDebugPanel = useDevUIStore((state) => state.showDebugPanel); const debugPanelMinimized = useDevUIStore((state) => state.debugPanelMinimized); const debugPanelWidth = useDevUIStore((state) => state.debugPanelWidth); const debugEvents = useDevUIStore((state) => state.debugEvents); const isResizing = useDevUIStore((state) => state.isResizing); // UI actions const setShowDebugPanel = useDevUIStore((state) => state.setShowDebugPanel); const setDebugPanelMinimized = useDevUIStore((state) => state.setDebugPanelMinimized); const setDebugPanelWidth = useDevUIStore((state) => state.setDebugPanelWidth); const addDebugEvent = useDevUIStore((state) => state.addDebugEvent); const clearDebugEvents = useDevUIStore((state) => state.clearDebugEvents); const setIsResizing = useDevUIStore((state) => state.setIsResizing); // Modal state const showAboutModal = useDevUIStore((state) => state.showAboutModal); const showGallery = useDevUIStore((state) => state.showGallery); const showDeployModal = useDevUIStore((state) => state.showDeployModal); const showEntityNotFoundToast = useDevUIStore((state) => state.showEntityNotFoundToast); // Modal actions const setShowAboutModal = useDevUIStore((state) => state.setShowAboutModal); const setShowGallery = useDevUIStore((state) => state.setShowGallery); const setShowDeployModal = useDevUIStore((state) => state.setShowDeployModal); const setShowEntityNotFoundToast = useDevUIStore((state) => state.setShowEntityNotFoundToast); // Toast state and actions const toasts = useDevUIStore((state) => state.toasts); const addToast = useDevUIStore((state) => state.addToast); const removeToast = useDevUIStore((state) => state.removeToast); // Initialize app - load agents and workflows useEffect(() => { const loadData = async () => { try { // Fetch server metadata first (ui_mode, capabilities, auth status) const meta = await apiClient.getMeta(); // Check if auth is required if (meta.auth_required) { setAuthRequired(true); // If we don't have a token, stop here and show auth UI if (!apiClient.getAuthToken()) { setEntityError("UNAUTHORIZED"); setIsLoadingEntities(false); return; } } useDevUIStore.getState().setServerMeta({ uiMode: meta.ui_mode, runtime: meta.runtime, capabilities: meta.capabilities, authRequired: meta.auth_required, version: meta.version, }); // Single API call instead of two parallel calls to same endpoint const { entities: allEntities, agents: agentList, workflows: workflowList } = await apiClient.getEntities(); setEntities(allEntities); setAgents(agentList); setWorkflows(workflowList); // Check if there's an entity_id in the URL const urlParams = new URLSearchParams(window.location.search); const entityId = urlParams.get("entity_id"); let selectedEntity: AgentInfo | WorkflowInfo | undefined; // Try to find entity from URL parameter first if (entityId) { selectedEntity = allEntities.find((e) => e.id === entityId); // If entity not found but was requested, show notification if (!selectedEntity) { setShowEntityNotFoundToast(true); } } // Fallback to first available entity if URL entity not found if (!selectedEntity) { // Use the first entity from the backend's original order // This respects the backend's intended display order selectedEntity = allEntities.length > 0 ? allEntities[0] : undefined; // Update URL to match actual selected entity (or clear if none) if (selectedEntity) { const url = new URL(window.location.href); url.searchParams.set("entity_id", selectedEntity.id); window.history.replaceState({}, "", url); } else { // Clear entity_id if no entities available const url = new URL(window.location.href); url.searchParams.delete("entity_id"); window.history.replaceState({}, "", url); } } if (selectedEntity) { selectEntity(selectedEntity); // Load full info for the first entity immediately if (selectedEntity.metadata?.lazy_loaded === false) { try { if (selectedEntity.type === "agent") { const fullAgent = await apiClient.getAgentInfo( selectedEntity.id ); updateAgent(fullAgent); } else { const fullWorkflow = await apiClient.getWorkflowInfo( selectedEntity.id ); updateWorkflow(fullWorkflow); } } catch (error) { console.error( `Failed to load full info for first entity ${selectedEntity.id}:`, error ); // Show toast for entity load errors (don't use setEntityError - that kills the whole UI) const errorMessage = error instanceof Error ? error.message : String(error); addToast({ type: "error", message: `Failed to load "${selectedEntity.id}": ${errorMessage}`, }); } } } setIsLoadingEntities(false); } catch (error) { console.error("Failed to load agents/workflows:", error); const errorMessage = error instanceof Error ? error.message : "Failed to load data"; // Check if this is an auth error if (errorMessage === "UNAUTHORIZED") { setAuthRequired(true); } setEntityError(errorMessage); setIsLoadingEntities(false); } }; loadData(); }, [setAgents, setWorkflows, selectEntity, updateAgent, updateWorkflow, setIsLoadingEntities, setEntityError, setShowEntityNotFoundToast, addToast, setEntities]); // Handle auth token submission const handleAuthTokenSubmit = useCallback(async () => { if (!authToken.trim()) return; setIsTestingToken(true); setAuthError(""); try { // Set token in API client (stores in localStorage) apiClient.setAuthToken(authToken.trim()); // Test the token with an actual PROTECTED endpoint (not /meta which is public) await apiClient.getEntities(); // If successful, reload to initialize with new token window.location.reload(); } catch (error) { // Token is invalid - clear it and show error apiClient.clearAuthToken(); setIsTestingToken(false); const errorMsg = error instanceof Error ? error.message : "Unknown error"; if (errorMsg === "UNAUTHORIZED") { setAuthError("Invalid token. Please check and try again."); } else { setAuthError(`Failed to connect: ${errorMsg}`); } } }, [authToken]); // Auto-switch from workflow to agent when OpenAI proxy mode is enabled useEffect(() => { if (oaiMode.enabled && selectedAgent?.type === "workflow") { // Workflows don't work with OpenAI proxy - switch to first available agent const firstAgent = agents[0]; if (firstAgent) { selectEntity(firstAgent); } } }, [oaiMode.enabled, selectedAgent, agents, selectEntity]); // Handle resize drag const handleMouseDown = useCallback( (e: React.MouseEvent) => { e.preventDefault(); setIsResizing(true); const startX = e.clientX; const startWidth = debugPanelWidth; const handleMouseMove = (e: MouseEvent) => { const deltaX = startX - e.clientX; // Subtract because we're dragging from right const newWidth = Math.max( 200, Math.min(window.innerWidth * 0.5, startWidth + deltaX) ); setDebugPanelWidth(newWidth); }; const handleMouseUp = () => { setIsResizing(false); document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }, [debugPanelWidth] ); // Handle entity selection - uses Zustand's selectEntity which handles ALL side effects const handleEntitySelect = useCallback( async (item: AgentInfo | WorkflowInfo) => { selectEntity(item); // This clears conversation state, debug events, and updates URL! // If entity is sparse (not fully loaded), load full details if (item.metadata?.lazy_loaded === false) { try { if (item.type === "agent") { const fullAgent = await apiClient.getAgentInfo(item.id); updateAgent(fullAgent); } else { const fullWorkflow = await apiClient.getWorkflowInfo(item.id); updateWorkflow(fullWorkflow); } } catch (error) { console.error(`Failed to load full info for ${item.id}:`, error); // Show toast for entity load errors (don't use setEntityError - that kills the whole UI) const errorMessage = error instanceof Error ? error.message : String(error); addToast({ type: "error", message: `Failed to load "${item.id}": ${errorMessage}`, }); } } }, [selectEntity, updateAgent, updateWorkflow, addToast] ); // Handle debug events from active view const handleDebugEvent = useCallback( (event: ExtendedResponseStreamEvent | "clear") => { if (event === "clear") { clearDebugEvents(); } else { addDebugEvent(event); } }, [addDebugEvent, clearDebugEvents] ); // Show loading state while initializing if (isLoadingEntities) { return (
{/* Top Bar - Skeleton */}
{/* Loading Content */}
Initializing DevUI...
Loading agents and workflows from your configuration
); } // Show error state if loading failed if (entityError) { const currentBackendUrl = apiClient.getBaseUrl(); const isAuthError = entityError === "UNAUTHORIZED" || authRequired; // Extract port from the backend URL for the command suggestion let backendPort = "8080"; // default fallback try { if (currentBackendUrl) { const url = new URL(currentBackendUrl); backendPort = url.port || (url.protocol === "https:" ? "443" : "80"); } } catch { // If URL parsing fails, keep default } return (
{}} isLoading={false} onSettingsClick={() => setShowAboutModal(true)} /> {/* Error Content */}
{/* Icon */}
{isAuthError ? ( ) : ( )}
{/* Heading */}

{isAuthError ? "Authentication Required" : "Can't Connect to Backend"}

{isAuthError ? "This backend requires a bearer token to access." : "No worries! Just start the DevUI backend server and you'll be good to go."}

{/* Auth Input or Command Instructions */} {isAuthError ? (

Enter Authentication Token

setAuthToken(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !isTestingToken) { handleAuthTokenSubmit(); } }} disabled={isTestingToken} className="font-mono text-sm" /> {/* Error message */} {authError && (

{authError}

)}
Where do I find the token?

Look for this in your DevUI server startup logs:

🔑 DEV TOKEN (localhost only, shown once):
   abc123xyz...
) : ( <>

Start the backend:

devui ./agents --port {backendPort}

Or launch programmatically with{" "} serve(entities=[agent])

Default:{" "} {currentBackendUrl}

{/* Error Details (Collapsible) */} {entityError && (
Error details

{entityError}

)} {/* Retry Button */} )}
{/* Settings Modal */}
); } return (
setShowGallery(true)} isLoading={isLoadingEntities} onSettingsClick={() => setShowAboutModal(true)} /> {/* Main Content - Split Panel or Gallery */}
{showGallery ? ( // Show gallery full screen (w-full ensures it takes entire width)
setShowGallery(false)} hasExistingEntities={ agents.length > 0 || workflows.length > 0 } />
) : agents.length === 0 && workflows.length === 0 ? ( // Empty state - show gallery inline (full width, no debug panel) ) : ( <> {/* Left Panel - Main View */}
{selectedAgent ? ( selectedAgent.type === "agent" ? ( ) : ( ) ) : (
Select an agent or workflow to get started.
)}
{uiMode === "developer" && showDebugPanel ? ( <> {/* Resize Handle */}
{/* Right Panel - Debug */}
{debugPanelMinimized ? ( /* Minimized Debug Panel - Vertical Bar (fully clickable) */
setDebugPanelMinimized(false)} title="Expand debug panel" > {/* Expand button at top (visual affordance) */}
{/* Text and count centered in middle */}
Debug Panel
{debugEvents.length > 0 && (
{debugEvents.length}
)}
) : ( <> setDebugPanelMinimized(true)} /> {/* Deploy Footer - Pinned to bottom */}
)}
) : uiMode === "developer" ? ( /* Button to reopen when closed */
) : null} )}
{/* Settings Modal */} {/* Deployment Modal */} setShowDeployModal(false)} agentName={selectedAgent?.name} entity={selectedAgent} /> {/* Toast Notification */} {showEntityNotFoundToast && ( setShowEntityNotFoundToast(false)} /> )} {/* Toast Container for reload and other notifications */}
); }