diff --git a/packages/views/agents/components/agent-detail.tsx b/packages/views/agents/components/agent-detail.tsx index 26daa5839..de3563537 100644 --- a/packages/views/agents/components/agent-detail.tsx +++ b/packages/views/agents/components/agent-detail.tsx @@ -11,6 +11,7 @@ import { AlertCircle, MoreHorizontal, Settings, + KeyRound, } from "lucide-react"; import type { Agent, RuntimeDevice } from "@multica/core/types"; import { @@ -34,17 +35,19 @@ import { InstructionsTab } from "./tabs/instructions-tab"; import { SkillsTab } from "./tabs/skills-tab"; import { TasksTab } from "./tabs/tasks-tab"; import { SettingsTab } from "./tabs/settings-tab"; +import { EnvTab } from "./tabs/env-tab"; function getRuntimeDevice(agent: Agent, runtimes: RuntimeDevice[]): RuntimeDevice | undefined { return runtimes.find((runtime) => runtime.id === agent.runtime_id); } -type DetailTab = "instructions" | "skills" | "tasks" | "settings"; +type DetailTab = "instructions" | "skills" | "tasks" | "env" | "settings"; const detailTabs: { id: DetailTab; label: string; icon: typeof FileText }[] = [ { id: "instructions", label: "Instructions", icon: FileText }, { id: "skills", label: "Skills", icon: BookOpenText }, { id: "tasks", label: "Tasks", icon: ListTodo }, + { id: "env", label: "Environment", icon: KeyRound }, { id: "settings", label: "Settings", icon: Settings }, ]; @@ -158,6 +161,12 @@ export function AgentDetail({ )} {activeTab === "tasks" && } + {activeTab === "env" && ( + onUpdate(agent.id, updates)} + /> + )} {activeTab === "settings" && ( ): EnvEntry[] { + return Object.entries(env).map(([key, value]) => ({ + id: nextEnvId++, + key, + value, + visible: false, + })); +} + +function entriesToEnvMap(entries: EnvEntry[]): Record { + const map: Record = {}; + for (const entry of entries) { + const key = entry.key.trim(); + if (key) { + map[key] = entry.value; + } + } + return map; +} + +export function EnvTab({ + agent, + onSave, +}: { + agent: Agent; + onSave: (updates: Partial) => Promise; +}) { + const [envEntries, setEnvEntries] = useState( + envMapToEntries(agent.custom_env ?? {}), + ); + const [saving, setSaving] = useState(false); + + const currentEnvMap = entriesToEnvMap(envEntries); + const originalEnvMap = agent.custom_env ?? {}; + const dirty = + JSON.stringify(currentEnvMap) !== JSON.stringify(originalEnvMap); + + const addEnvEntry = () => { + setEnvEntries([ + ...envEntries, + { id: nextEnvId++, key: "", value: "", visible: true }, + ]); + }; + + const removeEnvEntry = (index: number) => { + setEnvEntries(envEntries.filter((_, i) => i !== index)); + }; + + const updateEnvEntry = ( + index: number, + field: "key" | "value", + val: string, + ) => { + setEnvEntries( + envEntries.map((entry, i) => + i === index ? { ...entry, [field]: val } : entry, + ), + ); + }; + + const toggleEnvVisibility = (index: number) => { + setEnvEntries( + envEntries.map((entry, i) => + i === index ? { ...entry, visible: !entry.visible } : entry, + ), + ); + }; + + const handleSave = async () => { + const keys = envEntries.filter((e) => e.key.trim()).map((e) => e.key.trim()); + const uniqueKeys = new Set(keys); + if (uniqueKeys.size < keys.length) { + toast.error("Duplicate environment variable keys"); + return; + } + + setSaving(true); + try { + await onSave({ custom_env: currentEnvMap }); + toast.success("Environment variables saved"); + } catch { + toast.error("Failed to save environment variables"); + } finally { + setSaving(false); + } + }; + + return ( + + + + + Environment Variables + + + Injected into the agent process at launch (e.g. ANTHROPIC_API_KEY, + ANTHROPIC_BASE_URL) + + + + + Add + + + {envEntries.length > 0 && ( + + {envEntries.map((entry, index) => ( + + updateEnvEntry(index, "key", e.target.value)} + placeholder="KEY" + className="w-[40%] font-mono text-xs" + /> + + + updateEnvEntry(index, "value", e.target.value) + } + placeholder="value" + className="pr-8 font-mono text-xs" + /> + toggleEnvVisibility(index)} + className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" + > + {entry.visible ? ( + + ) : ( + + )} + + + removeEnvEntry(index)} + className="shrink-0 text-muted-foreground hover:text-destructive" + > + + + + ))} + + )} + + + {saving ? ( + + ) : ( + + )} + Save + + + ); +} diff --git a/packages/views/agents/components/tabs/settings-tab.tsx b/packages/views/agents/components/tabs/settings-tab.tsx index 7ba8e814c..dc69dc853 100644 --- a/packages/views/agents/components/tabs/settings-tab.tsx +++ b/packages/views/agents/components/tabs/settings-tab.tsx @@ -10,10 +10,6 @@ import { Lock, Camera, ChevronDown, - Plus, - Trash2, - Eye, - EyeOff, } from "lucide-react"; import type { Agent, AgentVisibility, RuntimeDevice } from "@multica/core/types"; import { @@ -29,35 +25,6 @@ import { api } from "@multica/core/api"; import { useFileUpload } from "@multica/core/hooks/use-file-upload"; import { ActorAvatar } from "../../../common/actor-avatar"; -let nextEnvId = 0; - -interface EnvEntry { - id: number; - key: string; - value: string; - visible: boolean; -} - -function envMapToEntries(env: Record): EnvEntry[] { - return Object.entries(env).map(([key, value]) => ({ - id: nextEnvId++, - key, - value, - visible: false, - })); -} - -function entriesToEnvMap(entries: EnvEntry[]): Record { - const map: Record = {}; - for (const entry of entries) { - const key = entry.key.trim(); - if (key) { - map[key] = entry.value; - } - } - return map; -} - export function SettingsTab({ agent, runtimes, @@ -74,9 +41,6 @@ export function SettingsTab({ const [selectedRuntimeId, setSelectedRuntimeId] = useState(agent.runtime_id); const [runtimeOpen, setRuntimeOpen] = useState(false); const [saving, setSaving] = useState(false); - const [envEntries, setEnvEntries] = useState( - envMapToEntries(agent.custom_env ?? {}) - ); const { upload, uploading } = useFileUpload(api); const fileInputRef = useRef(null); @@ -96,31 +60,18 @@ export function SettingsTab({ } }; - const currentEnvMap = entriesToEnvMap(envEntries); - const originalEnvMap = agent.custom_env ?? {}; - const envDirty = - JSON.stringify(currentEnvMap) !== JSON.stringify(originalEnvMap); - const dirty = name !== agent.name || description !== (agent.description ?? "") || visibility !== agent.visibility || maxTasks !== agent.max_concurrent_tasks || - selectedRuntimeId !== agent.runtime_id || - envDirty; + selectedRuntimeId !== agent.runtime_id; const handleSave = async () => { if (!name.trim()) { toast.error("Name is required"); return; } - // Validate env var keys - const keys = envEntries.filter((e) => e.key.trim()).map((e) => e.key.trim()); - const uniqueKeys = new Set(keys); - if (uniqueKeys.size < keys.length) { - toast.error("Duplicate environment variable keys"); - return; - } setSaving(true); try { @@ -130,7 +81,6 @@ export function SettingsTab({ visibility, max_concurrent_tasks: maxTasks, runtime_id: selectedRuntimeId, - custom_env: currentEnvMap, }); toast.success("Settings saved"); } catch { @@ -140,34 +90,6 @@ export function SettingsTab({ } }; - const addEnvEntry = () => { - setEnvEntries([...envEntries, { id: nextEnvId++, key: "", value: "", visible: true }]); - }; - - const removeEnvEntry = (index: number) => { - setEnvEntries(envEntries.filter((_, i) => i !== index)); - }; - - const updateEnvEntry = ( - index: number, - field: "key" | "value", - val: string - ) => { - setEnvEntries( - envEntries.map((entry, i) => - i === index ? { ...entry, [field]: val } : entry - ) - ); - }; - - const toggleEnvVisibility = (index: number) => { - setEnvEntries( - envEntries.map((entry, i) => - i === index ? { ...entry, visible: !entry.visible } : entry - ) - ); - }; - return ( @@ -336,71 +258,6 @@ export function SettingsTab({ - {/* Environment Variables */} - - - - Environment Variables - - Injected into the agent process at launch (e.g. ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL) - - - - - Add - - - {envEntries.length > 0 && ( - - {envEntries.map((entry, index) => ( - - updateEnvEntry(index, "key", e.target.value)} - placeholder="KEY" - className="w-[40%] font-mono text-xs" - /> - - - updateEnvEntry(index, "value", e.target.value) - } - placeholder="value" - className="pr-8 font-mono text-xs" - /> - toggleEnvVisibility(index)} - className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" - > - {entry.visible ? ( - - ) : ( - - )} - - - removeEnvEntry(index)} - className="shrink-0 text-muted-foreground hover:text-destructive" - > - - - - ))} - - )} - - {saving ? : } Save Changes
+ Injected into the agent process at launch (e.g. ANTHROPIC_API_KEY, + ANTHROPIC_BASE_URL) +
- Injected into the agent process at launch (e.g. ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL) -