AkurAI Build
Menu

popagent

public

Latest change a976edbfb97c44a0e4fbac194e5e94b27b53f1f8 - Create follow-up tasks autonomously from completed scheduled reports by AkurAI Build

import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import { Activity, Bot, Brain, Building2, ChevronLeft, Clock3, Globe, Hash, KeyRound, Mail, Moon, Palette, Plus, Settings2, SlidersHorizontal, Sparkles, Sun, Trash2, X } from "lucide-react";
import { apiFetch } from "./api";
import { useFocusTrap } from "./use-focus-trap";
import {
  AGENT_TOOL_NAMES,
  MAX_AGENT_INSTRUCTIONS_CHARACTERS,
  runtimeModelId,
  type AgentListResponse,
  type AgentBrowserAccess,
  type AgentRuntimeSettings,
  type ModelSource,
  type AgentWorkspace,
  type AgentSchedule,
  type AgentSettings,
  type AgentSkill,
  type AgentSkillListResponse,
  type AgentToolName,
  type AgentTaskSource,
  type BrowserHealth,
  type ModelRouting,
  type ReasoningEffort,
  type BrowserProfile,
  type BrowserSession,
  type BrowserSettings,
  type ModelCatalogResponse,
  type ScheduleListResponse,
  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";
import { AutonomySettingsPanel } from "./AutonomySettingsPanel";
import { executionAgentPeople } from "../company-roster";
import { DEFAULT_MODEL_ROUTING, REASONING_EFFORTS, ROUTING_MODEL_ID } from "../api-types";

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-xs font-medium text-ink-muted">{label}<input aria-label={label} type="number" min={min} max={max} step={step} value={value} onChange={(event) => onChange(event.currentTarget.valueAsNumber)} className="mt-1 min-h-10 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none focus:border-accent/50" /></label>;
}

/** Compact settings group: hairline-separated block with a small heading and optional hint, matching the Company page density. */
function SettingsGroup({ title, hint, children }: { title: string; hint?: string; children: ReactNode }) {
  return <section aria-label={title} className="border-t border-line px-5 py-4 first:border-t-0">
    <div className="flex flex-wrap items-baseline justify-between gap-2"><h2 className="text-sm font-medium">{title}</h2>{hint ? <p className="text-xs text-ink-dim">{hint}</p> : null}</div>
    <div className="mt-3">{children}</div>
  </section>;
}
/** Round status LED: green = working, red = unavailable, amber = checking. */
export function StatusLed({ state, label }: { state: "ready" | "down" | "checking"; label: string }) {
  const tone = state === "ready"
    ? "bg-emerald-500 shadow-[0_0_8px_2px_rgba(16,185,129,0.55)]"
    : state === "down"
      ? "bg-red-500 shadow-[0_0_8px_2px_rgba(239,68,68,0.45)]"
      : "bg-amber-400 animate-pulse shadow-[0_0_8px_2px_rgba(251,191,36,0.45)]";
  return <span role="img" aria-label={label} title={label} className={`inline-block size-2.5 shrink-0 rounded-full ${tone}`} />;
}

/** One row per browser provider with its LED, so the operator sees at a glance which browsers work. */
export function BrowserProviderLeds({ health, selected }: { health?: BrowserHealth; selected: BrowserHealth["selected"] }) {
  const providers = health?.providers ?? [
    { id: "agent-browser" as const, label: "Headless AgentBrowser", healthy: false, headless: true, detail: "Checking…" },
    { id: "bifrost-navigator" as const, label: "BifrOSt Navigator", healthy: false, headless: false, detail: "Checking…" },
  ];
  return <ul aria-label="Browser status" className="grid min-w-0 gap-2 sm:grid-cols-2">{providers.map((provider) => {
    const state = health ? (provider.healthy ? "ready" : "down") : "checking";
    const active = provider.id === selected;
    return <li key={provider.id} data-state={state} className={`flex min-h-11 min-w-0 items-center gap-3 overflow-hidden rounded border px-3 text-sm ${active ? "border-accent/60 bg-bg" : "border-line bg-bg/60"}`}>
      <StatusLed state={state} label={`${provider.label}: ${health ? (provider.healthy ? "working" : "unavailable") : "checking"}`} />
      <span className="min-w-0 flex-1"><span className="block truncate font-medium">{provider.label}</span><span className="block truncate text-xs text-ink-dim">{health ? provider.detail : "Checking…"}{provider.headless ? " · headless" : " · visible desktop"}</span></span>
      {active ? <span className="rounded bg-accent/15 px-2 py-0.5 text-[11px] font-medium uppercase tracking-wide text-accent">Selected</span> : null}
    </li>;
  })}</ul>;
}


