"use client"; export const dynamic = "force-dynamic"; import { useMemo, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { type getAgentApiV1AgentsAgentIdGetResponse, useGetAgentApiV1AgentsAgentIdGet, useUpdateAgentApiV1AgentsAgentIdPatch, } from "@/api/generated/agents/agents"; import { type listBoardsApiV1BoardsGetResponse, useListBoardsApiV1BoardsGet, } from "@/api/generated/boards/boards"; import type { AgentRead, AgentUpdate, BoardRead } from "@/api/generated/model"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import SearchableSelect, { type SearchableSelectOption, } from "@/components/ui/searchable-select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { DEFAULT_IDENTITY_PROFILE, DEFAULT_SOUL_TEMPLATE, } from "@/lib/agent-templates"; type IdentityProfile = { role: string; communication_style: string; emoji: string; }; const EMOJI_OPTIONS = [ { value: ":gear:", label: "Gear", glyph: "βš™οΈ" }, { value: ":sparkles:", label: "Sparkles", glyph: "✨" }, { value: ":rocket:", label: "Rocket", glyph: "πŸš€" }, { value: ":megaphone:", label: "Megaphone", glyph: "πŸ“£" }, { value: ":chart_with_upwards_trend:", label: "Growth", glyph: "πŸ“ˆ" }, { value: ":bulb:", label: "Idea", glyph: "πŸ’‘" }, { value: ":wrench:", label: "Builder", glyph: "πŸ”§" }, { value: ":shield:", label: "Shield", glyph: "πŸ›‘οΈ" }, { value: ":memo:", label: "Notes", glyph: "πŸ“" }, { value: ":brain:", label: "Brain", glyph: "🧠" }, ]; const HEARTBEAT_TARGET_OPTIONS: SearchableSelectOption[] = [ { value: "none", label: "None (no outbound message)" }, { value: "last", label: "Last channel" }, ]; const getBoardOptions = (boards: BoardRead[]): SearchableSelectOption[] => boards.map((board) => ({ value: board.id, label: board.name, })); const normalizeIdentityProfile = ( profile: IdentityProfile ): IdentityProfile | null => { const normalized: IdentityProfile = { role: profile.role.trim(), communication_style: profile.communication_style.trim(), emoji: profile.emoji.trim(), }; const hasValue = Object.values(normalized).some((value) => value.length > 0); return hasValue ? normalized : null; }; const withIdentityDefaults = ( profile: Partial | null | undefined ): IdentityProfile => ({ role: profile?.role ?? DEFAULT_IDENTITY_PROFILE.role, communication_style: profile?.communication_style ?? DEFAULT_IDENTITY_PROFILE.communication_style, emoji: profile?.emoji ?? DEFAULT_IDENTITY_PROFILE.emoji, }); export default function EditAgentPage() { const { isSignedIn } = useAuth(); const router = useRouter(); const params = useParams(); const agentIdParam = params?.agentId; const agentId = Array.isArray(agentIdParam) ? agentIdParam[0] : agentIdParam; const [name, setName] = useState(undefined); const [boardId, setBoardId] = useState(undefined); const [isGatewayMain, setIsGatewayMain] = useState( undefined, ); const [heartbeatEvery, setHeartbeatEvery] = useState( undefined, ); const [heartbeatTarget, setHeartbeatTarget] = useState( undefined, ); const [identityProfile, setIdentityProfile] = useState< IdentityProfile | undefined >(undefined); const [soulTemplate, setSoulTemplate] = useState( undefined, ); const [error, setError] = useState(null); const boardsQuery = useListBoardsApiV1BoardsGet< listBoardsApiV1BoardsGetResponse, ApiError >(undefined, { query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", retry: false, }, }); const agentQuery = useGetAgentApiV1AgentsAgentIdGet< getAgentApiV1AgentsAgentIdGetResponse, ApiError >(agentId ?? "", { query: { enabled: Boolean(isSignedIn && agentId), refetchOnMount: "always", retry: false, }, }); const updateMutation = useUpdateAgentApiV1AgentsAgentIdPatch({ mutation: { onSuccess: () => { if (agentId) { router.push(`/agents/${agentId}`); } }, onError: (err) => { setError(err.message || "Something went wrong."); }, }, }); const boards = boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : []; const loadedAgent: AgentRead | null = agentQuery.data?.status === 200 ? agentQuery.data.data : null; const loadedHeartbeat = useMemo(() => { const heartbeat = loadedAgent?.heartbeat_config; if (heartbeat && typeof heartbeat === "object") { const record = heartbeat as Record; const every = record.every; const target = record.target; return { every: typeof every === "string" && every.trim() ? every : "10m", target: typeof target === "string" && target.trim() ? target : "none", }; } return { every: "10m", target: "none" }; }, [loadedAgent?.heartbeat_config]); const loadedIdentityProfile = useMemo(() => { const identity = loadedAgent?.identity_profile; if (identity && typeof identity === "object") { const record = identity as Record; return withIdentityDefaults({ role: typeof record.role === "string" ? record.role : undefined, communication_style: typeof record.communication_style === "string" ? record.communication_style : undefined, emoji: typeof record.emoji === "string" ? record.emoji : undefined, }); } return withIdentityDefaults(null); }, [loadedAgent?.identity_profile]); const loadedSoulTemplate = useMemo(() => { return loadedAgent?.soul_template?.trim() || DEFAULT_SOUL_TEMPLATE; }, [loadedAgent?.soul_template]); const isLoading = boardsQuery.isLoading || agentQuery.isLoading || updateMutation.isPending; const errorMessage = error ?? agentQuery.error?.message ?? boardsQuery.error?.message ?? null; const resolvedName = name ?? loadedAgent?.name ?? ""; const resolvedIsGatewayMain = isGatewayMain ?? Boolean(loadedAgent?.is_gateway_main); const resolvedHeartbeatEvery = heartbeatEvery ?? loadedHeartbeat.every; const resolvedHeartbeatTarget = heartbeatTarget ?? loadedHeartbeat.target; const resolvedIdentityProfile = identityProfile ?? loadedIdentityProfile; const resolvedSoulTemplate = soulTemplate ?? loadedSoulTemplate; const resolvedBoardId = useMemo(() => { if (resolvedIsGatewayMain) return boardId ?? ""; return boardId ?? loadedAgent?.board_id ?? boards[0]?.id ?? ""; }, [boardId, boards, loadedAgent?.board_id, resolvedIsGatewayMain]); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); if (!isSignedIn || !agentId || !loadedAgent) return; const trimmed = resolvedName.trim(); if (!trimmed) { setError("Agent name is required."); return; } if (!resolvedIsGatewayMain && !resolvedBoardId) { setError("Select a board or mark this agent as the gateway main."); return; } if ( resolvedIsGatewayMain && !resolvedBoardId && !loadedAgent.is_gateway_main && !loadedAgent.board_id ) { setError( "Select a board once so we can resolve the gateway main session key." ); return; } setError(null); const payload: AgentUpdate = { name: trimmed, heartbeat_config: { every: resolvedHeartbeatEvery.trim() || "10m", target: resolvedHeartbeatTarget, } as unknown as Record, identity_profile: normalizeIdentityProfile(resolvedIdentityProfile) as unknown as Record< string, unknown > | null, soul_template: resolvedSoulTemplate.trim() || null, }; if (!resolvedIsGatewayMain) { payload.board_id = resolvedBoardId || null; } else if (resolvedBoardId) { payload.board_id = resolvedBoardId; } if (Boolean(loadedAgent.is_gateway_main) !== resolvedIsGatewayMain) { payload.is_gateway_main = resolvedIsGatewayMain; } updateMutation.mutate({ agentId, params: { force: true }, data: payload }); }; return (

Sign in to edit agents.

{resolvedName.trim() ? resolvedName : loadedAgent?.name ?? "Edit agent"}

Status is controlled by agent heartbeat.

Basic configuration

setName(event.target.value)} placeholder="e.g. Deploy bot" disabled={isLoading} />
setIdentityProfile({ ...resolvedIdentityProfile, role: event.target.value, }) } placeholder="e.g. Founder, Social Media Manager" disabled={isLoading} />
{resolvedBoardId ? ( ) : null}
setBoardId(value)} options={getBoardOptions(boards)} placeholder={resolvedIsGatewayMain ? "No board (main agent)" : "Select board"} searchPlaceholder="Search boards..." emptyMessage="No matching boards." triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200" contentClassName="rounded-xl border border-slate-200 shadow-lg" itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900" disabled={boards.length === 0} /> {resolvedIsGatewayMain ? (

Main agents are not attached to a board. If a board is selected, it is only used to resolve the gateway main session key and will be cleared on save.

) : boards.length === 0 ? (

Create a board before assigning agents.

) : null}

Personality & behavior

setIdentityProfile({ ...resolvedIdentityProfile, communication_style: event.target.value, }) } disabled={isLoading} />