diff --git a/agentex-ui/app/custom/page.tsx b/agentex-ui/app/custom/page.tsx
new file mode 100644
index 0000000..87840be
--- /dev/null
+++ b/agentex-ui/app/custom/page.tsx
@@ -0,0 +1,30 @@
+import { connection } from 'next/server';
+
+import { CustomPageRoot } from '@/components/custom/custom-page-root';
+import { AgentexProvider } from '@/components/providers';
+
+export default async function CustomPage() {
+ await connection();
+
+ const sgpAppURL = process.env.NEXT_PUBLIC_SGP_APP_URL ?? '';
+ const agentexAPIBaseURL =
+ process.env.NEXT_PUBLIC_AGENTEX_API_BASE_URL ?? 'http://localhost:5003';
+
+ if (!agentexAPIBaseURL) {
+ return (
+
+
Missing some configs
+
{JSON.stringify({ sgpAppURL, agentexAPIBaseURL }, null, 2)}
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/agentex-ui/components/custom/config-panel.tsx b/agentex-ui/components/custom/config-panel.tsx
new file mode 100644
index 0000000..0b8f50a
--- /dev/null
+++ b/agentex-ui/components/custom/config-panel.tsx
@@ -0,0 +1,214 @@
+'use client';
+
+import { useCallback, useEffect } from 'react';
+
+import { RotateCcw } from 'lucide-react';
+import { useForm } from 'react-hook-form';
+
+import { TagInput } from '@/components/custom/tag-input';
+import { Button } from '@/components/ui/button';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+} from '@/components/ui/form';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Textarea } from '@/components/ui/textarea';
+
+export type GoldenAgentConfig = {
+ harness: string;
+ model: string;
+ system_prompt: string;
+ allowed_tools: string[];
+};
+
+const SUPPORTED_HARNESSES = [
+ 'sandbox-claude',
+ 'claude-code',
+ 'codex',
+ 'agentex',
+] as const;
+
+const DEFAULT_MODEL_BY_HARNESS: Record = {
+ 'sandbox-claude': 'claude-opus-4-6',
+ 'claude-code': 'claude-opus-4-6',
+ codex: 'gpt-5.4',
+ agentex: 'gpt-5.4',
+};
+
+export const DEFAULT_CONFIG: GoldenAgentConfig = {
+ harness: 'sandbox-claude',
+ model: 'claude-opus-4-6',
+ system_prompt:
+ "You are a developer for Scale AI's SGP team. Your primary sources of truth are scaleapi/packages/egp-api-backend and scaleapi/packages/egp-annotation. Use these packages to do the development you need to do. Make sure to read the READMEs and CLAUDE.mds in those directories to get the context you need to address any issues. Your main goal is to take in the information from the prompt, use your sources of truth to come up with a course of action to address it, and then make a PR with the solution.",
+ allowed_tools: [
+ 'Read',
+ 'Write',
+ 'Edit',
+ 'Glob',
+ 'Grep',
+ 'Bash',
+ 'WebSearch',
+ 'WebFetch',
+ 'List',
+ ],
+};
+
+type ConfigPanelProps = {
+ disabled: boolean;
+ onConfigChange: (config: GoldenAgentConfig) => void;
+ onReset: () => void;
+};
+
+export function ConfigPanel({
+ disabled,
+ onConfigChange,
+ onReset,
+}: ConfigPanelProps) {
+ const form = useForm({
+ defaultValues: DEFAULT_CONFIG,
+ });
+
+ const harness = form.watch('harness');
+
+ useEffect(() => {
+ const subscription = form.watch(values => {
+ onConfigChange(values as GoldenAgentConfig);
+ });
+ return () => subscription.unsubscribe();
+ }, [form, onConfigChange]);
+
+ const handleHarnessChange = useCallback(
+ (value: string) => {
+ form.setValue('harness', value);
+ form.setValue('model', DEFAULT_MODEL_BY_HARNESS[value] ?? '');
+ },
+ [form]
+ );
+
+ const handleReset = useCallback(() => {
+ form.reset(DEFAULT_CONFIG);
+ onReset();
+ }, [form, onReset]);
+
+ return (
+
+
+
+
Configuration
+ {disabled && (
+
+ )}
+
+
golden-agent
+
+
+
+
+
+ );
+}
diff --git a/agentex-ui/components/custom/custom-chat-panel.tsx b/agentex-ui/components/custom/custom-chat-panel.tsx
new file mode 100644
index 0000000..f7455a2
--- /dev/null
+++ b/agentex-ui/components/custom/custom-chat-panel.tsx
@@ -0,0 +1,75 @@
+'use client';
+
+import { useRef } from 'react';
+
+import { motion } from 'framer-motion';
+
+import { useAgentexClient } from '@/components/providers';
+import { TaskMessages } from '@/components/task-messages/task-messages';
+import { CopyButton } from '@/components/ui/copy-button';
+import { useTaskSubscription } from '@/hooks/use-task-subscription';
+
+const GOLDEN_AGENT_NAME = 'golden-agent';
+
+type CustomChatPanelProps = {
+ taskId: string | null;
+};
+
+export function CustomChatPanel({ taskId }: CustomChatPanelProps) {
+ const { agentexClient } = useAgentexClient();
+ const headerRef = useRef(null);
+ const scrollContainerRef = useRef(null);
+
+ useTaskSubscription({
+ agentexClient,
+ taskId: taskId ?? '',
+ agentName: GOLDEN_AGENT_NAME,
+ enabled: !!taskId,
+ });
+
+ if (!taskId) {
+ return (
+
+
+ Configure the agent and send a message to start.
+
+
+ );
+ }
+
+ return (
+
+
+
+ golden-agent
+
+ |
+
+ {taskId.slice(0, 8)}...
+
+
+
+
+
+
+ );
+}
diff --git a/agentex-ui/components/custom/custom-page-root.tsx b/agentex-ui/components/custom/custom-page-root.tsx
new file mode 100644
index 0000000..19f3f17
--- /dev/null
+++ b/agentex-ui/components/custom/custom-page-root.tsx
@@ -0,0 +1,118 @@
+'use client';
+
+import { useCallback, useState } from 'react';
+
+import { ToastContainer } from 'react-toastify';
+
+import { useAgentexClient } from '@/components/providers';
+import { ResizableSidebar } from '@/components/ui/resizable-sidebar';
+import { useAgents } from '@/hooks/use-agents';
+
+import { ConfigPanel, DEFAULT_CONFIG } from './config-panel';
+import { CustomChatPanel } from './custom-chat-panel';
+import { CustomPromptInput } from './custom-prompt-input';
+
+import type { GoldenAgentConfig } from './config-panel';
+
+const GOLDEN_AGENT_NAME = 'golden-agent';
+
+export function CustomPageRoot() {
+ const { agentexClient } = useAgentexClient();
+ const { data: agents = [], isLoading } = useAgents(agentexClient);
+
+ const [config, setConfig] = useState(DEFAULT_CONFIG);
+ const [taskId, setTaskId] = useState(null);
+ const [isConfigLocked, setIsConfigLocked] = useState(false);
+ const [prompt, setPrompt] = useState('');
+
+ const goldenAgent = agents.find(a => a.name === GOLDEN_AGENT_NAME);
+
+ const handleTaskCreated = useCallback((newTaskId: string) => {
+ setTaskId(newTaskId);
+ setIsConfigLocked(true);
+ }, []);
+
+ const handleReset = useCallback(() => {
+ setTaskId(null);
+ setIsConfigLocked(false);
+ setPrompt('');
+ }, []);
+
+ const handleConfigChange = useCallback(
+ (newConfig: GoldenAgentConfig) => {
+ if (!isConfigLocked) {
+ setConfig(newConfig);
+ }
+ },
+ [isConfigLocked]
+ );
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!goldenAgent) {
+ return (
+
+
+
golden-agent not found
+
+ Make sure the golden-agent is deployed and has status Ready.
+
+
+
+ );
+ }
+
+ if (goldenAgent.status !== 'Ready') {
+ return (
+
+
+
golden-agent is not ready
+
+ Current status: {goldenAgent.status}
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/agentex-ui/components/custom/custom-prompt-input.tsx b/agentex-ui/components/custom/custom-prompt-input.tsx
new file mode 100644
index 0000000..593c4d0
--- /dev/null
+++ b/agentex-ui/components/custom/custom-prompt-input.tsx
@@ -0,0 +1,153 @@
+'use client';
+
+import { useCallback, useMemo, useRef } from 'react';
+
+import { ArrowUp } from 'lucide-react';
+
+import { useAgentexClient } from '@/components/providers';
+import { IconButton } from '@/components/ui/icon-button';
+import { toast } from '@/components/ui/toast';
+import { useCreateTask } from '@/hooks/use-create-task';
+import { useSendMessage } from '@/hooks/use-task-messages';
+import { useTask } from '@/hooks/use-tasks';
+import { TaskStatusEnum } from '@/lib/types';
+
+import type { GoldenAgentConfig } from './config-panel';
+import type { DataContent, TextContent } from 'agentex/resources';
+
+const GOLDEN_AGENT_NAME = 'golden-agent';
+
+type CustomPromptInputProps = {
+ taskId: string | null;
+ config: GoldenAgentConfig;
+ prompt: string;
+ setPrompt: (prompt: string) => void;
+ onTaskCreated: (taskId: string) => void;
+};
+
+export function CustomPromptInput({
+ taskId,
+ config,
+ prompt,
+ setPrompt,
+ onTaskCreated,
+}: CustomPromptInputProps) {
+ const { agentexClient } = useAgentexClient();
+ const createTaskMutation = useCreateTask({ agentexClient });
+ const sendMessageMutation = useSendMessage({ agentexClient });
+ const { data: task } = useTask({
+ agentexClient,
+ taskId: taskId ?? '',
+ });
+ const inputRef = useRef(null);
+
+ const isTaskTerminal = useMemo(() => {
+ if (!taskId || !task) return false;
+ return task.status != null && task.status !== TaskStatusEnum.RUNNING;
+ }, [taskId, task]);
+
+ const isDisabled = isTaskTerminal;
+
+ const handleSendPrompt = useCallback(async () => {
+ if (!prompt.trim()) {
+ toast.error('Please enter a prompt');
+ return;
+ }
+
+ const currentPrompt = prompt;
+ setPrompt('');
+
+ let currentTaskId = taskId;
+
+ if (!currentTaskId) {
+ const params: Record = {
+ system_prompt: config.system_prompt || null,
+ allowed_tools: config.allowed_tools,
+ harness: config.harness,
+ model: config.model,
+ };
+
+ const createdTask = await createTaskMutation.mutateAsync({
+ agentName: GOLDEN_AGENT_NAME,
+ params,
+ });
+ currentTaskId = createdTask.id;
+ onTaskCreated(currentTaskId);
+
+ const content: DataContent = {
+ type: 'data',
+ author: 'user',
+ data: {
+ system_prompt: config.system_prompt || null,
+ allowed_tools: config.allowed_tools,
+ harness: config.harness,
+ model: config.model,
+ message: currentPrompt,
+ },
+ };
+
+ await sendMessageMutation.mutateAsync({
+ taskId: currentTaskId,
+ agentName: GOLDEN_AGENT_NAME,
+ content,
+ });
+ } else {
+ const content: TextContent = {
+ type: 'text',
+ author: 'user',
+ format: 'plain',
+ attachments: [],
+ content: currentPrompt,
+ };
+
+ await sendMessageMutation.mutateAsync({
+ taskId: currentTaskId,
+ agentName: GOLDEN_AGENT_NAME,
+ content,
+ });
+ }
+ }, [
+ prompt,
+ taskId,
+ config,
+ setPrompt,
+ createTaskMutation,
+ sendMessageMutation,
+ onTaskCreated,
+ ]);
+
+ return (
+
+
+ setPrompt(e.target.value)}
+ onKeyDown={e => {
+ if (e.key === 'Enter' && !isDisabled && prompt.trim()) {
+ handleSendPrompt();
+ }
+ }}
+ disabled={isDisabled}
+ placeholder={
+ isTaskTerminal
+ ? `Task ${task?.status?.toLowerCase() ?? 'ended'}`
+ : 'Enter your prompt'
+ }
+ className="mr-2 flex-1 outline-none focus:ring-0 focus:outline-none"
+ style={{ backgroundColor: 'inherit', cursor: 'inherit' }}
+ />
+
+
+
+ );
+}
diff --git a/agentex-ui/components/custom/tag-input.tsx b/agentex-ui/components/custom/tag-input.tsx
new file mode 100644
index 0000000..0d9bb8b
--- /dev/null
+++ b/agentex-ui/components/custom/tag-input.tsx
@@ -0,0 +1,90 @@
+'use client';
+
+import { useCallback, useState, type KeyboardEvent } from 'react';
+
+import { X } from 'lucide-react';
+
+import { Badge } from '@/components/ui/badge';
+import { cn } from '@/lib/utils';
+
+type TagInputProps = {
+ value: string[];
+ onChange: (tags: string[]) => void;
+ disabled?: boolean;
+ placeholder?: string;
+ className?: string;
+};
+
+export function TagInput({
+ value,
+ onChange,
+ disabled = false,
+ placeholder = 'Type and press Enter',
+ className,
+}: TagInputProps) {
+ const [inputValue, setInputValue] = useState('');
+
+ const addTag = useCallback(
+ (tag: string) => {
+ const trimmed = tag.trim();
+ if (trimmed && !value.includes(trimmed)) {
+ onChange([...value, trimmed]);
+ }
+ setInputValue('');
+ },
+ [value, onChange]
+ );
+
+ const removeTag = useCallback(
+ (tagToRemove: string) => {
+ onChange(value.filter(tag => tag !== tagToRemove));
+ },
+ [value, onChange]
+ );
+
+ const handleKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ addTag(inputValue);
+ } else if (e.key === 'Backspace' && !inputValue && value.length > 0) {
+ removeTag(value[value.length - 1]!);
+ }
+ },
+ [inputValue, value, addTag, removeTag]
+ );
+
+ return (
+
+ {value.map(tag => (
+
+ {tag}
+ {!disabled && (
+
+ )}
+
+ ))}
+ setInputValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ disabled={disabled}
+ placeholder={value.length === 0 ? placeholder : ''}
+ className="placeholder:text-muted-foreground min-w-[120px] flex-1 bg-transparent text-sm outline-none disabled:cursor-not-allowed"
+ />
+
+ );
+}