export type ModelRouteHealthView = { spec: string; cooldownUntil: string | null; lastError: string | null; picks: number };

/** Editor for the model routing policy: ordered routes with reasoning effort, strategy, cooldown, and live route LEDs. */
export function ModelRoutingEditor({ value: given, models, health, onChange }: {
  value?: ModelRouting;
  models: string[];
  health?: ModelRouteHealthView[];
  onChange: (next: ModelRouting) => void;
}) {
  // A backend that predates the routing column returns no value; render the
  // defaults so the page never crashes on a mixed-version reload.
  const value = given ?? DEFAULT_MODEL_ROUTING;
  const selectable = models.filter((model) => model !== ROUTING_MODEL_ID);
  const update = (patch: Partial<ModelRouting>) => onChange({ ...value, ...patch });
  const updateRoute = (index: number, patch: Partial<ModelRouting["routes"][number]>) =>
    update({ routes: value.routes.map((route, position) => position === index ? { ...route, ...patch } : route) });
  const move = (index: number, delta: number) => {
    const target = index + delta;
    if (target < 0 || target >= value.routes.length) return;
    const routes = [...value.routes];
    [routes[index], routes[target]] = [routes[target]!, routes[index]!];
    update({ routes });
  };
  return <div aria-label="Model routing" className="space-y-3">
    <div className="grid gap-3 sm:grid-cols-[auto_minmax(0,1fr)_minmax(0,1fr)] sm:items-end">
      <label className="flex min-h-10 items-center gap-2 text-sm"><input aria-label="Enable model routing" type="checkbox" checked={value.enabled} onChange={(event) => update({ enabled: event.currentTarget.checked })} className="size-4 accent-accent" />Enabled</label>
      <label className="text-xs font-medium text-ink-muted">Strategy<select aria-label="Model routing strategy" value={value.strategy} onChange={(event) => update({ strategy: event.currentTarget.value as ModelRouting["strategy"] })} className="mt-1 min-h-10 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none"><option value="fallback">Fallback (first healthy route wins)</option><option value="round-robin">Round robin (rotate healthy routes)</option></select></label>
      <RuntimeNumberField label="Cooldown after failure (ms)" value={value.cooldownMs} min={60000} max={86400000} step={60000} onChange={(cooldownMs) => update({ cooldownMs })} />
    </div>
    <ol aria-label="Model routes" className="space-y-1.5">
      {value.routes.map((route, index) => {
        const spec = route.reasoningEffort === "default" ? route.model : `${route.model}@${route.reasoningEffort}`;
        const status = health?.find((item) => item.spec === spec);
        const state = !health ? "checking" as const : status?.cooldownUntil ? "down" as const : "ready" as const;
        return <li key={index} className="grid gap-2 rounded border border-line bg-bg px-3 py-2 sm:grid-cols-[6.5rem_minmax(0,1fr)_10rem_auto] sm:items-center">
          <span className="flex items-center gap-2 text-xs text-ink-dim"><StatusLed state={state} label={`Route ${index + 1}: ${state === "down" ? `cooling down until ${new Date(status!.cooldownUntil!).toLocaleTimeString()}` : state === "ready" ? "available" : "checking"}`} />{index === 0 ? "Primary" : `Route ${index + 1}`}</span>
          <select aria-label={`Route ${index + 1} model`} value={route.model} onChange={(event) => updateRoute(index, { model: event.currentTarget.value })} className="min-h-9 w-full rounded border border-line bg-panel px-2 text-sm text-ink outline-none">
            {!selectable.includes(route.model) ? <option value={route.model}>{route.model}</option> : null}
            {selectable.map((model) => <option key={model} value={model}>{model}</option>)}
          </select>
          <select aria-label={`Route ${index + 1} reasoning effort`} value={route.reasoningEffort} onChange={(event) => updateRoute(index, { reasoningEffort: event.currentTarget.value as ReasoningEffort })} className="min-h-9 w-full rounded border border-line bg-panel px-2 text-sm text-ink outline-none">
            {REASONING_EFFORTS.map((effort) => <option key={effort} value={effort}>{effort === "default" ? "Default reasoning" : `${effort[0]!.toUpperCase()}${effort.slice(1)} reasoning`}</option>)}
          </select>
          <span className="flex items-center gap-1">
            <button type="button" aria-label={`Move route ${index + 1} up`} disabled={index === 0} onClick={() => move(index, -1)} className="min-h-9 rounded border border-line px-2 text-xs disabled:opacity-40">↑</button>
            <button type="button" aria-label={`Move route ${index + 1} down`} disabled={index === value.routes.length - 1} onClick={() => move(index, 1)} className="min-h-9 rounded border border-line px-2 text-xs disabled:opacity-40">↓</button>
            <button type="button" aria-label={`Remove route ${index + 1}`} onClick={() => update({ routes: value.routes.filter((_, position) => position !== index) })} className="min-h-9 rounded border border-line px-2 text-xs text-red-400">Remove</button>
          </span>
          {status?.lastError ? <p className="text-[11px] text-ink-dim sm:col-span-4">Last failure: {status.lastError}{status.picks ? ` · picked ${status.picks}×` : ""}</p> : status?.picks ? <p className="text-[11px] text-ink-dim sm:col-span-4">Picked {status.picks}× since restart</p> : null}
        </li>;
      })}
    </ol>
    <div className="flex items-center justify-between gap-3">
      <p className="text-xs text-ink-dim">{value.routes.length === 0 ? "No routes yet — add a primary and at least one fallback." : `${value.routes.length} route${value.routes.length === 1 ? "" : "s"}; order matters for fallback.`}</p>
      <button type="button" disabled={value.routes.length >= 8} onClick={() => update({ routes: [...value.routes, { model: selectable[0] ?? "", reasoningEffort: "low" }] })} className="min-h-9 rounded border border-line px-3 text-xs disabled:opacity-50">Add route</button>
    </div>
  </div>;
}

