AkurAI Build
Menu

popagent

public

Latest change 1ce5ab5f17eefb3cef070f9d98b28f2bb1e5c879 - Secure agent secrets behind origin-bound browser use by AkurAI Build

import { useCallback, useEffect, useRef, useState } from "react";
import { Activity, Bot, Brain, Clock3, Globe, Hash, KeyRound, Moon, Palette, Plus, Settings2, SlidersHorizontal, Sun, Trash2, X } from "lucide-react";
import { apiFetch } from "./api";
import { useFocusTrap } from "./use-focus-trap";
import {
  AGENT_TOOL_NAMES,
  MAX_AGENT_INSTRUCTIONS_CHARACTERS,
  type AgentListResponse,
  type AgentBrowserAccess,
  type AgentRuntimeSettings,
  type AgentWorkspace,
  type AgentSchedule,
  type AgentSettings,
  type AgentSkill,
  type AgentSkillListResponse,
  type AgentToolName,
  type AgentTask,
  type BrowserProfile,
  type BrowserSession,
  type BrowserSettings,
  type ModelCatalogResponse,
  type ScheduleListResponse,
  type TaskListResponse,
  type AgentWorkspaceAccess,
} from "../api-types";
import { describeSchedule, scheduleCron, WEEKDAYS, type ScheduleFrequency } from "./schedules";
import { APPEARANCE_THEMES, preferredTheme, saveTheme, type AppearanceTheme } from "./appearance";
import type { SettingsSection } from "./routes";
import { ObservabilityPage } from "./ObservabilityPage";
import { ChannelSettingsPanel } from "./ChannelSettingsPanel";
import { MemorySettingsPanel } from "./MemorySettingsPanel";
import { SecretSettingsPanel } from "./SecretSettingsPanel";

function RuntimeNumberField({
  label,
  value,
  min,
  max,
  step = 1,
  onChange,
}: {
  label: string;
  value: number;
  min: number;
  max: number;
  step?: number;
  onChange: (value: number) => void;
}) {
  return <label className="text-sm font-medium">{label}<input aria-label={label} type="number" min={min} max={max} step={step} value={value} onChange={(event) => onChange(event.currentTarget.valueAsNumber)} className="mt-2 min-h-11 w-full rounded border border-line bg-bg px-3 text-ink outline-none focus:border-accent/50" /></label>;
}

