diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 93c048a9..a133949a 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -69,6 +69,8 @@ type AgentConfigFormProps = { showAdapterTestEnvironmentButton?: boolean; showCreateRunPolicySection?: boolean; hideInstructionsFile?: boolean; + /** Hide the prompt template field from the Identity section (used when it's shown in a separate Prompts tab). */ + hidePromptTemplate?: boolean; /** "cards" renders each section as heading + bordered card (for settings pages). Default: "inline" (border-b dividers). */ sectionLayout?: "inline" | "cards"; } & ( @@ -483,7 +485,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { }} /> - {isLocal && ( + {isLocal && !props.hidePromptTemplate && ( <> ; } const isPendingApproval = agent.status === "pending_approval"; - const showConfigActionBar = activeView === "configuration" && (configDirty || configSaving); + const showConfigActionBar = (activeView === "configuration" || activeView === "prompts") && (configDirty || configSaving); return (
@@ -867,9 +874,9 @@ export function AgentDetail() { )} + {activeView === "prompts" && ( + + )} + {activeView === "configuration" && (

API Keys

@@ -1351,6 +1370,7 @@ function ConfigurationTab({ onCancelActionChange, onSavingChange, updatePermissions, + hidePromptTemplate, }: { agent: Agent; companyId?: string; @@ -1359,6 +1379,7 @@ function ConfigurationTab({ onCancelActionChange: (cancel: (() => void) | null) => void; onSavingChange: (saving: boolean) => void; updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean }; + hidePromptTemplate?: boolean; }) { const queryClient = useQueryClient(); const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false); @@ -1412,6 +1433,7 @@ function ConfigurationTab({ onSaveActionChange={onSaveActionChange} onCancelActionChange={onCancelActionChange} hideInlineSave + hidePromptTemplate={hidePromptTemplate} sectionLayout="cards" /> @@ -1438,6 +1460,119 @@ function ConfigurationTab({ ); } +/* ---- Prompts Tab ---- */ + +function PromptsTab({ + agent, + companyId, + onDirtyChange, + onSaveActionChange, + onCancelActionChange, + onSavingChange, +}: { + agent: Agent; + companyId?: string; + onDirtyChange: (dirty: boolean) => void; + onSaveActionChange: (save: (() => void) | null) => void; + onCancelActionChange: (cancel: (() => void) | null) => void; + onSavingChange: (saving: boolean) => void; +}) { + const queryClient = useQueryClient(); + const { selectedCompanyId } = useCompany(); + const [draft, setDraft] = useState(null); + const [awaitingRefresh, setAwaitingRefresh] = useState(false); + const lastAgentRef = useRef(agent); + + const currentValue = String(agent.adapterConfig?.promptTemplate ?? ""); + const displayValue = draft ?? currentValue; + const isDirty = draft !== null && draft !== currentValue; + + const isLocal = + agent.adapterType === "claude_local" || + agent.adapterType === "codex_local" || + agent.adapterType === "opencode_local" || + agent.adapterType === "pi_local" || + agent.adapterType === "hermes_local" || + agent.adapterType === "cursor"; + + const updateAgent = useMutation({ + mutationFn: (data: Record) => agentsApi.update(agent.id, data, companyId), + onMutate: () => setAwaitingRefresh(true), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); + queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); + }, + onError: () => setAwaitingRefresh(false), + }); + + const uploadMarkdownImage = useMutation({ + mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => { + if (!selectedCompanyId) throw new Error("Select a company to upload images"); + return assetsApi.uploadImage(selectedCompanyId, file, namespace); + }, + }); + + useEffect(() => { + if (awaitingRefresh && agent !== lastAgentRef.current) { + setAwaitingRefresh(false); + setDraft(null); + } + lastAgentRef.current = agent; + }, [agent, awaitingRefresh]); + + const isSaving = updateAgent.isPending || awaitingRefresh; + + useEffect(() => { onSavingChange(isSaving); }, [onSavingChange, isSaving]); + useEffect(() => { onDirtyChange(isDirty); }, [onDirtyChange, isDirty]); + + useEffect(() => { + onSaveActionChange(isDirty ? () => { + updateAgent.mutate({ adapterConfig: { promptTemplate: draft } }); + } : null); + }, [onSaveActionChange, isDirty, draft, updateAgent]); + + useEffect(() => { + onCancelActionChange(isDirty ? () => setDraft(null) : null); + }, [onCancelActionChange, isDirty]); + + if (!isLocal) { + return ( +
+

+ Prompt templates are only available for local adapters. +

+
+ ); + } + + return ( +
+
+

Prompt Template

+
+

+ {help.promptTemplate} +

+ setDraft(v ?? "")} + placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..." + contentClassName="min-h-[88px] text-sm font-mono" + imageUploadHandler={async (file) => { + const namespace = `agents/${agent.id}/prompt-template`; + const asset = await uploadMarkdownImage.mutateAsync({ file, namespace }); + return asset.contentPath; + }} + /> +
+ Prompt template is replayed on every heartbeat. Keep it compact and dynamic to avoid recurring token cost and cache churn. +
+
+
+
+ ); +} + function AgentSkillsTab({ agent, companyId,