From 95cfd69e3d69e5d1a5e12d4985704be9712f60b2 Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Thu, 4 Jun 2026 12:55:20 -0400 Subject: [PATCH] feat(ui): add agent-configs page backed by SGP Adds an agent-configs page that lists/creates SGP agent configs through a server-side Next.js route handler and a thin SGP REST client, plus a sidebar nav entry to reach it. The route forwards SGP auth headers from the request and falls back to server-side SGP_API_KEY / SGP_ACCOUNT_ID env vars when the browser supplies none, so the UI-driven flow authenticates without a logged-in SGP session. Co-Authored-By: Claude Opus 4.8 (1M context) --- agentex-ui/app/agent-configs/page.tsx | 413 ++++++++++++++++++ agentex-ui/app/api/agent-configs/route.ts | 69 +++ .../task-sidebar/task-sidebar-header.tsx | 11 +- agentex-ui/lib/sgp-client.ts | 170 +++++++ 4 files changed, 662 insertions(+), 1 deletion(-) create mode 100644 agentex-ui/app/agent-configs/page.tsx create mode 100644 agentex-ui/app/api/agent-configs/route.ts create mode 100644 agentex-ui/lib/sgp-client.ts diff --git a/agentex-ui/app/agent-configs/page.tsx b/agentex-ui/app/agent-configs/page.tsx new file mode 100644 index 00000000..9ed71c8a --- /dev/null +++ b/agentex-ui/app/agent-configs/page.tsx @@ -0,0 +1,413 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; + +import { useRouter } from 'next/navigation'; + +import AgentexSDK from 'agentex'; +import { agentRPCNonStreaming } from 'agentex/lib'; + +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; + +type AgentConfig = { + id: string; + name: string; + description: string | null; + system_prompt: string; + harness: string; + allowed_tools: string[]; + model: string; + created_at: string; + updated_at: string; +}; + +const DEFAULT_AGENT_NAME = 'golden-agent'; + +const AGENTEX_API_BASE_URL = + process.env.NEXT_PUBLIC_AGENTEX_API_BASE_URL ?? 'http://localhost:5004'; + +export default function AgentConfigsPage() { + const router = useRouter(); + + const agentexClient = useMemo( + () => + new AgentexSDK({ + baseURL: AGENTEX_API_BASE_URL, + fetchOptions: { credentials: 'include' }, + }), + [] + ); + + const [configs, setConfigs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showCreate, setShowCreate] = useState(false); + + // Per-config launcher state — keyed by config id so each card has its own + // textarea + agent-name input + busy flag. + const [launchState, setLaunchState] = useState< + Record + >({}); + + async function refresh() { + setLoading(true); + setError(null); + try { + const res = await fetch('/api/agent-configs'); + if (!res.ok) throw new Error(await res.text()); + const data = await res.json(); + setConfigs(Array.isArray(data?.items) ? data.items : []); + } catch (e) { + setError(e instanceof Error ? e.message : 'Failed to load agent configs'); + } finally { + setLoading(false); + } + } + + useEffect(() => { + refresh(); + }, []); + + function updateLaunch( + id: string, + patch: Partial<{ message: string; agentName: string; busy: boolean }> + ) { + setLaunchState(prev => ({ + ...prev, + [id]: { + message: prev[id]?.message ?? '', + agentName: prev[id]?.agentName ?? DEFAULT_AGENT_NAME, + busy: false, + ...patch, + }, + })); + } + + async function launchWithConfig(config: AgentConfig) { + const state = launchState[config.id]; + const message = (state?.message ?? '').trim(); + const agentName = (state?.agentName ?? DEFAULT_AGENT_NAME).trim(); + if (!message) { + setError('Enter a message before launching.'); + return; + } + setError(null); + updateLaunch(config.id, { busy: true }); + + try { + // task/create — pass the agent config as params. The golden agent reads + // system_prompt / allowed_tools / harness / model off task params. + const createResp = await agentRPCNonStreaming( + agentexClient, + { agentName }, + 'task/create', + { + params: { + system_prompt: config.system_prompt, + allowed_tools: config.allowed_tools, + harness: config.harness, + model: config.model, + // attach the source config id so traces can be cross-referenced. + agent_config_id: config.id, + }, + } + ); + if (createResp.error) throw new Error(createResp.error.message); + const task = createResp.result as { id: string }; + + // First user message — agent's acp_type is `async`, so use event/send. + const sendResp = await agentRPCNonStreaming( + agentexClient, + { agentName }, + 'event/send', + { + task_id: task.id, + content: { + type: 'text', + author: 'user', + format: 'plain', + attachments: [], + content: message, + }, + } + ); + if (sendResp.error) throw new Error(sendResp.error.message); + + // Hand off to the main UI so the user can watch streaming output. + // useSafeSearchParams reads agent_name + task_id from the URL. + const params = new URLSearchParams({ + agent_name: agentName, + task_id: task.id, + }); + router.push(`/?${params.toString()}`); + } catch (e) { + setError(e instanceof Error ? e.message : 'Launch failed'); + updateLaunch(config.id, { busy: false }); + } + } + + return ( +
+
+

Agent configs

+

+ Pick a config from pubsec-dev SGP (via the forwarded{' '} + egp-api-backend), type a message, and dispatch it to a + locally running agent through your local agentex ( + {AGENTEX_API_BASE_URL}). Default target is{' '} + {DEFAULT_AGENT_NAME}. +

+
+ + {error && ( +
+ {error} +
+ )} + +
+

Launch a session

+ {loading ? ( +

Loading…

+ ) : configs.length === 0 ? ( +

No configs yet.

+ ) : ( +
    + {configs.map(c => { + const s = launchState[c.id] ?? { + message: '', + agentName: DEFAULT_AGENT_NAME, + busy: false, + }; + return ( +
  • +
    +
    +
    {c.name}
    + {c.description && ( +
    + {c.description} +
    + )} +
    + + {c.id} + +
    + +
    + + harness: {c.harness} + + + model: {c.model} + + {c.allowed_tools.map(t => ( + + {t} + + ))} +
    + +
    + + System prompt + +
    +                      {c.system_prompt}
    +                    
    +
    + +
    +
    + +