export function RuntimeModelSource({
  value,
  onChange,
}: {
  value: ModelSource;
  onChange: (value: ModelSource) => void;
}) {
  return <label className="text-xs font-medium text-ink-muted">Model source
    <select aria-label="Model source" value={value} onChange={(event) => onChange(event.currentTarget.value as ModelSource)} className="mt-1 min-h-10 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none focus:border-accent/50">
      <option value="configured">Configured model</option>
      <option value="local">Titan local 9B</option>
      <option value="openrouter-free">OpenRouter free router</option>
    </select>
  </label>;
}

export function AgentWorkSource({ source }: { source: AgentTaskSource }) {
  const label = source === "build-maintenance"
    ? "build-maintenance"
    : source === "self-update"
      ? "self-update"
      : "user";
  return <span> · source {label}</span>;
}

export function scheduleWorkspaceName(workspaceId: string, workspaces: AgentWorkspace[]): string {
  return workspaces.find((workspace) => workspace.id === workspaceId)?.name ?? workspaceId;
}

export function scheduleTimingLabel(schedule: Pick<AgentSchedule, "source" | "cron" | "nextRunAt">): string {
  if (schedule.source !== "user") return "When idle";
  return `${describeSchedule(schedule.cron)}${schedule.nextRunAt ? ` · next ${new Date(schedule.nextRunAt).toLocaleString()}` : ""}`;
}

export function AgentRosterRow({
  agent,
  selected,
  onChoose,
}: {
  agent: AgentSettings;
  selected: boolean;
  onChoose: () => void;
}) {
  const person = executionAgentPeople.get(agent.id);
  const unavailable = agent.id !== "orchistrator" && !agent.delegationEnabled;
  const rowRef = useRef<HTMLButtonElement>(null);
  useEffect(() => {
    if (selected) rowRef.current?.scrollIntoView({ block: "nearest" });
  }, [selected]);
  return <button
    ref={rowRef}
    type="button"
    aria-pressed={selected}
    onClick={onChoose}
    className={`agent-roster-row ${selected ? "is-selected" : ""} ${unavailable ? "is-disabled" : ""}`}
  >
    {person ? <img src={person.avatarUrl} alt="" className="size-8 shrink-0 rounded-full border border-line object-cover" /> : <span className="flex size-8 shrink-0 items-center justify-center rounded-full border border-line bg-panel-2"><Bot className="size-4 text-ink-dim" /></span>}
    <span className="min-w-0 flex-1">
      <span className="agent-roster-name">{person?.name ?? agent.name}</span>
      <span className="agent-roster-title">{person?.title ?? agent.description}</span>
    </span>
    {unavailable ? <><span aria-hidden="true" className="size-1.5 shrink-0 rounded-full bg-ink-dim" /><span className="sr-only">Delegation disabled</span></> : null}
  </button>;
}

export function agentRosterGroups(agents: AgentSettings[], query = ""): { name: string; members: AgentSettings[] }[] {
  const search = query.trim().toLowerCase();
  const groups = new Map<string, AgentSettings[]>();
  for (const agent of agents) {
    const person = executionAgentPeople.get(agent.id);
    const department = person?.department ?? "Other agents";
    if (search && !`${person?.name ?? agent.name} ${person?.title ?? ""} ${department}`.toLowerCase().includes(search)) continue;
    const members = groups.get(department) ?? [];
    members.push(agent);
    groups.set(department, members);
  }
  return [...groups].map(([name, members]) => ({ name, members })).sort((a, b) => a.name.localeCompare(b.name));
}