export function AgentSettingsPage({
  open,
  section,
  onSectionChange,
  onClose,
  workspaces,
  workspaceId,
}: {
  open: boolean;
  section: SettingsSection;
  onSectionChange: (section: SettingsSection) => void;
  onClose: () => void;
  workspaces: AgentWorkspace[];
  workspaceId?: string;
}) {
  const [theme, setTheme] = useState<AppearanceTheme>(preferredTheme);
  const [agents, setAgents] = useState<AgentSettings[]>([]);
  const [selectedId, setSelectedId] = useState<string>();
  const [agentName, setAgentName] = useState("");
  const [agentDescription, setAgentDescription] = useState("");
  const [instructions, setInstructions] = useState("");
  const [agentWorkspaceAccess, setAgentWorkspaceAccess] = useState<AgentWorkspaceAccess>("none");
  const [agentBrowserAccess, setAgentBrowserAccess] = useState<AgentBrowserAccess>("none");
  const [agentDelegationEnabled, setAgentDelegationEnabled] = useState(false);
  const [agentTools, setAgentTools] = useState<AgentToolName[]>([]);
  const [agentPanel, setAgentPanel] = useState<"instructions" | "skills">("instructions");
  const [skills, setSkills] = useState<AgentSkill[]>([]);
  const [editingSkill, setEditingSkill] = useState<AgentSkill>();
  const [skillName, setSkillName] = useState("");
  const [skillDescription, setSkillDescription] = useState("");
  const [skillInstructions, setSkillInstructions] = useState("");
  const [skillReferences, setSkillReferences] = useState("{}");
  const [skillEnabled, setSkillEnabled] = useState(true);
  const [skillUserInvocable, setSkillUserInvocable] = useState(true);
  const [skillError, setSkillError] = useState("");
  const [browserSettings, setBrowserSettings] = useState<BrowserSettings>();
  const [browserSessions, setBrowserSessions] = useState<BrowserSession[]>([]);
  const [browserProfiles, setBrowserProfiles] = useState<BrowserProfile[]>([]);
  const [browserHealth, setBrowserHealth] = useState<{ provider: string; healthy: boolean; headless: boolean; activeSessions: number }>();
  const [browserRecordings, setBrowserRecordings] = useState<Array<{ name: string; size: number; createdAt: string }>>([]);
  const [profileName, setProfileName] = useState("");
  const [profileState, setProfileState] = useState("{\n  \"cookies\": [],\n  \"origins\": []\n}");
  const [profileError, setProfileError] = useState("");
  const [testingBrowser, setTestingBrowser] = useState(false);
  const [runtimeSettings, setRuntimeSettings] = useState<AgentRuntimeSettings>();
  const [runtimeModels, setRuntimeModels] = useState<string[]>([]);
  const [tasks, setTasks] = useState<AgentTask[]>([]);
  const [schedules, setSchedules] = useState<AgentSchedule[]>([]);
  const [taskWorkspaceId, setTaskWorkspaceId] = useState(workspaceId ?? workspaces[0]?.id ?? "default");
  const [taskPrompt, setTaskPrompt] = useState("");
  const [scheduleName, setScheduleName] = useState("");
  const [schedulePrompt, setSchedulePrompt] = useState("");
  const [scheduleFrequency, setScheduleFrequency] = useState<ScheduleFrequency>("daily");
  const [scheduleTime, setScheduleTime] = useState("09:00");
  const [scheduleWeekday, setScheduleWeekday] = useState("1");
  const [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState(false);
  const closeRef = useRef<HTMLButtonElement>(null);
  const pageRef = useRef<HTMLDivElement>(null);
  const loadSkills = useCallback(async (agentId: string) => {
    const response = await apiFetch(`/api/agents/${agentId}/skills`);
    if (response.ok) setSkills(((await response.json()) as AgentSkillListResponse).skills);
  }, []);
  const loadTasks = useCallback(async (selectedWorkspaceId: string) => {
    const query = `?workspaceId=${encodeURIComponent(selectedWorkspaceId)}`;
    const [taskResponse, scheduleResponse] = await Promise.all([apiFetch(`/api/tasks${query}`), apiFetch(`/api/schedules${query}`)]);
    if (taskResponse.ok) setTasks(((await taskResponse.json()) as TaskListResponse).tasks);
    if (scheduleResponse.ok) setSchedules(((await scheduleResponse.json()) as ScheduleListResponse).schedules);
  }, []);
  useEffect(() => {
    if (!open) return;
    const selectedWorkspaceId = workspaceId ?? workspaces[0]?.id ?? "default";
    setTaskWorkspaceId(selectedWorkspaceId);
    void loadTasks(selectedWorkspaceId);
  }, [loadTasks, open, workspaceId, workspaces]);
  useEffect(() => {
    if (!open) return;
    void Promise.all([
      apiFetch("/api/agents"),
      apiFetch("/api/settings/browser"),
      apiFetch("/api/settings/agent-runtime"),
      apiFetch("/api/models"),
      apiFetch("/api/browser/sessions"),
      apiFetch("/api/browser/profiles"),
      apiFetch("/api/browser/recordings"),
      apiFetch("/api/browser/health"),
    ]).then(async ([agentResponse, browserResponse, runtimeResponse, modelResponse, browserSessionsResponse, browserProfilesResponse, browserRecordingsResponse, browserHealthResponse]) => {
      if (agentResponse.ok) {
        const loaded = ((await agentResponse.json()) as AgentListResponse).agents;
        const flowOrder: Record<string, number> = { popagent: 0, researcher: 1, implementer: 2, reviewer: 3 };
        const ordered = [...loaded].sort((a, b) => (flowOrder[a.id] ?? 99) - (flowOrder[b.id] ?? 99));
        setAgents(ordered);
        if (ordered[0]) {
          setSelectedId(ordered[0].id);
          setAgentName(ordered[0].name);
          setAgentDescription(ordered[0].description);
          setInstructions(ordered[0].instructions);
          setAgentWorkspaceAccess(ordered[0].workspaceAccess);
          setAgentBrowserAccess(ordered[0].browserAccess);
          setAgentDelegationEnabled(ordered[0].delegationEnabled);
          setAgentTools(ordered[0].tools);
          void loadSkills(ordered[0].id);
        }
      }
      if (browserResponse.ok) setBrowserSettings(await browserResponse.json() as BrowserSettings);
      if (browserSessionsResponse.ok) setBrowserSessions(((await browserSessionsResponse.json()) as { sessions: BrowserSession[] }).sessions);
      if (browserProfilesResponse.ok) setBrowserProfiles(((await browserProfilesResponse.json()) as { profiles: BrowserProfile[] }).profiles);
      if (browserRecordingsResponse.ok) setBrowserRecordings(((await browserRecordingsResponse.json()) as { recordings: Array<{ name: string; size: number; createdAt: string }> }).recordings);
      if (browserHealthResponse.ok) setBrowserHealth(await browserHealthResponse.json() as { provider: string; healthy: boolean; headless: boolean; activeSessions: number });
      if (runtimeResponse.ok) setRuntimeSettings(await runtimeResponse.json() as AgentRuntimeSettings);
      if (modelResponse.ok) setRuntimeModels(((await modelResponse.json()) as ModelCatalogResponse).models);
    });
  }, [loadSkills, open]);
  const selected = agents.find((item) => item.id === selectedId);
  const choose = (item: AgentSettings) => {
    setSelectedId(item.id);
    setAgentName(item.name);
    setAgentDescription(item.description);
    setInstructions(item.instructions);
    setAgentWorkspaceAccess(item.workspaceAccess);
    setAgentBrowserAccess(item.browserAccess);
    setAgentDelegationEnabled(item.delegationEnabled);
    setAgentTools(item.tools);
    setSaved(false);
    void loadSkills(item.id);
  };
  const saveInstructions = async () => {
    if (!selected) return;
    setSaving(true);
    const response = await apiFetch(`/api/agents/${selected.id}`, {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        name: agentName,
        description: agentDescription,
        instructions,
        workspaceAccess: agentWorkspaceAccess,
        browserAccess: agentBrowserAccess,
        delegationEnabled: agentDelegationEnabled,
        tools: agentTools,
      }),
    });
    if (response.ok) {
      const updated = (await response.json()) as AgentSettings;
      setAgents((current) => current.map((item) => item.id === updated.id ? updated : item));
      setAgentName(updated.name);
      setAgentDescription(updated.description);
      setAgentWorkspaceAccess(updated.workspaceAccess);
      setAgentBrowserAccess(updated.browserAccess);
      setAgentDelegationEnabled(updated.delegationEnabled);
      setAgentTools(updated.tools);
      setSaved(true);
    }
    setSaving(false);
  };
  const toggleAgentTool = (tool: AgentToolName) => {
    setAgentTools((current) => current.includes(tool)
      ? current.filter((candidate) => candidate !== tool)
      : [...current, tool]);
    setSaved(false);
  };
  const editSkill = (skill?: AgentSkill) => {
    setEditingSkill(skill);
    setSkillName(skill?.name ?? "");
    setSkillDescription(skill?.description ?? "");
    setSkillInstructions(skill?.instructions ?? "");
    setSkillReferences(JSON.stringify(skill?.references ?? {}, null, 2));
    setSkillEnabled(skill?.enabled ?? true);
    setSkillUserInvocable(skill?.userInvocable ?? true);
    setSkillError("");
    setSaved(false);
  };
  const saveSkill = async () => {
    if (!selected) return;
    let references: Record<string, string>;
    try {
      const parsed: unknown = JSON.parse(skillReferences);
      if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || Object.values(parsed).some((value) => typeof value !== "string")) throw new Error();
      references = Object.fromEntries(Object.entries(parsed).map(([key, value]) => [key, String(value)]));
    } catch {
      setSkillError("References must be a JSON object whose values are strings.");
      return;
    }
    setSaving(true);
    setSaved(false);
    const response = await apiFetch(editingSkill ? `/api/agents/${selected.id}/skills/${editingSkill.id}` : `/api/agents/${selected.id}/skills`, {
      method: editingSkill ? "PATCH" : "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ name: skillName, description: skillDescription, instructions: skillInstructions, references, enabled: skillEnabled, userInvocable: skillUserInvocable }),
    });
    if (response.ok) {
      await loadSkills(selected.id);
      setEditingSkill(await response.json() as AgentSkill);
      setSaved(true);
    } else {
      setSkillError("Unable to save skill. Names must be unique lowercase words separated by hyphens.");
    }
    setSaving(false);
  };
  const deleteSkill = async () => {
    if (!selected || !editingSkill || !confirm(`Delete the “${editingSkill.name}” skill?`)) return;
    const response = await apiFetch(`/api/agents/${selected.id}/skills/${editingSkill.id}`, { method: "DELETE" });
    if (response.ok) {
      await loadSkills(selected.id);
      editSkill();
    }
  };
  const saveBrowser = async () => {
    if (!browserSettings) return;
    setSaving(true);
    const { updatedAt, ...input } = browserSettings;
    void updatedAt;
    const response = await apiFetch("/api/settings/browser", {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(input),
    });
    if (response.ok) {
      setBrowserSettings(await response.json() as BrowserSettings);
      setSaved(true);
    }
    setSaving(false);
  };
  const updateBrowser = <K extends keyof BrowserSettings>(key: K, value: BrowserSettings[K]) => {
    setBrowserSettings((current) => current ? { ...current, [key]: value } : current);
    setSaved(false);
  };
  const createBrowserProfile = async () => {
    setProfileError("");
    let state: unknown;
    try { state = JSON.parse(profileState); } catch { setProfileError("Storage state must be valid JSON."); return; }
    const response = await apiFetch("/api/browser/profiles", {
      method: "POST", headers: { "content-type": "application/json" },
      body: JSON.stringify({ name: profileName, enabled: browserProfiles.length === 0, state }),
    });
    if (!response.ok) { setProfileError("Unable to save profile. Use a Playwright storage-state object."); return; }
    const profile = await response.json() as BrowserProfile;
    setBrowserProfiles((current) => [profile, ...current]);
    setProfileName("");
    setSaved(true);
  };
  const downloadBrowserRecording = async (name: string) => {
    const response = await apiFetch(`/api/browser/recordings/${encodeURIComponent(name)}`);
    if (!response.ok) return;
    const url = URL.createObjectURL(await response.blob());
    const anchor = document.createElement("a");
    anchor.href = url;
    anchor.download = name;
    anchor.click();
    URL.revokeObjectURL(url);
  };
  const setActiveBrowserProfile = async (profile: BrowserProfile, enabled: boolean) => {
    const response = await apiFetch(`/api/browser/profiles/${profile.id}`, {
      method: "PATCH", headers: { "content-type": "application/json" },
      body: JSON.stringify({ name: profile.name, enabled }),
    });
    if (response.ok) {
      const updated = await response.json() as BrowserProfile;
      setBrowserProfiles((current) => current.map((item) => item.id === updated.id ? updated : { ...item, enabled: false }));
    }
  };
  const createTask = async () => {
    if (!taskPrompt.trim()) return;
    const response = await apiFetch("/api/tasks", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ prompt: taskPrompt, workspaceId: taskWorkspaceId }) });
    if (response.ok) { setTaskPrompt(""); await loadTasks(taskWorkspaceId); }
  };
  const createSchedule = async () => {
    if (!scheduleName.trim() || !schedulePrompt.trim()) return;
    const cron = scheduleCron(scheduleFrequency, scheduleTime, scheduleWeekday);
    const response = await apiFetch("/api/schedules", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: scheduleName, prompt: schedulePrompt, cron, enabled: true, workspaceId: taskWorkspaceId }) });
    if (response.ok) { setScheduleName(""); setSchedulePrompt(""); await loadTasks(taskWorkspaceId); }
  };
  const saveRuntime = async () => {
    if (!runtimeSettings) return;
    setSaving(true);
    const { updatedAt, ...input } = runtimeSettings;
    void updatedAt;
    const response = await apiFetch("/api/settings/agent-runtime", {
      method: "PATCH",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(input),
    });
    if (response.ok) {
      setRuntimeSettings(await response.json() as AgentRuntimeSettings);
      setSaved(true);
    }
    setSaving(false);
  };
  const changeRuntime = <Key extends keyof AgentRuntimeSettings>(
    key: Key,
    value: AgentRuntimeSettings[Key],
  ) => {
    setRuntimeSettings((current) => current ? { ...current, [key]: value } : current);
    setSaved(false);
  };
  const toggleSchedule = async (schedule: AgentSchedule) => {
    await apiFetch(`/api/schedules/${schedule.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...schedule, enabled: !schedule.enabled }) });
    await loadTasks(taskWorkspaceId);
  };
  const deleteSchedule = async (id: string) => {
    await apiFetch(`/api/schedules/${id}`, { method: "DELETE" });
    await loadTasks(taskWorkspaceId);
  };
  useEffect(() => {
    if (!open || section !== "tasks") return;
    const timer = setInterval(() => void loadTasks(taskWorkspaceId), 2000);
    return () => clearInterval(timer);
  }, [loadTasks, open, section, taskWorkspaceId]);
  useEffect(() => {
    if (!open) return;
    closeRef.current?.focus();
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape") onClose();
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [onClose, open]);
  useFocusTrap(pageRef, open);
  if (!open) return null;
  const taskStepLimit = runtimeSettings?.supervisorMaxSteps ?? 1;
  return (
    <div ref={pageRef} role="dialog" aria-modal="true" aria-labelledby="agent-settings-title" className="fixed inset-0 z-50 flex flex-col bg-bg">
      <header className="flex min-h-14 items-center border-b border-line bg-panel px-4">
        <div id="agent-settings-title" className="flex items-center gap-2 font-semibold"><Settings2 className="size-4 text-accent" />Settings</div>
        <button ref={closeRef} type="button" aria-label="Close settings" onClick={onClose} className="ml-auto flex size-11 items-center justify-center rounded hover:bg-panel-2"><X className="size-4" /></button>
      </header>
      <div className="flex min-h-0 flex-1 flex-col md:flex-row">
        <nav aria-label="Settings sections" className="grid shrink-0 grid-cols-4 border-b border-line bg-panel p-2 sm:grid-cols-9 md:block md:w-56 md:border-b-0 md:border-r md:p-3">
          <button type="button" onClick={() => { onSectionChange("appearance"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "appearance" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><Palette className="size-4 text-accent" />Appearance</button>
          <button type="button" onClick={() => { onSectionChange("agents"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "agents" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><Bot className="size-4 text-accent" />Agents</button>
          <button type="button" onClick={() => { onSectionChange("channels"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "channels" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><Hash className="size-4 text-accent" />Channels</button>
          <button type="button" onClick={() => { onSectionChange("browser"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "browser" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><Globe className="size-4 text-accent" />Browser</button>
          <button type="button" onClick={() => { onSectionChange("secrets"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "secrets" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><KeyRound className="size-4 text-accent" />Secrets</button>
          <button type="button" onClick={() => { onSectionChange("memory"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "memory" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><Brain className="size-4 text-accent" />Memory</button>
          <button type="button" onClick={() => { onSectionChange("runtime"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "runtime" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><SlidersHorizontal className="size-4 text-accent" />Runtime</button>
          <button type="button" onClick={() => { onSectionChange("tasks"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "tasks" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><Clock3 className="size-4 text-accent" />Tasks</button>
          <button type="button" onClick={() => { onSectionChange("observability"); setSaved(false); }} className={`flex min-h-11 w-full items-center justify-center gap-1 rounded px-1 text-xs sm:gap-2 sm:px-3 sm:text-sm md:justify-start ${section === "observability" ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`}><Activity className="size-4 text-accent" />Observe</button>
        </nav>
        <main className="min-w-0 flex-1 overflow-y-auto p-4 md:p-8">
          {section === "appearance" ? <div className="w-full max-w-3xl">
            <h1 className="text-xl font-semibold">Appearance</h1>
            <p className="mt-1 text-sm text-ink-dim">Choose how Popagent looks on this device. Your selection is saved in this browser.</p>
            <section className="mt-6 border border-line bg-panel p-5">
              <h2 className="font-medium">Theme</h2>
              <div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3" role="radiogroup" aria-label="Color theme">
                {APPEARANCE_THEMES.map((option) => {
                  const selected = theme === option.id;
                  const Icon = option.id === "light" || option.id === "solarized-light" ? Sun : option.id === "dark" ? Moon : Palette;
                  return <button key={option.id} type="button" role="radio" aria-checked={selected} aria-label={`${option.label} theme`} onClick={() => { setTheme(option.id); saveTheme(option.id); }} className={`theme-choice theme-choice--${option.id} ${selected ? "is-selected" : ""}`}>
                    <span className="theme-preview" aria-hidden="true"><span className="theme-preview-sidebar" /><span className="theme-preview-content"><span /><span /><span /></span></span>
                    <span className="flex items-center gap-2 px-3 py-3 text-left text-sm font-medium"><Icon className="size-4" /><span>{option.label}</span><span className="theme-check ml-auto" /></span>
                  </button>;
                })}
              </div>
            </section>
          </div> : section === "agents" ? <div className="w-full">
            <h1 className="text-xl font-semibold">Agents</h1>
            <p className="mt-1 text-sm text-ink-dim">The orchestrator owns each request and delegates focused work to enabled specialists.</p>
            {agents.length ? <section className="agent-flow mt-6" aria-label="Agent handoff flow">
              {agents.map((item, index) => <div className="agent-flow-step" key={item.id}>
                {index > 0 ? <span className="agent-flow-arrow" aria-hidden="true">→</span> : null}
                <button type="button" onClick={() => choose(item)} className={`agent-flow-node ${item.id === selectedId ? "is-selected" : ""} ${item.id !== "popagent" && !item.delegationEnabled ? "is-disabled" : ""}`}>
                  <span className={`text-[10px] font-semibold uppercase tracking-widest ${item.id === "popagent" ? "text-accent" : "text-ink-dim"}`}>{item.id === "popagent" ? "Orchestrator" : "Specialist"}</span>
                  <strong>{item.name}</strong>
                  <span>{item.id === "popagent" ? "Coordinates" : item.delegationEnabled ? "Enabled" : "Disabled"}</span>
                </button>
              </div>)}
            </section> : null}
            <div className="mt-5">
              {selected ? <section className="rounded border border-line bg-panel p-4">
                <div className="mb-4 flex gap-1 border-b border-line pb-3">
                  <button type="button" onClick={() => { setAgentPanel("instructions"); setSaved(false); }} className={`min-h-11 rounded px-3 text-sm ${agentPanel === "instructions" ? "bg-accent-soft text-ink" : "text-ink-muted"}`}>Instructions</button>
                  <button type="button" onClick={() => { setAgentPanel("skills"); setSaved(false); }} className={`min-h-11 rounded px-3 text-sm ${agentPanel === "skills" ? "bg-accent-soft text-ink" : "text-ink-muted"}`}>Skills</button>
                </div>
                {agentPanel === "instructions" ? <>
                  <div className="grid gap-3 sm:grid-cols-2">
                    <label className="text-sm font-medium">Display name<input aria-label="Agent display name" maxLength={120} value={agentName} onChange={(event) => { setAgentName(event.currentTarget.value); setSaved(false); }} className="mt-2 min-h-11 w-full rounded border border-line bg-bg px-3 text-ink outline-none focus:border-accent/50" /></label>
                    <label className="text-sm font-medium">Delegation description<input aria-label="Agent delegation description" maxLength={1024} value={agentDescription} onChange={(event) => { setAgentDescription(event.currentTarget.value); setSaved(false); }} className="mt-2 min-h-11 w-full rounded border border-line bg-bg px-3 text-ink outline-none focus:border-accent/50" /></label>
                  </div>
                  <div className="mt-4 rounded border border-line bg-bg p-4">
                    <h3 className="text-sm font-medium">Capabilities</h3>
                    <p className="mt-1 text-xs leading-relaxed text-ink-dim">Capability changes apply on the next run. Writable workspace, interactive browser, and secret tools grant broader access.</p>
                    <div className="mt-3 grid gap-3 sm:grid-cols-2">
                      <label className="text-xs font-medium text-ink-dim">Workspace access<select aria-label="Agent workspace access" value={agentWorkspaceAccess} onChange={(event) => { setAgentWorkspaceAccess(event.currentTarget.value as AgentWorkspaceAccess); setSaved(false); }} className="mt-1 min-h-11 w-full rounded border border-line bg-panel px-3 text-sm text-ink outline-none"><option value="none">None</option><option value="read-only">Read only</option><option value="read-write">Read and write</option></select></label>
                      <label className="text-xs font-medium text-ink-dim">Browser access<select aria-label="Agent browser access" value={agentBrowserAccess} onChange={(event) => { setAgentBrowserAccess(event.currentTarget.value as AgentBrowserAccess); setSaved(false); }} className="mt-1 min-h-11 w-full rounded border border-line bg-panel px-3 text-sm text-ink outline-none"><option value="none">None</option><option value="read-only">Read only</option><option value="interactive">Interactive</option></select></label>
                    </div>
                    {selected.id === "popagent" ? null : <label className="mt-3 flex items-center gap-2 text-sm"><input aria-label="Enable agent delegation" type="checkbox" checked={agentDelegationEnabled} onChange={(event) => { setAgentDelegationEnabled(event.currentTarget.checked); setSaved(false); }} className="size-4 accent-accent" />Available for delegation</label>}
                    <fieldset className="mt-4"><legend className="text-xs font-medium text-ink-dim">Tools</legend><div className="mt-2 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">{AGENT_TOOL_NAMES.map((tool) => <label key={tool} className="flex min-h-10 items-center gap-2 rounded border border-line bg-panel px-3 text-sm"><input type="checkbox" checked={agentTools.includes(tool)} onChange={() => toggleAgentTool(tool)} className="size-4 accent-accent" />{tool}</label>)}</div></fieldset>
                  </div>
                  <label htmlFor="agent-instructions" className="mt-4 block text-sm font-medium">Instructions</label>
                  <p className="mt-1 text-xs text-ink-dim">Identity and instructions are stored in PostgreSQL and used on new runs. Maximum {MAX_AGENT_INSTRUCTIONS_CHARACTERS.toLocaleString()} instruction characters.</p>
                  {selected.sourceUrls.length ? <p className="mt-2 text-xs text-ink-dim">Adapted from {selected.sourceUrls.map((url, index) => <span key={url}>{index ? ", " : ""}<a href={url} target="_blank" rel="noreferrer" className="text-accent hover:underline">source {index + 1}</a></span>)}.</p> : null}
                  <textarea id="agent-instructions" maxLength={MAX_AGENT_INSTRUCTIONS_CHARACTERS} value={instructions} onChange={(event) => { setInstructions(event.currentTarget.value); setSaved(false); }} rows={16} className="mt-3 w-full resize-y rounded border border-line bg-bg p-3 text-sm leading-relaxed outline-none focus:border-accent/50" />
                  <div className="mt-3 flex items-center justify-end gap-3">{saved ? <span role="status" className="text-xs text-emerald-500">Saved</span> : null}<button type="button" disabled={saving || !agentName.trim() || !agentDescription.trim() || !instructions.trim()} onClick={() => void saveInstructions()} className="min-h-11 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-60">{saving ? "Saving…" : "Save agent"}</button></div>
                </> : <div className="grid gap-4 lg:grid-cols-[13rem_1fr]">
                  <div><button type="button" onClick={() => editSkill()} className="mb-2 min-h-11 w-full rounded border border-line bg-panel-2 px-3 text-left text-sm"><Plus className="mr-2 inline size-4 text-accent" />New skill</button>{skills.map((skill) => <button key={skill.id} type="button" onClick={() => editSkill(skill)} className={`mb-2 w-full rounded border px-3 py-2 text-left ${editingSkill?.id === skill.id ? "border-accent/40 bg-accent-soft" : "border-line bg-bg"}`}><span className="block truncate text-sm font-medium">{skill.name}</span><span className="text-xs text-ink-dim">{skill.enabled ? "Enabled" : "Disabled"}</span></button>)}</div>
                  <div>
                    {editingSkill?.sourceUrls.length ? <p className="mb-3 text-xs text-ink-dim">Adapted from {editingSkill.sourceUrls.map((url, index) => <span key={url}>{index ? ", " : ""}<a href={url} target="_blank" rel="noreferrer" className="text-accent hover:underline">source {index + 1}</a></span>)}.</p> : null}
                    <label className="text-xs font-medium text-ink-dim">Name<input aria-label="Skill name" value={skillName} onChange={(event) => setSkillName(event.currentTarget.value)} placeholder="release-checklist" className="mt-1 block w-full rounded border border-line bg-bg px-3 py-2 text-sm text-ink outline-none" /></label>
                    <label className="mt-3 block text-xs font-medium text-ink-dim">Description<input aria-label="Skill description" value={skillDescription} onChange={(event) => setSkillDescription(event.currentTarget.value)} placeholder="When this skill should be used" className="mt-1 block w-full rounded border border-line bg-bg px-3 py-2 text-sm text-ink outline-none" /></label>
                    <label className="mt-3 block text-xs font-medium text-ink-dim">Instructions<textarea aria-label="Skill instructions" value={skillInstructions} onChange={(event) => setSkillInstructions(event.currentTarget.value)} rows={10} className="mt-1 block w-full resize-y rounded border border-line bg-bg p-3 text-sm text-ink outline-none" /></label>
                    <label className="mt-3 block text-xs font-medium text-ink-dim">References JSON<textarea aria-label="Skill references" value={skillReferences} onChange={(event) => setSkillReferences(event.currentTarget.value)} rows={4} className="mt-1 block w-full resize-y rounded border border-line bg-bg p-3 font-mono text-xs text-ink outline-none" /></label>
                    <div className="mt-3 flex flex-wrap gap-5 text-sm"><label className="flex items-center gap-2"><input type="checkbox" checked={skillEnabled} onChange={(event) => setSkillEnabled(event.currentTarget.checked)} className="size-4 accent-accent" />Enabled</label><label className="flex items-center gap-2"><input type="checkbox" checked={skillUserInvocable} onChange={(event) => setSkillUserInvocable(event.currentTarget.checked)} className="size-4 accent-accent" />User invocable</label></div>
                    {skillError ? <p role="alert" className="mt-3 text-sm text-red-400">{skillError}</p> : null}
                    <div className="mt-4 flex items-center justify-end gap-2">{editingSkill ? <button type="button" onClick={() => void deleteSkill()} className="min-h-11 rounded border border-red-500/30 px-4 text-sm text-red-400">Delete</button> : null}{saved ? <span role="status" className="text-xs text-emerald-500">Saved</span> : null}<button type="button" disabled={saving || !skillName || !skillDescription || !skillInstructions} onClick={() => void saveSkill()} className="min-h-11 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-50">{saving ? "Saving…" : editingSkill ? "Save skill" : "Create skill"}</button></div>
                  </div>
                </div>}
              </section> : <div className="text-sm text-ink-dim">Loading agents…</div>}
            </div>
          </div> : section === "channels" ? <ChannelSettingsPanel /> : section === "browser" ? <div className="w-full max-w-5xl">
            <h1 className="text-xl font-semibold">Browser</h1>
            <p className="mt-1 text-sm text-ink-dim">Configure isolated Mastra AgentBrowser sessions and the live chat workspace.</p>
            {!browserSettings ? <p className="mt-6 text-sm text-ink-dim">Loading browser settings…</p> : <div className="mt-6 space-y-4">
              <section className="rounded border border-line bg-panel p-5">
                <label className="flex items-start justify-between gap-4"><span><span className="block font-medium">Enable browser tools</span><span className="mt-1 block text-sm leading-relaxed text-ink-dim">Agents can navigate rendered websites. Active sessions appear automatically in a live panel beside chat.</span></span><input aria-label="Enable browser tools" type="checkbox" checked={browserSettings.enabled} onChange={(event) => updateBrowser("enabled", event.currentTarget.checked)} className="mt-1 size-5 accent-accent" /></label>
                {browserHealth ? <div className="mt-4 flex flex-wrap items-end gap-4 border-t border-line pt-4 text-xs"><span className="min-w-28"><span className="block text-ink-dim">Provider</span>{browserHealth.provider}</span><span className="min-w-28"><span className="block text-ink-dim">Status</span>{browserHealth.healthy ? "Ready" : "Unavailable"}</span><span className="min-w-28"><span className="block text-ink-dim">Display</span>{browserHealth.headless ? "Headless" : "Visible"}</span><span className="min-w-20"><span className="block text-ink-dim">Sessions</span>{browserHealth.activeSessions}</span><button type="button" disabled={testingBrowser || !browserSettings.enabled} onClick={async () => { setTestingBrowser(true); const response = await apiFetch("/api/browser/test", { method: "POST" }); setTestingBrowser(false); setBrowserHealth((current) => current ? { ...current, healthy: response.ok } : current); }} className="ml-auto min-h-10 border border-line px-3 text-xs disabled:opacity-50">{testingBrowser ? "Testing…" : "Test browser"}</button></div> : null}
              </section>
              <section className="rounded border border-line bg-panel p-5"><h2 className="font-medium">Runtime</h2><p className="mt-1 text-xs text-ink-dim">Local headless Chromium. Thread scope is recommended for isolation.</p><div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
                <label className="text-sm font-medium">Session isolation<select aria-label="Browser session isolation" value={browserSettings.scope} onChange={(event) => updateBrowser("scope", event.currentTarget.value as BrowserSettings["scope"])} className="mt-2 min-h-11 w-full border border-line bg-bg px-3"><option value="thread">One browser per chat</option><option value="shared">Shared browser</option></select></label>
                <RuntimeNumberField label="Viewport width" value={browserSettings.viewportWidth} min={320} max={3840} onChange={(value) => updateBrowser("viewportWidth", value)} />
                <RuntimeNumberField label="Viewport height" value={browserSettings.viewportHeight} min={240} max={2160} onChange={(value) => updateBrowser("viewportHeight", value)} />
                <RuntimeNumberField label="Operation timeout (ms)" value={browserSettings.timeoutMs} min={1000} max={120000} step={1000} onChange={(value) => updateBrowser("timeoutMs", value)} />
                <RuntimeNumberField label="Maximum sessions" value={browserSettings.maxSessions} min={1} max={16} onChange={(value) => updateBrowser("maxSessions", value)} />
                <RuntimeNumberField label="Idle timeout (ms)" value={browserSettings.idleTimeoutMs} min={60000} max={86400000} step={60000} onChange={(value) => updateBrowser("idleTimeoutMs", value)} />
              </div></section>
              <section className="rounded border border-line bg-panel p-5"><h2 className="font-medium">Capabilities</h2><div className="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">{([
                ["screencastEnabled", "Live browser panel"], ["screenshotsEnabled", "Screenshots"], ["multiTabEnabled", "Multiple tabs"], ["formsEnabled", "Forms and keyboard"], ["dialogsEnabled", "Browser dialogs"], ["dragEnabled", "Drag and drop"], ["evaluateEnabled", "Page JavaScript"], ["recordingEnabled", "Session recording (beta)"],
              ] as const).map(([key, label]) => <label key={key} className="flex min-h-11 items-center gap-3 border border-line bg-bg px-3 text-sm"><input type="checkbox" checked={browserSettings[key]} onChange={(event) => updateBrowser(key, event.currentTarget.checked)} className="size-4 accent-accent" />{label}</label>)}</div><p className="mt-3 text-xs text-ink-dim">Read-only agents never receive form, click, dialog, drag, JavaScript, close, tab-management, recording, or admin-input capabilities.</p></section>
              <section className="rounded border border-line bg-panel p-5"><h2 className="font-medium">Network policy</h2><p className="mt-1 text-xs text-ink-dim">Private and local addresses remain blocked unless explicitly allowed. One hostname per line; <code>*.example.com</code> is supported.</p><div className="mt-4 grid gap-4 sm:grid-cols-2"><label className="text-sm font-medium">Allowed hosts<textarea aria-label="Allowed browser hosts" rows={4} value={browserSettings.allowHosts.join("\n")} onChange={(event) => updateBrowser("allowHosts", event.currentTarget.value.split(/\s+/).filter(Boolean))} className="mt-2 w-full border border-line bg-bg p-3 font-mono text-xs" /></label><label className="text-sm font-medium">Denied hosts<textarea aria-label="Denied browser hosts" rows={4} value={browserSettings.denyHosts.join("\n")} onChange={(event) => updateBrowser("denyHosts", event.currentTarget.value.split(/\s+/).filter(Boolean))} className="mt-2 w-full border border-line bg-bg p-3 font-mono text-xs" /></label></div></section>
              <section className="rounded border border-line bg-panel p-5"><h2 className="font-medium">Recordings</h2><div className={`mt-4 grid gap-4 sm:grid-cols-2 ${browserSettings.recordingEnabled ? "" : "opacity-50"}`}><RuntimeNumberField label="Retention (days)" value={browserSettings.recordingRetentionDays} min={1} max={90} onChange={(value) => updateBrowser("recordingRetentionDays", value)} /><RuntimeNumberField label="Maximum files" value={browserSettings.recordingMaxFiles} min={1} max={100} onChange={(value) => updateBrowser("recordingMaxFiles", value)} /></div><p className="mt-3 text-xs text-ink-dim">Recordings are stored under the contained Popagent data directory and may include sensitive page content.</p><div className="mt-3 space-y-2">{browserRecordings.map((recording) => <div key={recording.name} className="flex items-center gap-3 border border-line bg-bg p-3 text-xs"><div className="min-w-0 flex-1"><div className="truncate font-mono">{recording.name}</div><div className="text-ink-dim">{Math.ceil(recording.size / 1024)} KB · {new Date(recording.createdAt).toLocaleString()}</div></div><button type="button" onClick={() => void downloadBrowserRecording(recording.name)} className="min-h-10 border border-line px-3">Download</button><button type="button" onClick={async () => { await apiFetch(`/api/browser/recordings/${encodeURIComponent(recording.name)}`, { method: "DELETE" }); setBrowserRecordings((current) => current.filter((item) => item.name !== recording.name)); }} className="min-h-10 border border-line px-3 text-red-400">Delete</button></div>)}</div></section>
              <section className="rounded border border-line bg-panel p-5"><h2 className="font-medium">Authenticated profiles</h2><p className="mt-1 text-xs text-ink-dim">Upload Playwright storage state. Cookies and local storage are encrypted and never returned by the API.</p><div className="mt-4 grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]"><label className="text-sm font-medium">Profile name<input aria-label="Browser profile name" value={profileName} onChange={(event) => setProfileName(event.currentTarget.value)} className="mt-2 min-h-11 w-full border border-line bg-bg px-3" /></label><label className="text-sm font-medium">Storage-state JSON<textarea aria-label="Browser profile storage state" value={profileState} onChange={(event) => setProfileState(event.currentTarget.value)} rows={5} className="mt-2 w-full border border-line bg-bg p-3 font-mono text-xs" /></label></div>{profileError ? <p role="alert" className="mt-2 text-xs text-red-400">{profileError}</p> : null}<div className="mt-3 flex justify-end"><button type="button" disabled={!profileName.trim()} onClick={() => void createBrowserProfile()} className="min-h-11 bg-accent px-4 text-sm font-medium text-white disabled:opacity-50">Add encrypted profile</button></div><div className="mt-3 space-y-2">{browserProfiles.map((profile) => <div key={profile.id} className="flex items-center gap-3 border border-line bg-bg p-3 text-sm"><label className="flex min-w-0 flex-1 items-center gap-3"><input type="radio" name="active-browser-profile" checked={profile.enabled} onChange={() => void setActiveBrowserProfile(profile, true)} className="size-4 accent-accent" /><span className="truncate">{profile.name}</span></label><button type="button" onClick={async () => { await apiFetch(`/api/browser/profiles/${profile.id}`, { method: "DELETE" }); setBrowserProfiles((current) => current.filter((item) => item.id !== profile.id)); }} className="min-h-10 border border-line px-3 text-xs text-red-400">Delete</button></div>)}</div></section>
              <section className="rounded border border-line bg-panel p-5"><div className="flex items-center justify-between"><div><h2 className="font-medium">Active sessions</h2><p className="mt-1 text-xs text-ink-dim">{browserSessions.length} currently tracked</p></div></div><div className="mt-3 space-y-2">{browserSessions.length ? browserSessions.map((session) => <div key={session.id} className="flex items-center gap-3 border border-line bg-bg p-3 text-sm"><Globe className="size-4 text-accent" /><div className="min-w-0 flex-1"><div className="truncate">{session.currentUrl ?? "Starting…"}</div><div className="text-xs text-ink-dim">{session.access} · {session.tabs.length} tabs · {session.threadId}</div></div><button type="button" onClick={async () => { await apiFetch(`/api/browser/sessions/${encodeURIComponent(session.id)}/close`, { method: "POST" }); setBrowserSessions((current) => current.filter((item) => item.id !== session.id)); }} className="min-h-10 border border-line px-3 text-xs">Close</button></div>) : <p className="text-sm text-ink-dim">No active browser sessions.</p>}</div></section>
              <div className="flex items-center justify-end gap-3">{saved ? <span role="status" className="text-xs text-emerald-500">Saved</span> : null}<button type="button" disabled={saving} onClick={() => void saveBrowser()} className="min-h-11 bg-accent px-4 text-sm font-medium text-white disabled:opacity-60">{saving ? "Saving…" : "Save browser settings"}</button></div>
            </div>}
          </div> : section === "secrets" ? <SecretSettingsPanel /> : section === "memory" ? <MemorySettingsPanel /> : section === "runtime" ? <div className="w-full max-w-4xl">
            <h1 className="text-xl font-semibold">Runtime</h1>
            <p className="mt-1 text-sm text-ink-dim">Control model execution, delegation bounds, and background task scheduling. Changes apply to new turns and tasks.</p>
            {runtimeSettings ? <div className="mt-6 space-y-5">
              <section className="rounded border border-line bg-panel p-5">
                <h2 className="font-medium">Model and execution</h2>
                <div className="mt-4 grid gap-4 sm:grid-cols-2">
                  <label className="text-sm font-medium">Default model
                    <select aria-label="Default model" value={runtimeSettings.defaultModel} onChange={(event) => changeRuntime("defaultModel", event.currentTarget.value)} className="mt-2 min-h-11 w-full rounded border border-line bg-bg px-3 text-ink outline-none focus:border-accent/50">
                      {!runtimeModels.includes(runtimeSettings.defaultModel) ? <option value={runtimeSettings.defaultModel}>{runtimeSettings.defaultModel}</option> : null}
                      {runtimeModels.map((model) => <option key={model} value={model}>{model}</option>)}
                    </select>
                  </label>
                  <RuntimeNumberField label="Supervisor max steps" value={runtimeSettings.supervisorMaxSteps} min={1} max={128} onChange={(value) => changeRuntime("supervisorMaxSteps", value)} />
                  <RuntimeNumberField label="Specialist max steps" value={runtimeSettings.specialistMaxSteps} min={1} max={128} onChange={(value) => changeRuntime("specialistMaxSteps", value)} />
                  <RuntimeNumberField label="Concurrent tool calls" value={runtimeSettings.toolConcurrency} min={1} max={16} onChange={(value) => changeRuntime("toolConcurrency", value)} />
                  <RuntimeNumberField label="Processor retries" value={runtimeSettings.maxProcessorRetries} min={0} max={10} onChange={(value) => changeRuntime("maxProcessorRetries", value)} />
                </div>
                <label className="mt-4 block text-sm font-medium">Final-response feedback<textarea aria-label="Final-response feedback" maxLength={4000} rows={3} value={runtimeSettings.finalResponseFeedback} onChange={(event) => changeRuntime("finalResponseFeedback", event.currentTarget.value)} className="mt-2 w-full resize-y rounded border border-line bg-bg p-3 text-sm outline-none focus:border-accent/50" /></label>
              </section>
              <section className="rounded border border-line bg-panel p-5">
                <h2 className="font-medium">Delegation</h2>
                <div className="mt-4 grid gap-4 sm:grid-cols-2">
                  <RuntimeNumberField label="Context messages" value={runtimeSettings.delegationContextMessages} min={1} max={100} onChange={(value) => changeRuntime("delegationContextMessages", value)} />
                  <RuntimeNumberField label="Result character limit" value={runtimeSettings.delegationResultCharacters} min={1000} max={100000} step={1000} onChange={(value) => changeRuntime("delegationResultCharacters", value)} />
                </div>
                <label className="mt-4 block text-sm font-medium">Failure feedback<textarea aria-label="Delegation failure feedback" maxLength={4000} rows={3} value={runtimeSettings.delegationFailureFeedback} onChange={(event) => changeRuntime("delegationFailureFeedback", event.currentTarget.value)} className="mt-2 w-full resize-y rounded border border-line bg-bg p-3 text-sm outline-none focus:border-accent/50" /></label>
                <label className="mt-4 block text-sm font-medium">Result truncation marker<textarea aria-label="Delegation result truncation marker" maxLength={500} rows={2} value={runtimeSettings.delegationResultTruncationMarker} onChange={(event) => changeRuntime("delegationResultTruncationMarker", event.currentTarget.value)} className="mt-2 w-full resize-y rounded border border-line bg-bg p-3 font-mono text-sm outline-none focus:border-accent/50" /></label>
              </section>
              <section className="rounded border border-line bg-panel p-5">
                <h2 className="font-medium">Background tasks</h2>
                <div className="mt-4 grid gap-4 sm:grid-cols-2">
                  <RuntimeNumberField label="Concurrent tasks" value={runtimeSettings.taskConcurrency} min={1} max={16} onChange={(value) => changeRuntime("taskConcurrency", value)} />
                  <RuntimeNumberField label="Poll interval (ms)" value={runtimeSettings.taskPollIntervalMs} min={250} max={300000} step={250} onChange={(value) => changeRuntime("taskPollIntervalMs", value)} />
                  <RuntimeNumberField label="Task timeout (ms)" value={runtimeSettings.taskTimeoutMs} min={1000} max={86400000} step={1000} onChange={(value) => changeRuntime("taskTimeoutMs", value)} />
                  <RuntimeNumberField label="Restart stale threshold (ms)" value={runtimeSettings.taskStaleAfterMs} min={60000} max={604800000} step={60000} onChange={(value) => changeRuntime("taskStaleAfterMs", value)} />
                </div>
              </section>
              <div className="flex items-center justify-end gap-3">{saved ? <span role="status" className="text-xs text-emerald-500">Saved</span> : null}<button type="button" disabled={saving} onClick={() => void saveRuntime()} className="min-h-11 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-60">{saving ? "Saving…" : "Save runtime settings"}</button></div>
            </div> : <p className="mt-6 text-sm text-ink-dim">Loading runtime settings…</p>}
          </div> : section === "observability" ? <ObservabilityPage /> : <div className="w-full">
            <h1 className="text-xl font-semibold">Tasks</h1>
            <p className="mt-1 text-sm text-ink-dim">Run popagent in the background or on a recurring schedule.</p>
            <label className="mt-4 block max-w-sm text-xs font-medium text-ink-dim">Workspace
              <select aria-label="Task workspace" value={taskWorkspaceId} onChange={(event) => { const id = event.currentTarget.value; setTaskWorkspaceId(id); void loadTasks(id); }} className="mt-1 min-h-11 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none">
                {workspaces.map((workspace) => <option key={workspace.id} value={workspace.id}>{workspace.name}</option>)}
              </select>
            </label>
            <div className="mt-6 grid gap-5 xl:grid-cols-2">
              <section className="rounded border border-line bg-panel p-5">
                <h2 className="font-medium">New background task</h2>
                <textarea aria-label="Task prompt" value={taskPrompt} onChange={(event) => setTaskPrompt(event.currentTarget.value)} rows={5} placeholder="Describe the work popagent should complete…" className="mt-3 w-full rounded border border-line bg-bg p-3 text-sm outline-none" />
                <button type="button" disabled={!taskPrompt.trim()} onClick={() => void createTask()} className="mt-3 min-h-11 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-50">Start task</button>
              </section>
              <section className="rounded border border-line bg-panel p-5">
                <h2 className="font-medium">New schedule</h2>
                <input aria-label="Schedule name" value={scheduleName} onChange={(event) => setScheduleName(event.currentTarget.value)} placeholder="Daily summary" className="mt-3 w-full rounded border border-line bg-bg px-3 py-2 text-sm outline-none" />
                <textarea aria-label="Schedule prompt" value={schedulePrompt} onChange={(event) => setSchedulePrompt(event.currentTarget.value)} rows={3} placeholder="Task prompt" className="mt-2 w-full rounded border border-line bg-bg p-3 text-sm outline-none" />
                <div className="mt-2 grid gap-2 sm:grid-cols-2">
                  <label className="text-xs text-ink-dim">Repeat<select aria-label="Schedule frequency" value={scheduleFrequency} onChange={(event) => setScheduleFrequency(event.currentTarget.value as ScheduleFrequency)} className="mt-1 min-h-11 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none"><option value="daily">Every day</option><option value="weekdays">Weekdays</option><option value="weekly">Every week</option></select></label>
                  <label className="text-xs text-ink-dim">Time<input type="time" aria-label="Schedule time" value={scheduleTime} onChange={(event) => setScheduleTime(event.currentTarget.value)} className="mt-1 min-h-11 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none" /></label>
                </div>
                {scheduleFrequency === "weekly" ? <label className="mt-2 block text-xs text-ink-dim">Day<select aria-label="Schedule weekday" value={scheduleWeekday} onChange={(event) => setScheduleWeekday(event.currentTarget.value)} className="mt-1 min-h-11 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none">{WEEKDAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}</select></label> : null}
                <button type="button" disabled={!scheduleName.trim() || !schedulePrompt.trim() || !scheduleTime} onClick={() => void createSchedule()} className="mt-3 min-h-11 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-50">Create schedule</button>
              </section>
            </div>
            <section className="mt-5 rounded border border-line bg-panel p-5"><h2 className="font-medium">Schedules</h2><div className="mt-3 space-y-2">{schedules.length ? schedules.map((schedule) => <article key={schedule.id} className="flex items-center gap-3 rounded border border-line bg-bg p-3"><div className="min-w-0 flex-1"><div className="font-medium">{schedule.name}</div><div className="truncate text-xs text-ink-dim">{describeSchedule(schedule.cron)}{schedule.nextRunAt ? ` · next ${new Date(schedule.nextRunAt).toLocaleString()}` : ""}</div></div><button type="button" onClick={() => void toggleSchedule(schedule)} className="min-h-11 rounded border border-line px-3 text-xs">{schedule.enabled ? "Pause" : "Resume"}</button><button type="button" aria-label={`Delete schedule ${schedule.name}`} onClick={() => void deleteSchedule(schedule.id)} className="flex size-11 items-center justify-center rounded text-red-400"><Trash2 className="size-4" /></button></article>) : <p className="text-sm text-ink-dim">No schedules.</p>}</div></section>
            <section className="mt-5 rounded border border-line bg-panel p-5"><h2 className="font-medium">Recent tasks</h2><div className="mt-3 space-y-2">{tasks.length ? tasks.map((task) => <article key={task.id} className="rounded border border-line bg-bg p-3"><div className="flex items-start gap-3"><div className="min-w-0 flex-1"><div className="line-clamp-2 text-sm">{task.prompt}</div><><div className="mt-1 text-xs uppercase text-ink-dim">{task.status} · {new Date(task.createdAt).toLocaleString()}{task.recoveryCount ? ` · recovered ${task.recoveryCount}×` : ""}</div>{task.status === "running" ? <><div className="mt-2 flex items-center justify-between gap-3 text-xs text-ink-dim"><span>{task.progress ?? "Working"}</span><span>Step {task.stepsCompleted}/{taskStepLimit}</span></div><progress aria-label="Task progress" max={taskStepLimit} value={task.stepsCompleted} className="mt-1 h-1.5 w-full accent-accent" /></> : null}</></div>{task.status === "queued" || task.status === "running" ? <button type="button" onClick={async () => { await apiFetch(`/api/tasks/${task.id}/cancel`, { method: "POST" }); await loadTasks(taskWorkspaceId); }} className="min-h-11 rounded border border-line px-3 text-xs">Cancel</button> : null}</div>{task.output ? <pre className="mt-3 max-h-48 overflow-auto whitespace-pre-wrap rounded bg-panel-2 p-3 text-xs">{task.output}</pre> : null}{task.error ? <p className="mt-2 text-xs text-red-400">{task.error}</p> : null}</article>) : <p className="text-sm text-ink-dim">No tasks.</p>}</div></section>
          </div>}
        </main>
      </div>
    </div>
  );
}