From c62bd0ca12e934e520903b7014a64067dd7af2ef Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Tue, 14 Apr 2026 16:06:06 +0800 Subject: [PATCH] feat(views): extract environment variables into separate agent tab Move the Environment Variables section from the Settings tab into its own "Environment" tab (KeyRound icon) between Tasks and Settings. Each tab now has independent save state. --- .../views/agents/components/agent-detail.tsx | 11 +- .../views/agents/components/tabs/env-tab.tsx | 191 ++++++++++++++++++ .../agents/components/tabs/settings-tab.tsx | 145 +------------ 3 files changed, 202 insertions(+), 145 deletions(-) create mode 100644 packages/views/agents/components/tabs/env-tab.tsx 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 ( +
+
+
+ +

+ Injected into the agent process at launch (e.g. ANTHROPIC_API_KEY, + ANTHROPIC_BASE_URL) +

+
+ +
+ {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" + /> + +
+ +
+ ))} +
+ )} + + +
+ ); +} 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 */} -
-
-
- -

- Injected into the agent process at launch (e.g. ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL) -

-
- -
- {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" - /> - -
- -
- ))} -
- )} -
-