export function AgentIdentityCard({
  agentId,
  model,
  delegationEnabled,
}: {
  agentId: string;
  model: string | null;
  delegationEnabled: boolean;
}) {
  const person = executionAgentPeople.get(agentId);
  if (!person) return null;
  return <div className="mb-4 flex flex-wrap items-center gap-x-4 gap-y-3 border-b border-line pb-4">
    <img src={person.avatarUrl} alt="" className="size-14 shrink-0 rounded-full border border-line object-cover" />
    <div className="min-w-0 flex-1">
      <p className="truncate text-lg font-semibold tracking-tight">{person.name}</p>
      <p className="truncate text-sm text-ink-muted">{person.title}</p>
      <div className="mt-0.5 flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-ink-dim">
        <span>{person.department}</span>
        <a href={`mailto:${person.email}`} className="inline-flex items-center gap-1 text-accent hover:underline"><Mail className="size-3" />{person.email}</a>
      </div>
    </div>
    <div className="grid basis-full gap-0.5 text-left text-xs sm:basis-auto sm:text-right">
      <span className="max-w-64 truncate font-mono text-ink-muted">{model ?? "Request-selected model"}</span>
      <span className={delegationEnabled || agentId === "orchistrator" ? "text-emerald-500" : "text-ink-dim"}>
        {agentId === "orchistrator" ? "Orchestration lead" : delegationEnabled ? "Available for delegation" : "Delegation disabled"}
      </span>
    </div>
  </div>;
}

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 [rosterQuery, setRosterQuery] = useState("");
  const [rosterDetail, setRosterDetail] = useState(false);
  const [agentName, setAgentName] = useState("");
  const [agentDescription, setAgentDescription] = useState("");
  const [instructions, setInstructions] = useState("");
  const [agentModel, setAgentModel] = useState<string | null>(null);
  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<BrowserHealth>();
  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 [testResult, setTestResult] = useState<{ ok: boolean; message: string }>();
  const [runtimeSettings, setRuntimeSettings] = useState<AgentRuntimeSettings>();
  const [runtimeModels, setRuntimeModels] = useState<string[]>([]);
  const [routingHealth, setRoutingHealth] = useState<ModelRouteHealthView[]>();
  const [schedules, setSchedules] = useState<AgentSchedule[]>([]);
  const [scheduleWorkspaceId, setScheduleWorkspaceId] = useState(workspaceId ?? workspaces[0]?.id ?? "default");
  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) => {
    try {
      const response = await apiFetch(`/api/agents/${agentId}/skills`);
      if (response.ok) setSkills(((await response.json()) as AgentSkillListResponse).skills);
    } catch {}
  }, []);
  const loadSchedules = useCallback(async () => {
    try {
      const response = await apiFetch("/api/schedules");
      if (response.ok) setSchedules(((await response.json()) as ScheduleListResponse).schedules);
    } catch {}
  }, []);
  useEffect(() => {
    if (!open) return;
    setScheduleWorkspaceId(workspaceId ?? workspaces[0]?.id ?? "default");
    void loadSchedules();
  }, [loadSchedules, open, workspaceId, workspaces]);
  useEffect(() => {
    if (!open || section !== "runtime") return;
    let cancelled = false;
    const refresh = async () => {
      const response = await apiFetch("/api/models/routing").catch(() => undefined);
      if (!cancelled && response?.ok) setRoutingHealth(((await response.json()) as { routes: ModelRouteHealthView[] }).routes);
    };
    void refresh();
    const timer = window.setInterval(() => void refresh(), 15_000);
    return () => { cancelled = true; window.clearInterval(timer); };
  }, [open, section]);
  useEffect(() => {
    if (!open || section !== "browser") return;
    let cancelled = false;
    const refresh = async () => {
      const response = await apiFetch("/api/browser/health").catch(() => undefined);
      if (!cancelled && response?.ok) setBrowserHealth(await response.json() as BrowserHealth);
    };
    const timer = window.setInterval(() => void refresh(), 15_000);
    return () => { cancelled = true; window.clearInterval(timer); };
  }, [open, section]);
  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> = { orchistrator: 0, 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);
          setAgentModel(ordered[0].model);
          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 BrowserHealth);
      if (runtimeResponse.ok) setRuntimeSettings(await runtimeResponse.json() as AgentRuntimeSettings);
      if (modelResponse.ok) setRuntimeModels(((await modelResponse.json()) as ModelCatalogResponse).models);
    }).catch(() => {});
  }, [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);
    setAgentModel(item.model);
    setAgentWorkspaceAccess(item.workspaceAccess);
    setAgentBrowserAccess(item.browserAccess);
    setAgentDelegationEnabled(item.delegationEnabled);
    setAgentTools(item.tools);
    setSaved(false);
    setRosterDetail(true);
    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,
        model: agentModel,
        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 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: scheduleWorkspaceId }) });
    if (response.ok) { setScheduleName(""); setSchedulePrompt(""); await loadSchedules(); }
  };
  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 changeModelSource = (source: ModelSource) => {
    setRuntimeSettings((current) => current
      ? {
        ...current,
        modelSource: source,
        defaultModel: runtimeModelId(current.defaultModel, source),
      }
      : current);
    setSaved(false);
  };
  const updateSchedule = async (schedule: AgentSchedule, changes: Partial<Pick<AgentSchedule, "cron" | "enabled" | "followUps">>) => {
    try {
      await apiFetch(`/api/schedules/${schedule.id}`, {
        method: "PATCH",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ ...schedule, ...changes }),
      });
      await loadSchedules();
    } catch {}
  };
  const toggleSchedule = (schedule: AgentSchedule) =>
    updateSchedule(schedule, { cron: schedule.cron, enabled: !schedule.enabled });
  const deleteSchedule = async (id: string) => {
    try {
      await apiFetch(`/api/schedules/${id}`, { method: "DELETE" });
      await loadSchedules();
    } catch {}
  };
  useEffect(() => {
    if (!open || section !== "schedules") return;
    const timer = setInterval(() => void loadSchedules(), 2000);
    return () => clearInterval(timer);
  }, [loadSchedules, open, section]);
  useEffect(() => {
    if (!open) return;
    closeRef.current?.focus();
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape" && !pageRef.current?.querySelector('[role="alertdialog"]')) onClose();
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [onClose, open]);
  useFocusTrap(pageRef, open);
  if (!open) return null;
  const navigationClass = (target: SettingsSection) => `flex min-h-11 w-auto shrink-0 items-center justify-center gap-2 rounded px-3 text-sm md:w-full md:justify-start ${section === target ? "bg-accent-soft font-medium text-ink" : "text-ink-muted hover:bg-panel-2 hover:text-ink"}`;
  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="flex shrink-0 gap-1 overflow-x-auto border-b border-line bg-panel p-2 md:block md:w-56 md:border-b-0 md:border-r md:p-3">
          <button type="button" onClick={() => { onSectionChange("appearance"); setSaved(false); }} className={navigationClass("appearance")}><Palette className="size-4 text-accent" />Appearance</button>
          <button type="button" onClick={() => { onSectionChange("company"); setSaved(false); }} className={navigationClass("company")}><Building2 className="size-4 text-accent" />Company</button>
          <button type="button" onClick={() => { onSectionChange("channels"); setSaved(false); }} className={navigationClass("channels")}><Hash className="size-4 text-accent" />Channels</button>
          <button type="button" onClick={() => { onSectionChange("browser"); setSaved(false); }} className={navigationClass("browser")}><Globe className="size-4 text-accent" />Browser</button>
          <button type="button" onClick={() => { onSectionChange("secrets"); setSaved(false); }} className={navigationClass("secrets")}><KeyRound className="size-4 text-accent" />Secrets</button>
          <button type="button" onClick={() => { onSectionChange("memory"); setSaved(false); }} className={navigationClass("memory")}><Brain className="size-4 text-accent" />Memory</button>
          <button type="button" onClick={() => { onSectionChange("runtime"); setSaved(false); }} className={navigationClass("runtime")}><SlidersHorizontal className="size-4 text-accent" />Runtime</button>
          <button type="button" onClick={() => { onSectionChange("autonomy"); setSaved(false); }} className={navigationClass("autonomy")}><Sparkles className="size-4 text-accent" />Autonomy</button>
          <button type="button" onClick={() => { onSectionChange("schedules"); setSaved(false); }} className={navigationClass("schedules")}><Clock3 className="size-4 text-accent" />Schedules</button>
          <button type="button" onClick={() => { onSectionChange("observability"); setSaved(false); }} className={navigationClass("observability")}><Activity className="size-4 text-accent" />Observe</button>
        </nav>
        <main className="page-content min-w-0 flex-1 overflow-y-auto">
          {section === "appearance" ? <div className="w-full">
            <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 === "company" ? <div className="w-full">
            <h1 className="sr-only">Your operating team</h1>
            <div className="agent-roster">
              <div className={`agent-roster-list flex-col ${rosterDetail ? "hidden md:flex" : "flex"}`}>
                <div className="flex items-center gap-2 border-b border-line p-2">
                  <input
                    type="search"
                    aria-label="Filter agents"
                    placeholder="Filter agents"
                    value={rosterQuery}
                    onChange={(event) => setRosterQuery(event.currentTarget.value)}
                    className="min-h-9 min-w-0 flex-1 rounded border border-line bg-bg px-2.5 text-sm text-ink outline-none focus:border-accent/50"
                  />
                  <span className="shrink-0 pr-1 text-xs tabular-nums text-ink-dim">{agents.length}</span>
                </div>
                <div className="agent-roster-scroll" role="group" aria-label="Agents">
                  {agentRosterGroups(agents, rosterQuery).map((group) => <div key={group.name}>
                    <p className="agent-roster-group">{group.name}</p>
                    {group.members.map((item) => <AgentRosterRow
                      key={item.id}
                      agent={item}
                      selected={item.id === selectedId}
                      onChoose={() => choose(item)}
                    />)}
                  </div>)}
                  {agents.length && !agentRosterGroups(agents, rosterQuery).length ? <p className="px-3 py-4 text-sm text-ink-dim">No agent matches “{rosterQuery}”.</p> : null}
                </div>
              </div>
              <div className={`min-w-0 ${rosterDetail ? "" : "hidden md:block"}`}>
              <button type="button" onClick={() => setRosterDetail(false)} className="mb-3 flex min-h-10 items-center gap-1.5 text-sm text-ink-muted md:hidden"><ChevronLeft className="size-4" />All agents</button>
              {selected ? <section className="border border-line bg-panel p-3 sm:p-4">
                <AgentIdentityCard agentId={selected.id} model={selected.model} delegationEnabled={selected.delegationEnabled} />
                <div className="mb-4 flex gap-1 border-b border-line pb-2">
                  <button type="button" onClick={() => { setAgentPanel("instructions"); setSaved(false); }} className={`min-h-10 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-10 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-xs font-medium text-ink-muted">Display name<input aria-label="Agent display name" maxLength={120} value={agentName} onChange={(event) => { setAgentName(event.currentTarget.value); setSaved(false); }} className="mt-1 min-h-10 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none focus:border-accent/50" /></label>
                    <label className="text-xs font-medium text-ink-muted">Delegation description<input aria-label="Agent delegation description" maxLength={1024} value={agentDescription} onChange={(event) => { setAgentDescription(event.currentTarget.value); setSaved(false); }} className="mt-1 min-h-10 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none focus:border-accent/50" /></label>
                  </div>
                  <label className="mt-3 block text-xs font-medium text-ink-muted">Model<select aria-label="Agent model" value={agentModel ?? ""} onChange={(event) => { setAgentModel(event.currentTarget.value || null); setSaved(false); }} className="mt-1 min-h-10 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none"><option value="">Use selected request model</option>{runtimeModels.map((model) => <option key={model} value={model}>{model}</option>)}</select></label>
                  <div className="mt-4 border-y border-line py-4">
                    <div className="flex flex-wrap items-baseline justify-between gap-2"><h3 className="text-sm font-medium">Capabilities</h3><p className="text-xs text-ink-dim">Writable workspace, interactive browser, and secret tools grant broader access.</p></div>
                    <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-10 w-full rounded border border-line bg-bg 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-10 w-full rounded border border-line bg-bg 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 === "orchistrator" ? 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-3"><legend className="text-xs font-medium text-ink-dim">Tools</legend><div className="mt-2 flex flex-wrap gap-2">{AGENT_TOOL_NAMES.map((tool) => <label key={tool} className="flex min-h-9 items-center gap-2 rounded border border-line bg-bg px-3 text-xs"><input type="checkbox" checked={agentTools.includes(tool)} onChange={() => toggleAgentTool(tool)} className="size-4 accent-accent" />{tool}</label>)}</div></fieldset>
                  </div>
                  <div className="mt-4 flex flex-wrap items-baseline justify-between gap-1"><label htmlFor="agent-instructions" className="text-sm font-medium">Instructions</label><span className="text-xs text-ink-dim">{instructions.length.toLocaleString()} / {MAX_AGENT_INSTRUCTIONS_CHARACTERS.toLocaleString()}</span></div>
                  {selected.sourceUrls.length ? <p className="mt-1 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={14} className="mt-2 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-10 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-10 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-10 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-10 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>
          </div> : section === "channels" ? <ChannelSettingsPanel /> : section === "browser" ? <div className="w-full">
            <h1 className="text-xl font-semibold">Browser</h1>
            <p className="mt-1 text-sm text-ink-dim">Choose how agents reach the web: isolated headless sessions or the native BifrOSt Navigator on this host.</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>
                <label className="mt-4 block border-t border-line pt-4 text-sm font-medium">Browser provider<select aria-label="Browser provider" value={browserSettings.provider} onChange={(event) => updateBrowser("provider", event.currentTarget.value as BrowserSettings["provider"])} className="mt-2 min-h-11 w-full border border-line bg-bg px-3"><option value="agent-browser">Headless AgentBrowser (isolated Chromium)</option><option value="bifrost-navigator">Native BifrOSt Navigator (your visible desktop browser)</option></select></label>
                <p className="mt-2 text-xs text-ink-dim">{browserSettings.provider === "bifrost-navigator" ? "Agents with browser access operate the BifrOSt Navigator running on this host through its protected Unix socket. Read-only agents receive only non-mutating operations. Runtime, capability, recording, and profile settings below apply to the headless provider only." : "Agents receive isolated headless sessions governed by the runtime, capability, network, and profile settings below."}</p>
                <div className="mt-4 border-t border-line pt-4"><div className="mb-2 flex items-center justify-between gap-3"><h3 className="text-sm font-medium">Browser status</h3><span className="text-xs text-ink-dim">Rechecked every 15 s</span></div><BrowserProviderLeds health={browserHealth} selected={browserSettings.provider} /></div>
                {browserHealth ? <div className="mt-4 flex flex-wrap items-end gap-4 text-xs"><span className="min-w-20"><span className="block text-ink-dim">Sessions</span>{browserHealth.activeSessions}</span>{testResult ? <span role="status" className={`min-w-0 basis-full sm:basis-auto sm:flex-1 ${testResult.ok ? "text-emerald-500" : "text-red-400"}`}>{testResult.message}</span> : null}<button type="button" disabled={testingBrowser || !browserSettings.enabled} onClick={async () => { setTestingBrowser(true); setTestResult(undefined); const response = await apiFetch("/api/browser/test", { method: "POST" }); const body = await response.json().catch(() => ({})) as { url?: string; title?: string; error?: string }; setTestingBrowser(false); setTestResult(response.ok ? { ok: true, message: `Test passed: ${body.title || body.url || "page reachable"}` } : { ok: false, message: `Test failed: ${body.error ?? response.statusText}` }); const health = await apiFetch("/api/browser/health").catch(() => undefined); if (health?.ok) setBrowserHealth(await health.json() as BrowserHealth); }} className="ml-auto min-h-10 border border-line px-3 text-xs disabled:opacity-50">{testingBrowser ? "Testing…" : "Test selected 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">
            <div className="flex flex-wrap items-start justify-between gap-3">
              <div><h1 className="text-xl font-semibold">Runtime</h1><p className="mt-1 text-sm text-ink-dim">Model execution, routing, delegation bounds, and background task scheduling. Changes apply to new turns and tasks.</p></div>
              {runtimeSettings ? <div className="flex items-center 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-10 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-60">{saving ? "Saving…" : "Save runtime settings"}</button></div> : null}
            </div>
            {runtimeSettings ? <div className="mt-5 rounded-lg border border-line bg-panel">
              <SettingsGroup title="Model" hint="Default route for chat, agents without an override, tasks, and schedules.">
                <div className="grid gap-3 sm:grid-cols-2">
                  <RuntimeModelSource value={runtimeSettings.modelSource} onChange={changeModelSource} />
                  <label className="text-xs font-medium text-ink-muted">Default model
                    <select aria-label="Default model" disabled={runtimeSettings.modelSource !== "configured"} value={runtimeSettings.defaultModel} onChange={(event) => changeRuntime("defaultModel", event.currentTarget.value)} className="mt-1 min-h-10 w-full rounded border border-line bg-bg px-3 text-sm text-ink outline-none focus:border-accent/50 disabled:opacity-60">
                      {!runtimeModels.includes(runtimeSettings.defaultModel) ? <option value={runtimeSettings.defaultModel}>{runtimeSettings.defaultModel}</option> : null}
                      {runtimeModels.map((model) => <option key={model} value={model}>{model}</option>)}
                    </select>
                  </label>
                </div>
                <p className="mt-2 text-xs leading-relaxed text-ink-dim">The free route uses 9Router’s <code>auto/coding:free</code> route, which delegates to currently available OpenRouter free coding models. Availability and model behavior may vary.</p>
              </SettingsGroup>
              <SettingsGroup title="Model routing" hint={`Select ${ROUTING_MODEL_ID} as a model anywhere to resolve it through this policy; rate-limited or unknown routes leave rotation for the cooldown.`}>
                <ModelRoutingEditor value={runtimeSettings.modelRouting} models={runtimeModels} health={routingHealth} onChange={(modelRouting) => changeRuntime("modelRouting", modelRouting)} />
              </SettingsGroup>
              <SettingsGroup title="Execution" hint="Step budgets and tool concurrency per run.">
                <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
                  <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-3 block text-xs font-medium text-ink-muted">Final-response feedback<textarea aria-label="Final-response feedback" maxLength={4000} rows={2} value={runtimeSettings.finalResponseFeedback} onChange={(event) => changeRuntime("finalResponseFeedback", event.currentTarget.value)} className="mt-1 w-full resize-y rounded border border-line bg-bg p-2.5 text-sm text-ink outline-none focus:border-accent/50" /></label>
              </SettingsGroup>
              <SettingsGroup title="Delegation" hint="How much context specialists receive and how their results return.">
                <div className="grid gap-3 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)} />
                  <label className="text-xs font-medium text-ink-muted">Failure feedback<textarea aria-label="Delegation failure feedback" maxLength={4000} rows={2} value={runtimeSettings.delegationFailureFeedback} onChange={(event) => changeRuntime("delegationFailureFeedback", event.currentTarget.value)} className="mt-1 w-full resize-y rounded border border-line bg-bg p-2.5 text-sm text-ink outline-none focus:border-accent/50" /></label>
                  <label className="text-xs font-medium text-ink-muted">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-1 w-full resize-y rounded border border-line bg-bg p-2.5 font-mono text-sm text-ink outline-none focus:border-accent/50" /></label>
                </div>
              </SettingsGroup>
              <SettingsGroup title="Background tasks" hint="Queue concurrency, polling, and stale-run recovery.">
                <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
                  <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>
              </SettingsGroup>
              <div className="flex items-center justify-end gap-3 border-t border-line px-5 py-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-10 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 === "autonomy" ? <AutonomySettingsPanel /> : section === "observability" ? <ObservabilityPage /> : <div className="w-full">
            <h1 className="text-xl font-semibold">Schedules</h1>
            <p className="mt-1 text-sm text-ink-dim">Create recurring work. Live tasks and progress are on the workspace overview.</p>
            <section className="mt-6 rounded border border-line bg-panel p-5">
              <h2 className="font-medium">New schedule</h2>
              <label className="mt-3 block max-w-sm text-xs font-medium text-ink-dim">Execution workspace
                <select aria-label="Schedule workspace" value={scheduleWorkspaceId} onChange={(event) => setScheduleWorkspaceId(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">
                  {workspaces.map((workspace) => <option key={workspace.id} value={workspace.id}>{workspace.name}</option>)}
                </select>
              </label>
              <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>
            <section className="mt-5 rounded border border-line bg-panel p-5">
              <div className="flex items-center justify-between gap-3">
                <h2 className="font-medium">All schedules</h2>
                <span className="text-xs tabular-nums text-ink-dim">{schedules.length}</span>
              </div>
              <div className="mt-3 space-y-2">
                {schedules.length ? schedules.map((schedule) => <article key={schedule.id} className="rounded border border-line bg-bg p-3">
                  <div className="flex flex-wrap items-center gap-3">
                    <div className="min-w-0 flex-1">
                      <div className="font-medium">{schedule.name}</div>
                      <div className="truncate text-xs text-ink-dim">{scheduleWorkspaceName(schedule.workspaceId, workspaces)} · {scheduleTimingLabel(schedule)}<AgentWorkSource source={schedule.source} /></div>
                      {schedule.latestTaskStatus ? <div className="mt-1 text-xs text-ink-muted">Latest: {schedule.latestTaskStatus}{schedule.latestHeartbeatAt ? ` · heartbeat ${new Date(schedule.latestHeartbeatAt).toLocaleString()}` : ""}</div> : null}
                    </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>
                    {schedule.source === "user" ? <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> : null}
                  </div>
                  {schedule.source === "user" ? <form className="mt-3 flex flex-wrap items-end gap-2" onSubmit={(event) => { event.preventDefault(); const cron = new FormData(event.currentTarget).get("cron"); if (typeof cron === "string") void updateSchedule(schedule, { cron, enabled: schedule.enabled }); }}>
                    <label className="min-w-52 flex-1 text-xs text-ink-dim">Cron<input name="cron" aria-label={`Cron for ${schedule.name}`} defaultValue={schedule.cron} maxLength={100} className="mt-1 min-h-10 w-full rounded border border-line bg-panel px-3 font-mono text-sm text-ink outline-none focus:border-accent/50" /></label>
                    <button type="submit" className="min-h-10 rounded border border-line px-3 text-xs">Save timing</button>
                    <label className="flex min-h-10 items-center gap-2 text-xs text-ink-muted"><input type="checkbox" aria-label={`Create follow-up tasks from ${schedule.name}`} checked={schedule.followUps} onChange={(event) => void updateSchedule(schedule, { followUps: event.currentTarget.checked })} className="size-4 accent-accent" />Create follow-up tasks from results</label>
                  </form> : null}
                </article>) : <p className="text-sm text-ink-dim">No schedules.</p>}
              </div>
            </section>
          </div>}
        </main>
      </div>
    </div>
  );
}