import React from "react";
import { createRoot } from "react-dom/client";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
  Archive,
  Bot,
  Calendar,
  Check,
  ChevronDown,
  ChevronLeft,
  ChevronRight,
  Copy,
  ExternalLink,
  FileText,
  Globe,
  Image as ImageIcon,
  Info,
  KeyRound,
  LockKeyhole,
  Loader2,
  LogOut,
  Mail,
  MessageSquare,
  MonitorCog,
  Pencil,
  Play,
  Plus,
  Puzzle,
  RefreshCw,
  Rocket,
  Save,
  Search,
  Send,
  Server,
  Settings,
  Shield,
  Sparkles,
  Trash2,
  Upload,
  User,
  Users,
  X,
} from "lucide-react";
import {
  appShellRecipe,
  badgeRecipe,
  buttonRecipe,
  cardRecipe,
  componentRecipe,
  designTokens,
  inputRecipe,
  overlayRecipe,
  tableRecipe,
} from "../styling.gen";
import "../../../packages/styles/fleeet.css";
import "./styles.css";

type JsonObject = Record<string, unknown>;

interface AuthSession {
  token: string;
  uuid: string;
  email: string;
  role?: WorkspaceRole;
}

interface Agent {
  id: string;
  name: string;
  version: number;
  description: string | null;
  system: string | null;
  model: unknown;
  metadata: Record<string, string>;
  tools: unknown[];
  skills: unknown[];
  mcp_servers: unknown[];
  multiagent: unknown | null;
  archived_at: string | null;
  created_at: string;
  updated_at: string;
}

interface AgentRecord {
  id: string;
  creator_uuid: string;
  name: string;
  version: number;
  archived_at: string | null;
  created_at: string;
  updated_at: string;
  agent: Agent;
}

interface Member {
  uuid: string;
  email: string;
  role: WorkspaceRole;
}

type WorkspaceRole = "admin" | "member";

interface ApiKeyRecord {
  id: string;
  name: string;
  key_prefix: string;
  project_id: string | null;
  creator_uuid: string | null;
  creator_email: string | null;
  last_used_at: string | null;
  created_at: string;
  updated_at: string;
}

interface EmailReceiverRecord {
  id: string;
  name: string;
  domain: string;
  project_id: string;
  creator_uuid: string | null;
  creator_email: string | null;
  created_at: string;
  updated_at: string;
}

interface GoogleCredentialResponse {
  credential?: string;
}

type ActiveTab = "chat" | "agents" | "mcpServers" | "integrations" | "skills" | "deployments" | "environments" | "secrets" | "apiKeys" | "members";
type EnvironmentKind = "cloud" | "self_hosted";
type SecretKind = "static_bearer" | "environment_variable";
type McpAuthKind = "no_auth" | SecretKind;
type McpAuthEditKind = "unchanged" | McpAuthKind;
type ScheduleMode = "hours" | "days" | "weeks" | "cron";
type PaletteTab = "triggers" | "agents" | "mcps" | "skills";

interface McpServerDraft {
  id: string;
  registryId: string;
  name: string;
  url: string;
}

interface RegisteredMcpServer {
  id: string;
  name: string;
  description: string | null;
  url: string;
  icon_data_url: string | null;
  auth_type: McpAuthKind;
  vault_id: string | null;
  credential_id: string | null;
  project_ids: string[];
  created_at: string;
  updated_at: string;
}

interface IntegrationTemplate {
  id: string;
  logo_data_url: string | null;
  name: string;
  description: string | null;
  mcp_server_url: string;
  mcp_auth_type: SecretKind;
  agent_name: string;
  agent_description: string | null;
  agent_system_prompt: string | null;
  agent_model: string;
  creator_uuid: string;
  created_at: string;
  updated_at: string;
}

interface SkillRecord {
  id: string;
  display_title: string | null;
  description?: string | null;
  latest_version: string | null;
  project_ids?: string[];
  source: "custom" | "anthropic" | string;
  type: string;
  created_at: string;
  updated_at: string;
}

interface SkillDraft {
  id: string;
  type: "anthropic" | "custom";
  skillId: string;
  version: string;
}

interface SubAgentDraft {
  id: string;
  agentId: string;
}

type AgentParameterType = "text" | "number" | "boolean" | "select";

interface AgentParameterDraft {
  id: string;
  key: string;
  label: string;
  type: AgentParameterType;
  defaultValue: string;
  description: string;
  options: string;
}

interface AgentParameterConfig {
  enabled: boolean;
  allowAdditional: boolean;
  parameters: AgentParameterDraft[];
}

interface ScheduleDraft {
  mode: ScheduleMode;
  interval: number;
  minute: number;
  hour: number;
  dayOfWeek: number;
  expression: string;
  timezone: string;
}

type SlackTriggerType = "none" | "all" | "channel" | "user" | "keyword";

interface SlackTriggerDraft {
  type: SlackTriggerType;
  keyword?: string;
  channel_id?: string;
  user_id?: string;
}

interface ApiTriggerDraft {
  api_key_id: string;
}

interface EmailTriggerDraft {
  receiver_id: string;
}

type ProjectNodeType = "play" | "agent" | "schedule" | "mcp" | "skill" | "slack" | "api" | "email";
type ProjectEdgeType = "runs" | "sub_agent" | "schedules" | "uses_mcp" | "uses_skill" | "slack_triggers" | "api_triggers" | "email_triggers";

interface ProjectNode {
  id: string;
  type: ProjectNodeType;
  x: number;
  y: number;
  agent_id?: string;
  mcp_server_id?: string;
  skill_id?: string;
  prompt?: string;
  schedule?: ScheduleDraft;
  slack_trigger?: SlackTriggerDraft;
  api_trigger?: ApiTriggerDraft;
  email_trigger?: EmailTriggerDraft;
  parameter_values?: Record<string, string>;
  synced_from_agent_id?: string;
  synced_ref_id?: string;
  synced_role?: "sub_agent" | "mcp" | "skill";
}

interface ProjectEdge {
  id: string;
  source: string;
  target: string;
  type: ProjectEdgeType;
  deployment_id?: string;
}

interface ProjectGraph {
  nodes: ProjectNode[];
  edges: ProjectEdge[];
}

interface ProjectCollaborator {
  email: string;
  role: "editor" | "viewer";
  user_uuid: string | null;
  invited_by_uuid: string;
  created_at: string;
  updated_at: string;
}

interface GeneratedProjectPlan {
  project: {
    name: string;
    description: string;
  };
  agents: Array<{
    id: string;
    name: string;
    description: string;
    system_prompt: string;
    model?: string;
    skill_ids?: string[];
  }>;
  triggers: Array<{
    id: string;
    type: "play" | "schedule" | "slack" | "api" | "email";
    name: string;
    description: string;
    prompt?: string;
    schedule?: ScheduleDraft;
    slack_trigger?: SlackTriggerDraft;
  }>;
  mcps: Array<{
    id: string;
    name: string;
    description: string;
  }>;
  skills: Array<{
    id: string;
    name: string;
    description: string;
    type?: "anthropic" | "custom";
    skill_id?: string;
    version?: string;
  }>;
  connections: Array<{
    from: string;
    to: string;
    type: "runs" | "sub_agent" | "uses_mcp" | "uses_skill" | "schedules" | "slack_triggers" | "api_triggers" | "email_triggers";
  }>;
}

interface ProjectRecord {
  id: string;
  name: string;
  description: string | null;
  creator_uuid: string;
  graph: ProjectGraph;
  is_public: boolean;
  collaborators: ProjectCollaborator[];
  current_user_role?: "owner" | "editor" | "viewer";
  created_at: string;
  updated_at: string;
}

interface ChatMessage {
  id: string;
  role: "user" | "assistant";
  content: string;
  awaitingApproval?: ChatApprovalWait | null;
  approvalStatus?: "pending" | "allowed" | "denied";
}

interface ChatApprovalWait {
  event_ids: string[];
  approvals: Array<{
    id: string;
    type: string;
    name?: string;
    mcp_server_name?: string;
    evaluated_permission?: string | null;
    session_thread_id?: string | null;
  } | null>;
  message: string;
}

interface ManagedSession {
  id: string;
  agent: {
    id: string;
    name: string;
    version: number;
  };
  archived_at: string | null;
  created_at: string;
  environment_id: string;
  status: "rescheduling" | "running" | "idle" | "terminated";
  title: string | null;
  updated_at: string;
  vault_ids: string[];
}

interface AnthropicDeployment {
  id: string;
  agent: { id: string; name?: string; version?: number; [key: string]: unknown };
  archived_at: string | null;
  created_at: string;
  description: string | null;
  environment_id: string;
  initial_events: unknown[];
  metadata: Record<string, string>;
  name: string;
  paused_reason: unknown | null;
  resources: unknown[];
  schedule: unknown | null;
  status: "active" | "paused" | string;
  type: "deployment";
  updated_at: string;
  vault_ids: string[];
}

interface AnthropicEnvironment {
  id: string;
  name: string;
  description: string | null;
  archived_at: string | null;
  created_at: string;
  updated_at: string;
  config: { type: string; [key: string]: unknown };
  metadata: Record<string, string>;
  scope?: "organization" | "account";
}

interface VaultRecord {
  id: string;
  managed_id?: string;
  anthropic_vault_id?: string;
  archived_at?: string | null;
  created_at: string;
  display_name: string;
  metadata?: Record<string, string>;
  type: "vault" | "global" | "project";
  scope?: "global" | "project";
  project_id?: string | null;
  updated_at: string;
}

interface VaultCredential {
  id: string;
  archived_at: string | null;
  auth: { type: string; [key: string]: unknown };
  created_at: string;
  display_name?: string | null;
  metadata: Record<string, string>;
  type: "vault_credential";
  updated_at: string;
  vault_id: string;
}

declare global {
  interface Window {
    google?: {
      accounts: {
        id: {
          initialize(config: {
            client_id: string;
            callback: (response: GoogleCredentialResponse) => void;
          }): void;
          renderButton(parent: HTMLElement, options: Record<string, unknown>): void;
        };
      };
    };
  }
}

const backendUrl = trimTrailingSlash(import.meta.env.VITE_BACKEND_URL ?? "http://127.0.0.1:8787");
const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID ?? "";
const authStorageKey = "agent-registry-auth";
const selectedProjectStorageKey = "agent-registry-selected-project";
const paletteAgentSectionsStorageKey = "agent-registry-palette-agent-sections";
const paletteMcpSectionsStorageKey = "agent-registry-palette-mcp-sections";
const paletteSkillSectionsStorageKey = "agent-registry-palette-skill-sections";
const projectIntroEnabled = true;
const themedStyle = themeVariables();
const defaultAgentModel = "claude-opus-4-8";
const agentModelOptions = [
  { value: "claude-opus-4-8", label: "Claude Opus 4.8" },
  { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
];

function App() {
  const [auth, setAuth] = React.useState<AuthSession | null>(() => readStoredAuth());
  const [routeProjectId, setRouteProjectId] = React.useState(() => readProjectIdFromPath());
  const [agents, setAgents] = React.useState<AgentRecord[]>([]);
  const [query, setQuery] = React.useState("");
  const [selectedAgent, setSelectedAgent] = React.useState<AgentRecord | null>(null);
  const [createOpen, setCreateOpen] = React.useState(false);
  const [createAgentPlacement, setCreateAgentPlacement] = React.useState<{ projectId: string; x: number; y: number } | null>(null);
  const [createdCanvasAgentPlacement, setCreatedCanvasAgentPlacement] = React.useState<{ projectId: string; agentId: string; x: number; y: number; nonce: number } | null>(null);
  const [projects, setProjects] = React.useState<ProjectRecord[]>([]);
  const [selectedProjectId, setSelectedProjectId] = React.useState(() => readProjectIdFromPath() ?? readStoredSelectedProjectId());
  const [projectsLoading, setProjectsLoading] = React.useState(false);
  const [projectSaving, setProjectSaving] = React.useState(false);
  const [projectRunning, setProjectRunning] = React.useState(false);
  const [projectsError, setProjectsError] = React.useState<string | null>(null);
  const [environmentCreateOpen, setEnvironmentCreateOpen] = React.useState(false);
  const [selectedEnvironment, setSelectedEnvironment] = React.useState<AnthropicEnvironment | null>(null);
  const [deploymentCreateOpen, setDeploymentCreateOpen] = React.useState(false);
  const [selectedDeployment, setSelectedDeployment] = React.useState<AnthropicDeployment | null>(null);
  const [mcpServerCreateOpen, setMcpServerCreateOpen] = React.useState(false);
  const [settingsPageOpen, setSettingsPageOpen] = React.useState(false);
  const [projectSettingsPageOpen, setProjectSettingsPageOpen] = React.useState(false);
  const [projectIntroOpen, setProjectIntroOpen] = React.useState(false);
  const [confirmSignOutOpen, setConfirmSignOutOpen] = React.useState(false);
  const [activeTab, setActiveTab] = React.useState<ActiveTab>("chat");
  const [chatAgentId, setChatAgentId] = React.useState("");
  const [chatEnvironmentId, setChatEnvironmentId] = React.useState("");
  const [chatVaultIds, setChatVaultIds] = React.useState<string[]>([]);
  const [chatVaultSelectionInitialized, setChatVaultSelectionInitialized] = React.useState(false);
  const [chatInput, setChatInput] = React.useState("");
  const [chatMessagesByConversation, setChatMessagesByConversation] = React.useState<Record<string, ChatMessage[]>>({});
  const [chatSessionsByConversation, setChatSessionsByConversation] = React.useState<Record<string, string>>({});
  const [sessions, setSessions] = React.useState<ManagedSession[]>([]);
  const [sessionsLoading, setSessionsLoading] = React.useState(false);
  const [removingSessionId, setRemovingSessionId] = React.useState<string | null>(null);
  const [selectedSessionId, setSelectedSessionId] = React.useState("");
  const [chatLoading, setChatLoading] = React.useState(false);
  const [approvalLoadingId, setApprovalLoadingId] = React.useState<string | null>(null);
  const [chatError, setChatError] = React.useState<string | null>(null);
  const [environments, setEnvironments] = React.useState<AnthropicEnvironment[]>([]);
  const [environmentLoading, setEnvironmentLoading] = React.useState(false);
  const [environmentSaving, setEnvironmentSaving] = React.useState(false);
  const [environmentError, setEnvironmentError] = React.useState<string | null>(null);
  const [deployments, setDeployments] = React.useState<AnthropicDeployment[]>([]);
  const [deploymentsLoading, setDeploymentsLoading] = React.useState(false);
  const [deploymentSaving, setDeploymentSaving] = React.useState(false);
  const [runningDeploymentId, setRunningDeploymentId] = React.useState<string | null>(null);
  const [deploymentsError, setDeploymentsError] = React.useState<string | null>(null);
  const [mcpServers, setMcpServers] = React.useState<RegisteredMcpServer[]>([]);
  const [mcpServersLoading, setMcpServersLoading] = React.useState(false);
  const [mcpServerSaving, setMcpServerSaving] = React.useState(false);
  const [mcpServersError, setMcpServersError] = React.useState<string | null>(null);
  const [selectedMcpServer, setSelectedMcpServer] = React.useState<RegisteredMcpServer | null>(null);
  const [integrations, setIntegrations] = React.useState<IntegrationTemplate[]>([]);
  const [integrationsLoading, setIntegrationsLoading] = React.useState(false);
  const [integrationSaving, setIntegrationSaving] = React.useState(false);
  const [integrationsError, setIntegrationsError] = React.useState<string | null>(null);
  const [integrationCreateOpen, setIntegrationCreateOpen] = React.useState(false);
  const [selectedIntegration, setSelectedIntegration] = React.useState<IntegrationTemplate | null>(null);
  const [projectIntegrationsOpen, setProjectIntegrationsOpen] = React.useState(false);
  const [skills, setSkills] = React.useState<SkillRecord[]>([]);
  const [skillsLoading, setSkillsLoading] = React.useState(false);
  const [skillSaving, setSkillSaving] = React.useState(false);
  const [skillsError, setSkillsError] = React.useState<string | null>(null);
  const [selectedSkill, setSelectedSkill] = React.useState<SkillRecord | null>(null);
  const [workspaceSkillCreateOpen, setWorkspaceSkillCreateOpen] = React.useState(false);
  const [vaults, setVaults] = React.useState<VaultRecord[]>([]);
  const [vaultsLoading, setVaultsLoading] = React.useState(false);
  const [vaultSaving, setVaultSaving] = React.useState(false);
  const [vaultsError, setVaultsError] = React.useState<string | null>(null);
  const [secretCreateVault, setSecretCreateVault] = React.useState<VaultRecord | null>(null);
  const [expandedVaultIds, setExpandedVaultIds] = React.useState<Set<string>>(() => new Set());
  const [credentialsByVault, setCredentialsByVault] = React.useState<Record<string, VaultCredential[]>>({});
  const [credentialsLoadingByVault, setCredentialsLoadingByVault] = React.useState<Record<string, boolean>>({});
  const [apiKeys, setApiKeys] = React.useState<ApiKeyRecord[]>([]);
  const [apiKeysLoading, setApiKeysLoading] = React.useState(false);
  const [apiKeySaving, setApiKeySaving] = React.useState(false);
  const [apiKeysError, setApiKeysError] = React.useState<string | null>(null);
  const [apiKeyCreateOpen, setApiKeyCreateOpen] = React.useState(false);
  const [emailReceivers, setEmailReceivers] = React.useState<EmailReceiverRecord[]>([]);
  const [emailReceiversLoading, setEmailReceiversLoading] = React.useState(false);
  const [emailReceiverSaving, setEmailReceiverSaving] = React.useState(false);
  const [emailReceiversError, setEmailReceiversError] = React.useState<string | null>(null);
  const [revealedApiKey, setRevealedApiKey] = React.useState<{ name: string; key: string } | null>(null);
  const [members, setMembers] = React.useState<Member[]>([]);
  const [membersLoading, setMembersLoading] = React.useState(false);
  const [membersError, setMembersError] = React.useState<string | null>(null);
  const [memberRoleSavingUuid, setMemberRoleSavingUuid] = React.useState<string | null>(null);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState<string | null>(null);
  const agentsRef = React.useRef<AgentRecord[]>([]);
  const mcpServersRef = React.useRef<RegisteredMcpServer[]>([]);
  const skillsRef = React.useRef<SkillRecord[]>([]);
  const routeProjectIdRef = React.useRef(routeProjectId);
  const agentConnectorUpdateQueuesRef = React.useRef<Record<string, Promise<void>>>({});

  React.useEffect(() => {
    agentsRef.current = agents;
  }, [agents]);

  React.useEffect(() => {
    mcpServersRef.current = mcpServers;
  }, [mcpServers]);

  React.useEffect(() => {
    skillsRef.current = skills;
  }, [skills]);

  React.useEffect(() => {
    routeProjectIdRef.current = routeProjectId;
  }, [routeProjectId]);

  React.useEffect(() => {
    function syncRouteProjectId() {
      setRouteProjectId(readProjectIdFromPath());
    }
    window.addEventListener("popstate", syncRouteProjectId);
    return () => window.removeEventListener("popstate", syncRouteProjectId);
  }, []);

  const loadProjects = React.useCallback(async () => {
    if (!auth) return;

    setProjectsLoading(true);
    setProjectsError(null);
    try {
      const response = await apiFetch<{ projects: ProjectRecord[] }>("/projects", auth);
      setProjects(response.projects);
      setSelectedProjectId((current) => {
        const stored = readStoredSelectedProjectId();
        const pathProjectId = routeProjectIdRef.current;
        return response.projects.find((project) => project.id === pathProjectId)?.id ?? response.projects.find((project) => project.id === current)?.id ?? response.projects.find((project) => project.id === stored)?.id ?? response.projects[0]?.id ?? "";
      });
    } catch (loadError) {
      setProjectsError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setProjectsLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadProjects();
  }, [loadProjects]);

  React.useEffect(() => {
    storeSelectedProjectId(selectedProjectId);
    if (auth && selectedProjectId) {
      replaceProjectPath(selectedProjectId);
      setRouteProjectId(selectedProjectId);
    }
  }, [auth, selectedProjectId]);

  const loadAgents = React.useCallback(async (): Promise<AgentRecord[]> => {
    if (!auth) return agentsRef.current;

    setLoading(true);
    setError(null);
    try {
      const response = await apiFetch<{ agents: AgentRecord[] }>("/agents", auth);
      const sortedAgents = sortAgents(response.agents);
      agentsRef.current = sortedAgents;
      setAgents(sortedAgents);
      return sortedAgents;
    } catch (loadError) {
      setError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
      return agentsRef.current;
    } finally {
      setLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadAgents();
  }, [loadAgents]);

  const loadEnvironments = React.useCallback(async () => {
    if (!auth) return;

    setEnvironmentLoading(true);
    setEnvironmentError(null);
    try {
      const response = await apiFetch<{ environments: AnthropicEnvironment[] }>("/environments", auth);
      setEnvironments(response.environments);
    } catch (loadError) {
      setEnvironmentError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setEnvironmentLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadEnvironments();
  }, [loadEnvironments]);

  const loadDeployments = React.useCallback(async () => {
    if (!auth) return;

    setDeploymentsLoading(true);
    setDeploymentsError(null);
    try {
      const response = await apiFetch<{ deployments: AnthropicDeployment[] }>("/deployments", auth);
      setDeployments(response.deployments);
    } catch (loadError) {
      setDeploymentsError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setDeploymentsLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadDeployments();
  }, [loadDeployments]);

  const loadMcpServers = React.useCallback(async () => {
    if (!auth) return;

    setMcpServersLoading(true);
    setMcpServersError(null);
    try {
      const response = await apiFetch<{ mcpServers: RegisteredMcpServer[] }>("/mcp-servers", auth);
      setMcpServers(response.mcpServers);
    } catch (loadError) {
      setMcpServersError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setMcpServersLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadMcpServers();
  }, [loadMcpServers]);

  const loadIntegrations = React.useCallback(async () => {
    if (!auth) return;

    setIntegrationsLoading(true);
    setIntegrationsError(null);
    try {
      const response = await apiFetch<{ integrations: IntegrationTemplate[] }>("/integrations", auth);
      setIntegrations(response.integrations);
    } catch (loadError) {
      setIntegrationsError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setIntegrationsLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadIntegrations();
  }, [loadIntegrations]);

  const loadSkills = React.useCallback(async () => {
    if (!auth) return;

    setSkillsLoading(true);
    setSkillsError(null);
    try {
      const response = await apiFetch<{ skills: SkillRecord[] }>("/skills", auth);
      setSkills(response.skills);
    } catch (loadError) {
      setSkillsError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setSkillsLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadSkills();
  }, [loadSkills]);

  const loadVaults = React.useCallback(async () => {
    if (!auth) return;

    setVaultsLoading(true);
    setVaultsError(null);
    try {
      const response = await apiFetch<{ vaults: VaultRecord[] }>("/vaults", auth);
      setVaults(response.vaults);
    } catch (loadError) {
      setVaultsError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setVaultsLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadVaults();
  }, [loadVaults]);

  React.useEffect(() => {
    if (vaults.length === 0) return;
    const vaultIds = vaults.map((vault) => vault.id);
    if (!chatVaultSelectionInitialized) {
      setChatVaultIds(vaultIds);
      setChatVaultSelectionInitialized(true);
      return;
    }
    setChatVaultIds((current) => current.filter((vaultId) => vaultIds.includes(vaultId)));
  }, [vaults, chatVaultSelectionInitialized]);

  const loadMembers = React.useCallback(async () => {
    if (!auth) return;

    setMembersLoading(true);
    setMembersError(null);
    try {
      const response = await apiFetch<{ users: Member[] }>("/users", auth);
      setMembers(response.users);
      const localMember = response.users.find((member) => member.uuid === auth.uuid);
      if (localMember && auth.role !== localMember.role) {
        const nextAuth = { ...auth, role: localMember.role };
        localStorage.setItem(authStorageKey, JSON.stringify(nextAuth));
        setAuth(nextAuth);
      }
    } catch (loadError) {
      setMembersError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setMembersLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadMembers();
  }, [loadMembers]);

  const loadApiKeys = React.useCallback(async () => {
    if (!auth) return;

    setApiKeysLoading(true);
    setApiKeysError(null);
    try {
      const response = await apiFetch<{ apiKeys: ApiKeyRecord[] }>("/api-keys", auth);
      setApiKeys(response.apiKeys);
    } catch (loadError) {
      setApiKeysError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setApiKeysLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadApiKeys();
  }, [loadApiKeys]);

  const loadEmailReceivers = React.useCallback(async () => {
    if (!auth) return;

    setEmailReceiversLoading(true);
    setEmailReceiversError(null);
    try {
      const response = await apiFetch<{ emailReceivers: EmailReceiverRecord[] }>("/email-receivers", auth);
      setEmailReceivers(response.emailReceivers);
    } catch (loadError) {
      setEmailReceiversError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setEmailReceiversLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadEmailReceivers();
  }, [loadEmailReceivers]);

  const loadSessions = React.useCallback(async () => {
    if (!auth) return;

    setSessionsLoading(true);
    try {
      const response = await apiFetch<{ sessions: ManagedSession[] }>("/sessions", auth);
      setSessions(response.sessions);
    } catch (loadError) {
      setChatError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setSessionsLoading(false);
    }
  }, [auth]);

  React.useEffect(() => {
    void loadSessions();
  }, [loadSessions]);

  React.useEffect(() => {
    if (!auth) return;
    const interval = window.setInterval(() => {
      void loadSessions();
    }, 10000);
    return () => window.clearInterval(interval);
  }, [auth, loadSessions]);

  React.useEffect(() => {
    if (!chatAgentId && agents.length > 0) {
      setChatAgentId(agents[0].id);
    }
  }, [agents, chatAgentId]);

  React.useEffect(() => {
    if (!chatEnvironmentId && environments.length > 0) {
      setChatEnvironmentId(environments[0].id);
    }
  }, [environments, chatEnvironmentId]);

  const filteredAgents = React.useMemo(() => {
    const normalized = query.trim().toLowerCase();
    return sortAgents(
      agents.filter((record) => {
        if (!normalized) return true;
        const agent = record.agent;
        return [record.id, agent.name, agent.description ?? "", modelLabel(agent.model), record.creator_uuid].some((value) =>
          value.toLowerCase().includes(normalized),
        );
      }),
    );
  }, [agents, query]);
  const selectedProject = projects.find((project) => project.id === selectedProjectId) ?? projects[0] ?? null;
  const selectedProjectCanEdit = selectedProject ? canEditProject(selectedProject) : true;
  const projectAgents = React.useMemo(
    () => agents.filter((record) => agentIsGlobal(record.agent) || Boolean(selectedProject?.id && agentInProject(record.agent, selectedProject.id))),
    [agents, selectedProject?.id],
  );
  const visibleMcpServers = React.useMemo(
    () => mcpServers.filter((server) => mcpServerIsGlobal(server) || Boolean(selectedProject?.id && server.project_ids.includes(selectedProject.id))),
    [mcpServers, selectedProject?.id],
  );
  const visibleSkills = React.useMemo(
    () => skills.filter((skill) => skillIsBuiltIn(skill) || skillIsGlobal(skill) || Boolean(selectedProject?.id && skillInProject(skill, selectedProject.id))),
    [skills, selectedProject?.id],
  );
  const selectedAgentMcpServers = React.useMemo(() => {
    const agentProjectIds = selectedAgent ? agentProjectIdsFromMetadata(selectedAgent.agent.metadata) : [];
    return mcpServers.filter((server) => mcpServerIsGlobal(server) || server.project_ids.some((projectId) => agentProjectIds.includes(projectId)));
  }, [mcpServers, selectedAgent?.agent.metadata]);
  const projectApiKeys = React.useMemo(
    () => apiKeys.filter((apiKey) => apiKey.project_id === selectedProject?.id),
    [apiKeys, selectedProject?.id],
  );
  const projectEmailReceivers = React.useMemo(
    () => emailReceivers.filter((receiver) => receiver.project_id === selectedProject?.id),
    [emailReceivers, selectedProject?.id],
  );
  const runningSessions = React.useMemo(
    () => sessions.filter((session) => session.status === "running" || session.status === "rescheduling"),
    [sessions],
  );

  function handleAuth(nextAuth: AuthSession) {
    localStorage.setItem(authStorageKey, JSON.stringify(nextAuth));
    setAuth(nextAuth);
  }

  function signOut() {
    clearStoredAuth();
    setAuth(null);
    setProjects([]);
    setSelectedProjectId("");
    setAgents([]);
    setSelectedAgent(null);
    setDeployments([]);
    setSelectedDeployment(null);
    setMcpServers([]);
    setIntegrations([]);
    setSelectedIntegration(null);
    setProjectIntegrationsOpen(false);
    setVaults([]);
    setApiKeys([]);
    setRevealedApiKey(null);
    setSessions([]);
    setSelectedSessionId("");
    setChatVaultIds([]);
    setChatVaultSelectionInitialized(false);
    setCredentialsByVault({});
    setExpandedVaultIds(new Set());
    setMembers([]);
  }

  async function updateMemberRole(member: Member, role: WorkspaceRole) {
    if (!auth || member.role === role) return;

    setMemberRoleSavingUuid(member.uuid);
    setMembersError(null);
    try {
      const response = await apiFetch<{ users: Member[] }>(`/users/${encodeURIComponent(member.uuid)}`, auth, {
        method: "PATCH",
        body: JSON.stringify({ role }),
      });
      setMembers(response.users);
      const localMember = response.users.find((candidate) => candidate.uuid === auth.uuid);
      if (localMember) {
        const nextAuth = { ...auth, role: localMember.role };
        localStorage.setItem(authStorageKey, JSON.stringify(nextAuth));
        setAuth(nextAuth);
      }
    } catch (updateError) {
      setMembersError(errorMessage(updateError));
    } finally {
      setMemberRoleSavingUuid(null);
    }
  }

  async function handleCreated(agent: Agent) {
    setCreateOpen(false);
    await loadAgents();
    if (createAgentPlacement) {
      setCreatedCanvasAgentPlacement({
        ...createAgentPlacement,
        agentId: agent.id,
        nonce: Date.now(),
      });
      setCreateAgentPlacement(null);
    }
  }

  async function handleChanged() {
    setSelectedAgent(null);
    await loadAgents();
  }

  async function createProject() {
    if (!auth) return;

    setProjectSaving(true);
    setProjectsError(null);
    try {
      const response = await apiFetch<{ project: ProjectRecord }>("/projects", auth, {
        method: "POST",
        body: JSON.stringify({
          name: `Project ${projects.length + 1}`,
          description: null,
          graph: createDefaultProjectGraph(),
        }),
      });
      await loadProjects();
      setSelectedProjectId(response.project.id);
    } catch (createError) {
      setProjectsError(errorMessage(createError));
    } finally {
      setProjectSaving(false);
    }
  }

  async function generateProjectPlan(prompt: string): Promise<GeneratedProjectPlan> {
    if (!auth) throw new Error("Sign in before generating a project.");

    setProjectSaving(true);
    setProjectsError(null);
    try {
      const response = await apiFetch<{ plan: GeneratedProjectPlan }>("/projects/generate", auth, {
        method: "POST",
        body: JSON.stringify({ prompt }),
      });
      return response.plan;
    } catch (generateError) {
      setProjectsError(errorMessage(generateError));
      throw generateError;
    } finally {
      setProjectSaving(false);
    }
  }

  async function confirmGeneratedProject(plan: GeneratedProjectPlan): Promise<ProjectRecord> {
    if (!auth) throw new Error("Sign in before creating a project.");

    setProjectSaving(true);
    setProjectsError(null);
    try {
      const response = await apiFetch<{ project: ProjectRecord }>("/projects/confirm", auth, {
        method: "POST",
        body: JSON.stringify({ plan }),
      });
      await Promise.all([loadProjects(), loadAgents()]);
      setSelectedProjectId(response.project.id);
      return response.project;
    } catch (confirmError) {
      setProjectsError(errorMessage(confirmError));
      throw confirmError;
    } finally {
      setProjectSaving(false);
    }
  }

  async function saveProject(project: ProjectRecord) {
    if (!auth) return;

    setProjectSaving(true);
    setProjectsError(null);
    try {
      const response = await apiFetch<{ project: ProjectRecord }>(`/projects/${encodeURIComponent(project.id)}`, auth, {
        method: "PATCH",
        body: JSON.stringify({
          name: project.name,
          description: project.description,
          graph: project.graph,
        }),
      });
      setProjects((current) => current.map((item) => (item.id === response.project.id ? response.project : item)));
    } catch (saveError) {
      setProjectsError(errorMessage(saveError));
    } finally {
      setProjectSaving(false);
    }
  }

  async function inviteProjectCollaborator(project: ProjectRecord, email: string, role: ProjectCollaborator["role"]) {
    if (!auth) return;

    setProjectSaving(true);
    setProjectsError(null);
    try {
      const response = await apiFetch<{ project: ProjectRecord }>(`/projects/${encodeURIComponent(project.id)}/collaborators`, auth, {
        method: "POST",
        body: JSON.stringify({ email, role }),
      });
      setProjects((current) => current.map((item) => (item.id === response.project.id ? response.project : item)));
    } catch (inviteError) {
      setProjectsError(errorMessage(inviteError));
      throw inviteError;
    } finally {
      setProjectSaving(false);
    }
  }

  async function removeProjectCollaborator(project: ProjectRecord, collaborator: ProjectCollaborator) {
    if (!auth) return;

    setProjectSaving(true);
    setProjectsError(null);
    try {
      const response = await apiFetch<{ project: ProjectRecord }>(
        `/projects/${encodeURIComponent(project.id)}/collaborators/${encodeURIComponent(collaborator.email)}`,
        auth,
        { method: "DELETE" },
      );
      setProjects((current) => current.map((item) => (item.id === response.project.id ? response.project : item)));
    } catch (removeError) {
      setProjectsError(errorMessage(removeError));
    } finally {
      setProjectSaving(false);
    }
  }

  async function deleteProject(project: ProjectRecord) {
    if (!auth) return;
    if (!window.confirm(`Remove project "${project.name}"?`)) return;

    setProjectSaving(true);
    setProjectsError(null);
    try {
      await apiFetch<{ ok: true }>(`/projects/${encodeURIComponent(project.id)}`, auth, { method: "DELETE" });
      setProjects((current) => {
        const next = current.filter((item) => item.id !== project.id);
        setSelectedProjectId(next[0]?.id ?? "");
        return next;
      });
    } catch (deleteError) {
      setProjectsError(errorMessage(deleteError));
    } finally {
      setProjectSaving(false);
    }
  }

  async function runProject(project: ProjectRecord, environmentId: string) {
    if (!auth) return;

    setProjectRunning(true);
    setProjectsError(null);
    try {
      await apiFetch<{ session: ManagedSession }>(`/projects/${encodeURIComponent(project.id)}/run`, auth, {
        method: "POST",
        body: JSON.stringify({ environment_id: environmentId }),
      });
      await loadSessions();
      setActiveTab("chat");
    } catch (runError) {
      setProjectsError(errorMessage(runError));
    } finally {
      setProjectRunning(false);
    }
  }

  async function sendChatMessage(event: React.FormEvent) {
    event.preventDefault();
    const content = chatInput.trim();
    if (!content || !chatAgentId || !chatEnvironmentId || chatLoading || !auth) return;
    const conversationKey = chatKey(chatAgentId, chatEnvironmentId, selectedProject ? [selectedProject.id] : []);

    const userMessage: ChatMessage = { id: crypto.randomUUID(), role: "user", content };
    setChatMessagesByConversation((current) => ({
      ...current,
      [conversationKey]: [...(current[conversationKey] ?? []), userMessage],
    }));
    setChatInput("");
    setChatLoading(true);
    setChatError(null);

    try {
      const response = await apiFetch<{ sessionId: string; messages: string[]; awaitingApproval?: ChatApprovalWait | null }>(
        "/chat",
        auth,
        {
          method: "POST",
          body: JSON.stringify({
            agentId: chatAgentId,
            environment_id: chatEnvironmentId,
            sessionId: chatSessionsByConversation[conversationKey],
            ...(selectedProject ? { project_id: selectedProject.id } : {}),
            message: content,
          }),
        },
      );
      setChatSessionsByConversation((current) => ({ ...current, [conversationKey]: response.sessionId }));
      setSelectedSessionId(response.sessionId);
      appendChatResponse(conversationKey, response);
      await loadSessions();
    } catch (chatSendError) {
      setChatError(errorMessage(chatSendError));
    } finally {
      setChatLoading(false);
    }
  }

  function appendChatResponse(conversationKey: string, response: { messages: string[]; awaitingApproval?: ChatApprovalWait | null }) {
    const nextMessages: ChatMessage[] =
      response.messages.length > 0
        ? response.messages.map((message) => ({
            id: crypto.randomUUID(),
            role: "assistant" as const,
            content: message,
          }))
        : response.awaitingApproval
          ? [
              {
                id: crypto.randomUUID(),
                role: "assistant" as const,
                content: formatApprovalWait(response.awaitingApproval),
                awaitingApproval: response.awaitingApproval,
                approvalStatus: "pending" as const,
              },
            ]
          : [
              {
                id: crypto.randomUUID(),
                role: "assistant" as const,
                content: "The agent completed without returning text.",
              },
            ];

    setChatMessagesByConversation((current) => ({
      ...current,
      [conversationKey]: [...(current[conversationKey] ?? []), ...nextMessages],
    }));
  }

  async function confirmApproval(message: ChatMessage, result: "allow" | "deny") {
    if (!auth || !message.awaitingApproval || approvalLoadingId || chatLoading) return;
    const conversationKey = chatKey(chatAgentId, chatEnvironmentId, selectedProject ? [selectedProject.id] : []);
    const sessionId = chatSessionsByConversation[conversationKey];
    const approval = firstToolApproval(message.awaitingApproval);
    if (!sessionId || !approval) return;

    setApprovalLoadingId(message.id);
    setChatError(null);
    setChatMessagesByConversation((current) => ({
      ...current,
      [conversationKey]: (current[conversationKey] ?? []).map((chatMessage) =>
        chatMessage.id === message.id ? { ...chatMessage, approvalStatus: result === "allow" ? "allowed" : "denied" } : chatMessage,
      ),
    }));

    try {
      const response = await apiFetch<{ sessionId: string; messages: string[]; awaitingApproval?: ChatApprovalWait | null }>("/chat/approval", auth, {
        method: "POST",
        body: JSON.stringify({
          sessionId,
          tool_use_id: approval.id,
          result,
          ...(approval.session_thread_id ? { session_thread_id: approval.session_thread_id } : {}),
        }),
      });
      setChatSessionsByConversation((current) => ({ ...current, [conversationKey]: response.sessionId }));
      appendChatResponse(conversationKey, response);
      await loadSessions();
    } catch (approvalError) {
      setChatError(errorMessage(approvalError));
      setChatMessagesByConversation((current) => ({
        ...current,
        [conversationKey]: (current[conversationKey] ?? []).map((chatMessage) =>
          chatMessage.id === message.id ? { ...chatMessage, approvalStatus: "pending" } : chatMessage,
        ),
      }));
    } finally {
      setApprovalLoadingId(null);
    }
  }

  async function selectChatSession(session: ManagedSession) {
    if (!auth) return;

    const vaultIds = session.vault_ids ?? [];
    const conversationKey = chatKey(session.agent.id, session.environment_id, vaultIds);
    setSelectedSessionId(session.id);
    setChatAgentId(session.agent.id);
    setChatEnvironmentId(session.environment_id);
    setChatVaultIds(vaultIds);
    setChatSessionsByConversation((current) => ({ ...current, [conversationKey]: session.id }));
    setChatLoading(true);
    setChatError(null);
    try {
      const response = await apiFetch<{
        messages: Array<{ role: "user" | "assistant"; content: string }>;
        awaitingApproval?: ChatApprovalWait | null;
      }>(`/sessions/${encodeURIComponent(session.id)}/events`, auth);
      const loadedMessages: ChatMessage[] = response.messages.map((message) => ({
        id: crypto.randomUUID(),
        role: message.role,
        content: message.content,
      }));
      if (response.awaitingApproval) {
        loadedMessages.push({
          id: crypto.randomUUID(),
          role: "assistant",
          content: formatApprovalWait(response.awaitingApproval),
          awaitingApproval: response.awaitingApproval,
          approvalStatus: "pending",
        });
      }
      setChatMessagesByConversation((current) => ({ ...current, [conversationKey]: loadedMessages }));
    } catch (selectError) {
      setChatError(errorMessage(selectError));
    } finally {
      setChatLoading(false);
    }
  }

  function openRunningSession(session: ManagedSession) {
    setSettingsPageOpen(true);
    setProjectSettingsPageOpen(false);
    setActiveTab("chat");
    void selectChatSession(session);
  }

  async function loadSessionMessages(sessionId: string): Promise<ChatMessage[]> {
    if (!auth) return [];
    const response = await apiFetch<{
      messages: Array<{ role: "user" | "assistant"; content: string }>;
      awaitingApproval?: ChatApprovalWait | null;
    }>(`/sessions/${encodeURIComponent(sessionId)}/events`, auth);
    const loadedMessages: ChatMessage[] = response.messages.map((message) => ({
      id: crypto.randomUUID(),
      role: message.role,
      content: message.content,
    }));
    if (response.awaitingApproval) {
      loadedMessages.push({
        id: crypto.randomUUID(),
        role: "assistant",
        content: formatApprovalWait(response.awaitingApproval),
        awaitingApproval: response.awaitingApproval,
        approvalStatus: "pending",
      });
    }
    return loadedMessages;
  }

  async function runCanvasPlay(agentIds: string[], prompt: string): Promise<string[]> {
    if (!auth) return [];
    const environmentId = environments[0]?.id;
    if (!environmentId) {
      setProjectsError("Create an environment before running a play card.");
      return [];
    }
    const content = prompt.trim();
    if (!content) {
      setProjectsError("Add a prompt to the play card before running it.");
      return [];
    }

    setProjectRunning(true);
    setProjectsError(null);
    try {
      const sessionIds: string[] = [];
      for (const agentId of agentIds) {
        const response = await apiFetch<{ sessionId: string; messages: string[]; awaitingApproval?: ChatApprovalWait | null }>("/chat", auth, {
          method: "POST",
          body: JSON.stringify({
            agentId,
            environment_id: environmentId,
            ...(selectedProject ? { project_id: selectedProject.id } : {}),
            message: content,
          }),
        });
        sessionIds.push(response.sessionId);
      }
      await loadSessions();
      return sessionIds;
    } catch (runError) {
      setProjectsError(errorMessage(runError));
      return [];
    } finally {
      setProjectRunning(false);
    }
  }

  async function removeChatSession(session: ManagedSession) {
    if (!auth || removingSessionId) return;

    const vaultIds = session.vault_ids ?? [];
    const conversationKey = chatKey(session.agent.id, session.environment_id, vaultIds);
    setRemovingSessionId(session.id);
    setChatError(null);
    try {
      await apiFetch<{ session: unknown }>(`/sessions/${encodeURIComponent(session.id)}`, auth, { method: "DELETE" });
      setSessions((current) => current.filter((item) => item.id !== session.id));
      setChatSessionsByConversation((current) => {
        const next = { ...current };
        if (next[conversationKey] === session.id) {
          delete next[conversationKey];
        }
        return next;
      });
      setChatMessagesByConversation((current) => {
        const next = { ...current };
        delete next[conversationKey];
        return next;
      });
      if (selectedSessionId === session.id) {
        setSelectedSessionId("");
      }
      await loadSessions();
    } catch (removeError) {
      setChatError(errorMessage(removeError));
    } finally {
      setRemovingSessionId(null);
    }
  }

  async function createEnvironment(payload: JsonObject) {
    if (!auth) return;

    setEnvironmentSaving(true);
    setEnvironmentError(null);
    try {
      await apiFetch<{ environment: AnthropicEnvironment }>("/environments", auth, {
        method: "POST",
        body: JSON.stringify(payload),
      });
      await loadEnvironments();
    } catch (createError) {
      setEnvironmentError(errorMessage(createError));
      throw createError;
    } finally {
      setEnvironmentSaving(false);
    }
  }

  async function updateEnvironment(environmentId: string, payload: JsonObject) {
    if (!auth) return;

    setEnvironmentSaving(true);
    setEnvironmentError(null);
    try {
      await apiFetch<{ environment: AnthropicEnvironment }>(`/environments/${encodeURIComponent(environmentId)}`, auth, {
        method: "PATCH",
        body: JSON.stringify(payload),
      });
      await loadEnvironments();
    } catch (updateError) {
      setEnvironmentError(errorMessage(updateError));
      throw updateError;
    } finally {
      setEnvironmentSaving(false);
    }
  }

  async function createDeployment(payload: JsonObject) {
    if (!auth) return null;

    setDeploymentSaving(true);
    setDeploymentsError(null);
    try {
      const response = await apiFetch<{ deployment: AnthropicDeployment }>("/deployments", auth, {
        method: "POST",
        body: JSON.stringify(payload),
      });
      await loadDeployments();
      return response.deployment;
    } catch (createError) {
      setDeploymentsError(errorMessage(createError));
      throw createError;
    } finally {
      setDeploymentSaving(false);
    }
  }

  function enqueueAgentConnectorUpdate(agentId: string, task: () => Promise<void>) {
    const previous = agentConnectorUpdateQueuesRef.current[agentId] ?? Promise.resolve();
    const next = previous
      .catch(() => undefined)
      .then(task)
      .finally(() => {
        if (agentConnectorUpdateQueuesRef.current[agentId] === next) {
          delete agentConnectorUpdateQueuesRef.current[agentId];
        }
      });
    agentConnectorUpdateQueuesRef.current[agentId] = next;
    return next;
  }

  async function updateAgentSubAgent(agentId: string, subAgentId: string, enabled: boolean) {
    if (!auth) return;
    setError(null);
    void enqueueAgentConnectorUpdate(agentId, async () => {
      try {
        await patchAgentSubAgent(agentId, subAgentId, enabled);
      } catch (updateError) {
        if (!isConflict(updateError)) {
          setProjectsError(errorMessage(updateError));
          return;
        }
        await loadAgents();
        try {
          await patchAgentSubAgent(agentId, subAgentId, enabled);
        } catch (retryError) {
          setProjectsError(errorMessage(retryError));
        }
      }
    });
  }

  async function updateAgentMcp(agentId: string, mcpServerId: string, enabled: boolean) {
    if (!auth) return;
    setError(null);
    void enqueueAgentConnectorUpdate(agentId, async () => {
      try {
        await patchAgentMcp(agentId, mcpServerId, enabled);
      } catch (updateError) {
        if (!isConflict(updateError)) {
          setProjectsError(errorMessage(updateError));
          return;
        }
        await loadAgents();
        try {
          await patchAgentMcp(agentId, mcpServerId, enabled);
        } catch (retryError) {
          setProjectsError(errorMessage(retryError));
        }
      }
    });
  }

  async function updateAgentSkill(agentId: string, skillId: string, enabled: boolean) {
    if (!auth) return;
    setError(null);
    void enqueueAgentConnectorUpdate(agentId, async () => {
      try {
        await patchAgentSkill(agentId, skillId, enabled);
      } catch (updateError) {
        if (!isConflict(updateError)) {
          setProjectsError(errorMessage(updateError));
          return;
        }
        await loadAgents();
        try {
          await patchAgentSkill(agentId, skillId, enabled);
        } catch (retryError) {
          setProjectsError(errorMessage(retryError));
        }
      }
    });
  }

  async function patchAgentSubAgent(agentId: string, subAgentId: string, enabled: boolean) {
    if (!auth) return;
    const record = agentsRef.current.find((item) => item.id === agentId);
    if (!record) return;
    const currentIds = subAgentIds(record.agent.multiagent);
    const nextIds = enabled ? [...new Set([...currentIds, subAgentId])] : currentIds.filter((id) => id !== subAgentId);
    const subAgents = nextIds.map((id) => ({ id: crypto.randomUUID(), agentId: id }));

    await apiFetch<{ agent: Agent }>(`/agents/${encodeURIComponent(agentId)}`, auth, {
      method: "PATCH",
      body: JSON.stringify({
        version: record.agent.version,
        multiagent: serializeSubAgents(subAgents),
      }),
    });
    await loadAgents();
  }

  async function patchAgentMcp(agentId: string, mcpServerId: string, enabled: boolean) {
    if (!auth) return;
    const record = agentsRef.current.find((item) => item.id === agentId);
    const server = mcpServersRef.current.find((item) => item.id === mcpServerId);
    if (!record || !server) return;

    const currentDrafts = mcpServerDraftsFromAgent(record.agent.mcp_servers, mcpServersRef.current);
    const nextDrafts = enabled
      ? currentDrafts.some((draft) => draft.registryId === server.id)
        ? currentDrafts
        : [...currentDrafts, mcpServerDraftFromRegistered(server)]
      : currentDrafts.filter((draft) => draft.registryId !== server.id);
    const selectedMcpServers = serializeMcpServerDrafts(nextDrafts, mcpServersRef.current);

    await apiFetch<{ agent: Agent }>(`/agents/${encodeURIComponent(agentId)}`, auth, {
      method: "PATCH",
      body: JSON.stringify({
        version: record.agent.version,
        mcp_servers: selectedMcpServers,
        tools: serializeMcpToolsets(selectedMcpServers),
      }),
    });
    await loadAgents();
  }

  async function patchAgentSkill(agentId: string, skillId: string, enabled: boolean) {
    if (!auth) return;
    const record = agentsRef.current.find((item) => item.id === agentId);
    const skill = skillsRef.current.find((item) => item.id === skillId);
    if (!record || !skill) return;

    const currentDrafts = skillDraftsFromAgent(record.agent.skills);
    const nextSkill: SkillDraft = {
      id: crypto.randomUUID(),
      type: skill.source === "anthropic" ? "anthropic" : "custom",
      skillId: skill.id,
      version: skill.latest_version ?? "",
    };
    const nextDrafts = enabled
      ? currentDrafts.some((draft) => draft.skillId === skill.id)
        ? currentDrafts
        : [...currentDrafts, nextSkill]
      : currentDrafts.filter((draft) => draft.skillId !== skill.id);

    await apiFetch<{ agent: Agent }>(`/agents/${encodeURIComponent(agentId)}`, auth, {
      method: "PATCH",
      body: JSON.stringify({
        version: record.agent.version,
        skills: serializeSkillDrafts(nextDrafts),
      }),
    });
    await loadAgents();
  }

  async function createSkill(payload: { name: string; description: string; files: File[]; publicUrl: string; projectIds: string[] }) {
    if (!auth) return;

    setSkillSaving(true);
    setSkillsError(null);
    try {
      const form = skillFormData(payload);
      form.append("display_title", payload.name.trim());
      form.append("description", payload.description.trim());
      form.append("project_ids", JSON.stringify(payload.projectIds));
      await apiFetch<{ skill: SkillRecord }>("/skills", auth, {
        method: "POST",
        body: form,
      });
      await loadSkills();
    } catch (createError) {
      setSkillsError(errorMessage(createError));
      throw createError;
    } finally {
      setSkillSaving(false);
    }
  }

  async function createSkillVersion(skillId: string, payload: { files: File[]; publicUrl: string }) {
    if (!auth) return;

    setSkillSaving(true);
    setSkillsError(null);
    try {
      await apiFetch<{ version: unknown }>(`/skills/${encodeURIComponent(skillId)}/versions`, auth, {
        method: "POST",
        body: skillFormData(payload),
      });
      await loadSkills();
      setSelectedSkill((current) => (current?.id === skillId ? (skillsRef.current.find((skill) => skill.id === skillId) ?? current) : current));
    } catch (updateError) {
      setSkillsError(errorMessage(updateError));
      throw updateError;
    } finally {
      setSkillSaving(false);
    }
  }

  async function updateSkillMetadata(skillId: string, payload: { name: string; description: string; projectIds: string[] }) {
    if (!auth) return;

    setSkillSaving(true);
    setSkillsError(null);
    try {
      await apiFetch<{ skill: SkillRecord }>(`/skills/${encodeURIComponent(skillId)}`, auth, {
        method: "PATCH",
        body: JSON.stringify({
          display_title: payload.name.trim(),
          description: nullableText(payload.description),
          project_ids: payload.projectIds,
        }),
      });
      await loadSkills();
      setSelectedSkill((current) => (current?.id === skillId ? (skillsRef.current.find((skill) => skill.id === skillId) ?? current) : current));
    } catch (updateError) {
      setSkillsError(errorMessage(updateError));
      throw updateError;
    } finally {
      setSkillSaving(false);
    }
  }

  function skillFormData(payload: { files: File[]; publicUrl: string }) {
    const form = new FormData();
    if (payload.publicUrl.trim()) form.append("public_url", payload.publicUrl.trim());
    for (const file of payload.files) {
      const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name;
      form.append("files", file, relativePath);
    }
    return form;
  }

  async function createScheduledDeploymentFromCanvas(project: ProjectRecord, _graph: ProjectGraph, scheduleNode: ProjectNode, agentNode: ProjectNode) {
    const firstEnvironment = environments[0];
    const agentRecord = agentNode?.agent_id ? agents.find((record) => record.id === agentNode.agent_id) : undefined;
    if (!firstEnvironment || !agentRecord || !scheduleNode.schedule) return null;

    const deployment = await createDeployment({
      name: `${project.name || "Canvas"} · ${agentRecord.agent.name}`,
      description: `Created from ${project.name || "My Canvas"}.`,
      agent: agentRecord.id,
      environment_id: firstEnvironment.id,
      project_id: project.id,
      initial_events: deploymentInitialEvents(scheduleNode.prompt?.trim() || "Run this deployment."),
      resources: [],
      metadata: {
        project_id: project.id,
        schedule_node_id: scheduleNode.id,
        agent_node_id: agentNode.id,
      },
      schedule: {
        type: "cron",
        expression: cronExpressionForSchedule(scheduleNode.schedule),
        timezone: scheduleNode.schedule.timezone,
      },
    });
    return deployment?.id ?? null;
  }

  async function updateDeployment(deploymentId: string, payload: JsonObject) {
    if (!auth) return;

    setDeploymentSaving(true);
    setDeploymentsError(null);
    try {
      await apiFetch<{ deployment: AnthropicDeployment }>(`/deployments/${encodeURIComponent(deploymentId)}`, auth, {
        method: "PATCH",
        body: JSON.stringify(payload),
      });
      setSelectedDeployment(null);
      await loadDeployments();
    } catch (updateError) {
      setDeploymentsError(errorMessage(updateError));
      throw updateError;
    } finally {
      setDeploymentSaving(false);
    }
  }

  async function deleteDeployment(deploymentId: string) {
    if (!auth) return;

    setDeploymentSaving(true);
    setDeploymentsError(null);
    try {
      await apiFetch<{ deployment: AnthropicDeployment }>(`/deployments/${encodeURIComponent(deploymentId)}`, auth, { method: "DELETE" });
      setSelectedDeployment(null);
      await loadDeployments();
    } catch (deleteError) {
      setDeploymentsError(errorMessage(deleteError));
      throw deleteError;
    } finally {
      setDeploymentSaving(false);
    }
  }

  async function runDeployment(deploymentId: string) {
    if (!auth || runningDeploymentId) return;

    setRunningDeploymentId(deploymentId);
    setDeploymentsError(null);
    try {
      await apiFetch<{ run: unknown }>(`/deployments/${encodeURIComponent(deploymentId)}/run`, auth, {
        method: "POST",
        body: "{}",
      });
      await Promise.all([loadDeployments(), loadSessions()]);
    } catch (runError) {
      setDeploymentsError(errorMessage(runError));
    } finally {
      setRunningDeploymentId(null);
    }
  }

  async function createMcpServer(payload: JsonObject) {
    if (!auth) return;

    setMcpServerSaving(true);
    setMcpServersError(null);
    try {
      await apiFetch<{ mcpServer: RegisteredMcpServer }>("/mcp-servers", auth, {
        method: "POST",
        body: JSON.stringify(payload),
      });
      await Promise.all([loadMcpServers(), loadVaults()]);
    } catch (createError) {
      setMcpServersError(errorMessage(createError));
      throw createError;
    } finally {
      setMcpServerSaving(false);
    }
  }

  async function updateMcpServer(serverId: string, payload: JsonObject) {
    if (!auth) return;

    setMcpServerSaving(true);
    setMcpServersError(null);
    try {
      await apiFetch<{ mcpServer: RegisteredMcpServer }>(`/mcp-servers/${encodeURIComponent(serverId)}`, auth, {
        method: "PATCH",
        body: JSON.stringify(payload),
      });
      await Promise.all([loadMcpServers(), loadVaults()]);
    } catch (updateError) {
      setMcpServersError(errorMessage(updateError));
      throw updateError;
    } finally {
      setMcpServerSaving(false);
    }
  }

  async function deleteMcpServer(serverId: string) {
    if (!auth) return;

    setMcpServerSaving(true);
    setMcpServersError(null);
    try {
      await apiFetch<{ ok: true }>(`/mcp-servers/${encodeURIComponent(serverId)}`, auth, { method: "DELETE" });
      setSelectedMcpServer(null);
      await Promise.all([loadMcpServers(), loadAgents()]);
    } catch (deleteError) {
      setMcpServersError(errorMessage(deleteError));
      throw deleteError;
    } finally {
      setMcpServerSaving(false);
    }
  }

  async function createIntegration(payload: JsonObject) {
    if (!auth) return;

    setIntegrationSaving(true);
    setIntegrationsError(null);
    try {
      await apiFetch<{ integration: IntegrationTemplate }>("/integrations", auth, {
        method: "POST",
        body: JSON.stringify(payload),
      });
      await loadIntegrations();
    } catch (createError) {
      setIntegrationsError(errorMessage(createError));
      throw createError;
    } finally {
      setIntegrationSaving(false);
    }
  }

  async function updateIntegration(templateId: string, payload: JsonObject) {
    if (!auth) return;

    setIntegrationSaving(true);
    setIntegrationsError(null);
    try {
      await apiFetch<{ integration: IntegrationTemplate }>(`/integrations/${encodeURIComponent(templateId)}`, auth, {
        method: "PATCH",
        body: JSON.stringify(payload),
      });
      await loadIntegrations();
    } catch (updateError) {
      setIntegrationsError(errorMessage(updateError));
      throw updateError;
    } finally {
      setIntegrationSaving(false);
    }
  }

  async function deleteIntegration(templateId: string) {
    if (!auth) return;

    setIntegrationSaving(true);
    setIntegrationsError(null);
    try {
      await apiFetch<{ ok: true }>(`/integrations/${encodeURIComponent(templateId)}`, auth, { method: "DELETE" });
      setSelectedIntegration(null);
      await loadIntegrations();
    } catch (deleteError) {
      setIntegrationsError(errorMessage(deleteError));
      throw deleteError;
    } finally {
      setIntegrationSaving(false);
    }
  }

  async function installIntegration(template: IntegrationTemplate, payload: JsonObject) {
    if (!auth || !selectedProject) return;

    setIntegrationSaving(true);
    setProjectsError(null);
    try {
      const response = await apiFetch<{ agent: Agent }>(
        `/projects/${encodeURIComponent(selectedProject.id)}/integrations/${encodeURIComponent(template.id)}/install`,
        auth,
        {
          method: "POST",
          body: JSON.stringify(payload),
        },
      );
      await Promise.all([loadAgents(), loadVaults()]);
      setCreatedCanvasAgentPlacement({
        projectId: selectedProject.id,
        agentId: response.agent.id,
        x: 180,
        y: 160,
        nonce: Date.now(),
      });
      setProjectIntegrationsOpen(false);
    } catch (installError) {
      setProjectsError(errorMessage(installError));
      throw installError;
    } finally {
      setIntegrationSaving(false);
    }
  }

  async function createApiKey(name: string, projectId = selectedProjectId): Promise<{ apiKey: ApiKeyRecord; key: string } | null> {
    if (!auth) return null;
    if (!projectId) throw new Error("Select a project before creating an API key.");

    setApiKeySaving(true);
    setApiKeysError(null);
    try {
      const response = await apiFetch<{ apiKey: ApiKeyRecord; key: string }>("/api-keys", auth, {
        method: "POST",
        body: JSON.stringify({ name, project_id: projectId }),
      });
      setRevealedApiKey({ name: response.apiKey.name, key: response.key });
      await loadApiKeys();
      return response;
    } catch (createError) {
      setApiKeysError(errorMessage(createError));
      throw createError;
    } finally {
      setApiKeySaving(false);
    }
  }

  async function rotateApiKey(apiKey: ApiKeyRecord, confirm = true): Promise<{ apiKey: ApiKeyRecord; key: string } | null> {
    if (!auth) return null;
    if (confirm && !window.confirm(`Rotate API key "${apiKey.name}"? Existing clients using this key will stop authenticating.`)) return null;

    setApiKeySaving(true);
    setApiKeysError(null);
    try {
      const response = await apiFetch<{ apiKey: ApiKeyRecord; key: string }>(`/api-keys/${encodeURIComponent(apiKey.id)}/rotate`, auth, {
        method: "POST",
        body: "{}",
      });
      setRevealedApiKey({ name: response.apiKey.name, key: response.key });
      await loadApiKeys();
      return response;
    } catch (rotateError) {
      setApiKeysError(errorMessage(rotateError));
      throw rotateError;
    } finally {
      setApiKeySaving(false);
    }
  }

  async function deleteApiKey(apiKey: ApiKeyRecord) {
    if (!auth) return;
    if (!window.confirm(`Delete API key "${apiKey.name}"? Clients using this key will stop authenticating.`)) return;

    setApiKeySaving(true);
    setApiKeysError(null);
    try {
      await apiFetch<{ ok: true }>(`/api-keys/${encodeURIComponent(apiKey.id)}`, auth, { method: "DELETE" });
      setRevealedApiKey((current) => (current?.name === apiKey.name ? null : current));
      await loadApiKeys();
    } catch (deleteError) {
      setApiKeysError(errorMessage(deleteError));
    } finally {
      setApiKeySaving(false);
    }
  }

  async function createEmailReceiver(name: string, projectId = selectedProjectId): Promise<{ emailReceiver: EmailReceiverRecord } | null> {
    if (!auth) return null;
    if (!projectId) throw new Error("Select a project before creating an email receiver.");

    setEmailReceiverSaving(true);
    setEmailReceiversError(null);
    try {
      const response = await apiFetch<{ emailReceiver: EmailReceiverRecord }>("/email-receivers", auth, {
        method: "POST",
        body: JSON.stringify({ name, project_id: projectId }),
      });
      await loadEmailReceivers();
      return response;
    } catch (createError) {
      setEmailReceiversError(errorMessage(createError));
      throw createError;
    } finally {
      setEmailReceiverSaving(false);
    }
  }

  async function loadVaultCredentials(vaultId: string) {
    if (!auth) return;

    setCredentialsLoadingByVault((current) => ({ ...current, [vaultId]: true }));
    setVaultsError(null);
    try {
      const response = await apiFetch<{ credentials: VaultCredential[] }>(`/vaults/${encodeURIComponent(vaultId)}/credentials`, auth);
      setCredentialsByVault((current) => ({ ...current, [vaultId]: response.credentials }));
    } catch (loadError) {
      setVaultsError(errorMessage(loadError));
      if (isUnauthorized(loadError)) {
        clearStoredAuth();
        setAuth(null);
      }
    } finally {
      setCredentialsLoadingByVault((current) => ({ ...current, [vaultId]: false }));
    }
  }

  async function toggleVault(vaultId: string) {
    const isExpanded = expandedVaultIds.has(vaultId);
    setExpandedVaultIds((current) => {
      const next = new Set(current);
      if (next.has(vaultId)) {
        next.delete(vaultId);
      } else {
        next.add(vaultId);
      }
      return next;
    });
    if (!isExpanded && !credentialsByVault[vaultId]) {
      await loadVaultCredentials(vaultId);
    }
  }

  async function createVaultCredential(vaultId: string, payload: JsonObject) {
    if (!auth) return;

    setVaultSaving(true);
    setVaultsError(null);
    try {
      await apiFetch<{ credential: VaultCredential }>(`/vaults/${encodeURIComponent(vaultId)}/credentials`, auth, {
        method: "POST",
        body: JSON.stringify(payload),
      });
      setExpandedVaultIds((current) => new Set(current).add(vaultId));
      await loadVaultCredentials(vaultId);
    } catch (createError) {
      setVaultsError(errorMessage(createError));
      throw createError;
    } finally {
      setVaultSaving(false);
    }
  }

  async function deleteVault(vaultId: string) {
    if (!auth) return;

    setVaultsError(null);
    try {
      await apiFetch<{ vault: unknown }>(`/vaults/${encodeURIComponent(vaultId)}`, auth, { method: "DELETE" });
      setCredentialsByVault((current) => {
        const next = { ...current };
        delete next[vaultId];
        return next;
      });
      setExpandedVaultIds((current) => {
        const next = new Set(current);
        next.delete(vaultId);
        return next;
      });
      await loadVaults();
    } catch (deleteError) {
      setVaultsError(errorMessage(deleteError));
    }
  }

  async function deleteVaultCredential(vaultId: string, credentialId: string) {
    if (!auth) return;

    setVaultsError(null);
    try {
      await apiFetch<{ credential: unknown }>(`/vaults/${encodeURIComponent(vaultId)}/credentials/${encodeURIComponent(credentialId)}`, auth, { method: "DELETE" });
      await loadVaultCredentials(vaultId);
    } catch (deleteError) {
      setVaultsError(errorMessage(deleteError));
    }
  }

  const currentWorkspaceRole = auth ? (members.find((member) => member.uuid === auth.uuid)?.role ?? auth.role ?? "member") : "member";

  React.useEffect(() => {
    if (settingsPageOpen && currentWorkspaceRole !== "admin") {
      setSettingsPageOpen(false);
    }
  }, [currentWorkspaceRole, settingsPageOpen]);

  if (!auth) {
    return <SignInView onAuth={handleAuth} />;
  }

  const canOpenWorkspace = currentWorkspaceRole === "admin";

  return (
    <main className={`app-shell ${settingsPageOpen || projectSettingsPageOpen ? "" : "projects-shell"}`} style={themedStyle}>
      <div className={settingsPageOpen ? "settings-window" : projectSettingsPageOpen ? "project-settings-window" : "project-window"}>
      {settingsPageOpen ? (
      <aside className="sidebar">
        <div className="brand">
          <div className="brand-title">Workspace</div>
          <button className="icon-button" type="button" onClick={() => setSettingsPageOpen(false)} title="Back to projects">
            <X size={16} aria-hidden="true" />
          </button>
        </div>

        <nav className="sidebar-nav" aria-label="Primary">
          <button className={activeTab === "chat" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("chat")}>
            <MessageSquare size={16} aria-hidden="true" />
            Sessions
          </button>
          <button className={activeTab === "agents" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("agents")}>
            <Bot size={16} aria-hidden="true" />
            Agents
          </button>
          <button className={activeTab === "mcpServers" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("mcpServers")}>
            <Server size={16} aria-hidden="true" />
            MCP Servers
          </button>
          <button className={activeTab === "integrations" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("integrations")}>
            <Puzzle size={16} aria-hidden="true" />
            Integrations
          </button>
          <button className={activeTab === "skills" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("skills")}>
            <Sparkles size={16} aria-hidden="true" />
            Skills
          </button>
          <button className={activeTab === "deployments" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("deployments")}>
            <Rocket size={16} aria-hidden="true" />
            Deployments
          </button>
          <button className={activeTab === "environments" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("environments")}>
            <MonitorCog size={16} aria-hidden="true" />
            Environments
          </button>
          <button className={activeTab === "secrets" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("secrets")}>
            <LockKeyhole size={16} aria-hidden="true" />
            Secrets
          </button>
          <button className={activeTab === "apiKeys" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("apiKeys")}>
            <KeyRound size={16} aria-hidden="true" />
            API Keys
          </button>
          <button className={activeTab === "members" ? "nav-item active" : "nav-item"} type="button" onClick={() => setActiveTab("members")}>
            <Users size={16} aria-hidden="true" />
            Members
          </button>
        </nav>
      </aside>
      ) : null}

      <section className="workspace">
        <div className="workspace-inner">
          {!settingsPageOpen ? (
            projectSettingsPageOpen && selectedProject ? (
              <ProjectSettingsView
                project={selectedProject}
                saving={projectSaving}
                error={projectsError}
                onSave={saveProject}
                onInviteCollaborator={(project, email, role) => inviteProjectCollaborator(project, email, role)}
                onRemoveCollaborator={(project, collaborator) => removeProjectCollaborator(project, collaborator)}
                onDelete={(project) => {
                  void deleteProject(project);
                  setProjectSettingsPageOpen(false);
                }}
                onClose={() => setProjectSettingsPageOpen(false)}
              />
            ) : (
            <ProjectsView
              projects={projects}
              selectedProjectId={selectedProjectId}
              agents={projectAgents}
              mcpServers={visibleMcpServers}
              skills={visibleSkills}
              apiKeys={projectApiKeys}
              emailReceivers={projectEmailReceivers}
              integrations={integrations}
              environments={environments}
              sessions={sessions}
              sessionsLoading={sessionsLoading}
              skillsLoading={skillsLoading}
              skillSaving={skillSaving}
              apiKeySaving={apiKeySaving}
              emailReceiverSaving={emailReceiverSaving}
              currentUserId={auth.uuid}
              loading={projectsLoading}
              error={projectsError}
              projectSaving={projectSaving}
              onSave={(project) => {
                void saveProject(project);
              }}
              onCreate={() => {
                if (projectIntroEnabled) {
                  setProjectIntroOpen(true);
                } else {
                  void createProject();
                }
              }}
              onSelectProject={setSelectedProjectId}
              onSubAgentEdgeChange={(agentId, subAgentId, enabled) => {
                void updateAgentSubAgent(agentId, subAgentId, enabled);
              }}
              onMcpEdgeChange={(agentId, mcpServerId, enabled) => {
                void updateAgentMcp(agentId, mcpServerId, enabled);
              }}
              onSkillEdgeChange={(agentId, skillId, enabled) => {
                void updateAgentSkill(agentId, skillId, enabled);
              }}
              onCreateScheduledDeployment={(project, graph, scheduleNode, agentNode) => createScheduledDeploymentFromCanvas(project, graph, scheduleNode, agentNode)}
              onDeleteScheduledDeployment={(deploymentId) => {
                void deleteDeployment(deploymentId);
              }}
              onRunPlay={(agentIds, prompt) => runCanvasPlay(agentIds, prompt)}
              onLoadSessionMessages={loadSessionMessages}
              onCreateAgent={(position) => {
                if (createOpen) {
                  setCreateOpen(false);
                  setCreateAgentPlacement(null);
                  return;
                }
                setCreateAgentPlacement(position ? { projectId: selectedProjectId, ...position } : null);
                setCreateOpen(true);
              }}
              onCreateMcpServer={() => setMcpServerCreateOpen(true)}
              onCreateSkill={createSkill}
              onCreateApiKey={createApiKey}
              onCreateEmailReceiver={createEmailReceiver}
              onRotateApiKey={(apiKey) => rotateApiKey(apiKey, false)}
              onOpenAgent={setSelectedAgent}
              onOpenMcpServer={setSelectedMcpServer}
              onOpenSkill={setSelectedSkill}
              onOpenSettings={() => {
                setProjectSettingsPageOpen(true);
              }}
              onOpenIntegrations={() => {
                setProjectIntegrationsOpen(true);
              }}
              onOpenWorkspace={() => {
                setSettingsPageOpen(true);
              }}
              onSignOut={() => setConfirmSignOutOpen(true)}
              canOpenWorkspace={canOpenWorkspace}
              createdAgentPlacement={createdCanvasAgentPlacement}
              onCreatedAgentPlacementConsumed={() => setCreatedCanvasAgentPlacement(null)}
            />
            )
          ) : activeTab === "agents" ? (
            <>
              <header className="toolbar">
                <div>
                  <h1>Agents</h1>
                  <p>{agents.length} registered</p>
                </div>
                <div className="toolbar-actions">
                  <button className="icon-button" type="button" onClick={loadAgents} disabled={loading} title="Refresh agents">
                    {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
                  </button>
                  <button
                    className="primary-button"
                    type="button"
                    onClick={() => {
                      setCreateAgentPlacement(null);
                      setCreateOpen(true);
                    }}
                  >
                    <Plus size={17} aria-hidden="true" />
                    New agent
                  </button>
                </div>
              </header>

              <div className="search-row">
                <Search size={17} aria-hidden="true" />
                <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search by name, model, description, or id" />
              </div>

              {error ? <div className="notice error">{error}</div> : null}

              <AgentList
                agents={filteredAgents}
                members={members}
                currentUserEmail={auth.email}
                loading={loading}
                onSelect={setSelectedAgent}
                onCreate={() => {
                  setCreateAgentPlacement(null);
                  setCreateOpen(true);
                }}
              />
            </>
          ) : activeTab === "mcpServers" ? (
            <McpServersView
              servers={mcpServers}
              loading={mcpServersLoading}
              error={mcpServersError}
              onRefresh={loadMcpServers}
              onOpenCreate={() => setMcpServerCreateOpen(true)}
              onSelect={setSelectedMcpServer}
            />
          ) : activeTab === "integrations" ? (
            <IntegrationsView
              integrations={integrations}
              loading={integrationsLoading}
              error={integrationsError}
              onRefresh={loadIntegrations}
              onOpenCreate={() => setIntegrationCreateOpen(true)}
              onSelect={setSelectedIntegration}
            />
          ) : activeTab === "skills" ? (
            <SkillsView
              skills={skills}
              loading={skillsLoading}
              saving={skillSaving}
              error={skillsError}
              onRefresh={loadSkills}
              onOpenCreate={() => setWorkspaceSkillCreateOpen(true)}
              onSelect={setSelectedSkill}
            />
          ) : activeTab === "deployments" ? (
            <DeploymentsView
              deployments={deployments}
              agents={agents}
              environments={environments}
              loading={deploymentsLoading}
              runningDeploymentId={runningDeploymentId}
              error={deploymentsError}
              onRefresh={loadDeployments}
              onOpenCreate={() => setDeploymentCreateOpen(true)}
              onSelect={setSelectedDeployment}
              onRun={(deployment) => {
                void runDeployment(deployment.id);
              }}
            />
          ) : activeTab === "chat" ? (
            <ChatView
              agents={agents}
              environments={environments}
              vaults={vaults}
              sessions={sessions}
              sessionsLoading={sessionsLoading}
              removingSessionId={removingSessionId}
              selectedSessionId={selectedSessionId}
              selectedAgentId={chatAgentId}
              selectedEnvironmentId={chatEnvironmentId}
              onAgentChange={(agentId) => {
                setChatAgentId(agentId);
                setChatError(null);
              }}
              onEnvironmentChange={(environmentId) => {
                setChatEnvironmentId(environmentId);
                setChatError(null);
              }}
              onSessionSelect={(session) => {
                void selectChatSession(session);
              }}
              onSessionRemove={(session) => {
                void removeChatSession(session);
              }}
              messages={chatMessagesByConversation[chatKey(chatAgentId, chatEnvironmentId, selectedProject ? [selectedProject.id] : [])] ?? []}
              input={chatInput}
              onInputChange={setChatInput}
              loading={chatLoading}
              approvalLoadingId={approvalLoadingId}
              error={chatError}
              onSubmit={sendChatMessage}
              onConfirmApproval={confirmApproval}
              onCreateAgent={() => {
                setActiveTab("agents");
                setCreateAgentPlacement(null);
                setCreateOpen(true);
              }}
            />
          ) : activeTab === "environments" ? (
            <EnvironmentsView
              environments={environments}
              loading={environmentLoading}
              error={environmentError}
              onRefresh={loadEnvironments}
              onOpenCreate={() => setEnvironmentCreateOpen(true)}
              onOpenEdit={setSelectedEnvironment}
            />
          ) : activeTab === "secrets" ? (
            <SecretsView
              vaults={vaults}
              loading={vaultsLoading}
              error={vaultsError}
              expandedVaultIds={expandedVaultIds}
              credentialsByVault={credentialsByVault}
              credentialsLoadingByVault={credentialsLoadingByVault}
              onRefresh={loadVaults}
              onOpenCreateSecret={setSecretCreateVault}
              onToggleVault={toggleVault}
              onDeleteVault={deleteVault}
              onDeleteCredential={deleteVaultCredential}
            />
          ) : activeTab === "apiKeys" ? (
            <ApiKeysView
              apiKeys={apiKeys}
              loading={apiKeysLoading}
              saving={apiKeySaving}
              error={apiKeysError}
              canEdit={selectedProjectCanEdit}
              onRefresh={loadApiKeys}
              onOpenCreate={() => setApiKeyCreateOpen(true)}
              onRotate={(apiKey) => {
                void rotateApiKey(apiKey);
              }}
              onDelete={(apiKey) => {
                void deleteApiKey(apiKey);
              }}
            />
          ) : (
            <MembersView
              members={members}
              auth={auth}
              loading={membersLoading}
              error={membersError}
              savingRoleUuid={memberRoleSavingUuid}
              onRefresh={loadMembers}
              onRoleChange={(member, role) => {
                void updateMemberRole(member, role);
              }}
            />
          )}
        </div>
      </section>
      </div>

      {createOpen && selectedProject ? (
        <CreateAgentDialog
          auth={auth}
          projectId={selectedProject.id}
          projects={projects}
          agents={projectAgents}
          registeredMcpServers={visibleMcpServers}
          projectCanEdit={selectedProjectCanEdit}
          workspaceRole={currentWorkspaceRole}
          onClose={() => {
            setCreateOpen(false);
            setCreateAgentPlacement(null);
          }}
          onCreated={handleCreated}
          side={!settingsPageOpen && !projectSettingsPageOpen}
        />
      ) : null}

      {projectIntroEnabled && projectIntroOpen ? (
        <CreateProjectIntroDialog
          saving={projectSaving}
          onClose={() => setProjectIntroOpen(false)}
          onGenerate={generateProjectPlan}
          onConfirm={async (plan) => {
            await confirmGeneratedProject(plan);
            setProjectIntroOpen(false);
          }}
          onSkip={async () => {
            await createProject();
            setProjectIntroOpen(false);
          }}
        />
      ) : null}

      {mcpServerCreateOpen ? (
        <CreateMcpServerDialog
          projects={projects}
          selectedProjectId={selectedProject?.id ?? null}
          saving={mcpServerSaving}
          onClose={() => setMcpServerCreateOpen(false)}
          side={!settingsPageOpen && !projectSettingsPageOpen}
          onSubmit={async (payload) => {
            await createMcpServer(payload);
            setMcpServerCreateOpen(false);
          }}
        />
      ) : null}

      {apiKeyCreateOpen ? (
        <CreateApiKeyDialog
          saving={apiKeySaving}
          onClose={() => setApiKeyCreateOpen(false)}
          onCreate={async (name) => {
            await createApiKey(name);
            setApiKeyCreateOpen(false);
          }}
        />
      ) : null}

      {selectedMcpServer ? (
        <CreateMcpServerDialog
          server={selectedMcpServer}
          projects={projects}
          selectedProjectId={selectedProject?.id ?? null}
          saving={mcpServerSaving}
          onClose={() => setSelectedMcpServer(null)}
          side={!settingsPageOpen && !projectSettingsPageOpen}
          onSubmit={async (payload) => {
            await updateMcpServer(selectedMcpServer.id, payload);
            setSelectedMcpServer(null);
          }}
          onDelete={async () => {
            await deleteMcpServer(selectedMcpServer.id);
          }}
        />
      ) : null}

      {integrationCreateOpen ? (
        <IntegrationTemplateDialog
          saving={integrationSaving}
          onClose={() => setIntegrationCreateOpen(false)}
          onSubmit={async (payload) => {
            await createIntegration(payload);
            setIntegrationCreateOpen(false);
          }}
        />
      ) : null}

      {selectedIntegration && settingsPageOpen ? (
        <IntegrationTemplateDialog
          integration={selectedIntegration}
          saving={integrationSaving}
          onClose={() => setSelectedIntegration(null)}
          onSubmit={async (payload) => {
            await updateIntegration(selectedIntegration.id, payload);
            setSelectedIntegration(null);
          }}
          onDelete={async () => {
            await deleteIntegration(selectedIntegration.id);
          }}
        />
      ) : null}

      {projectIntegrationsOpen && selectedProject ? (
        <ProjectIntegrationsDialog
          integrations={integrations}
          saving={integrationSaving}
          onClose={() => setProjectIntegrationsOpen(false)}
          onInstall={installIntegration}
        />
      ) : null}

      {selectedSkill ? (
        <SkillDetailsDialog
          skill={selectedSkill}
          saving={skillSaving}
          onClose={() => setSelectedSkill(null)}
          onSaveMetadata={(payload) => updateSkillMetadata(selectedSkill.id, payload)}
          onCreateVersion={(payload) => createSkillVersion(selectedSkill.id, payload)}
          projects={projects}
          selectedProjectId={selectedProject?.id ?? null}
        />
      ) : null}

      {workspaceSkillCreateOpen ? (
        <CreateSkillDialog
          projects={projects}
          selectedProjectId={selectedProject?.id ?? null}
          saving={skillSaving}
          onClose={() => setWorkspaceSkillCreateOpen(false)}
          onCreate={async (payload) => {
            await createSkill(payload);
            setWorkspaceSkillCreateOpen(false);
          }}
        />
      ) : null}

      {confirmSignOutOpen ? (
        <ConfirmDialog
          title="Sign out"
          message="Sign out of this workspace?"
          confirmLabel="Sign out"
          onCancel={() => setConfirmSignOutOpen(false)}
          onConfirm={() => {
            setConfirmSignOutOpen(false);
            signOut();
          }}
        />
      ) : null}

      {revealedApiKey ? <ApiKeyRevealToast revealedApiKey={revealedApiKey} onClose={() => setRevealedApiKey(null)} /> : null}

      {environmentCreateOpen ? (
        <EnvironmentDialog
          saving={environmentSaving}
          onClose={() => setEnvironmentCreateOpen(false)}
          onSubmit={async (payload) => {
            await createEnvironment(payload);
            setEnvironmentCreateOpen(false);
          }}
        />
      ) : null}

      {selectedEnvironment ? (
        <EnvironmentDialog
          environment={selectedEnvironment}
          saving={environmentSaving}
          onClose={() => setSelectedEnvironment(null)}
          onSubmit={async (payload) => {
            await updateEnvironment(selectedEnvironment.id, payload);
            setSelectedEnvironment(null);
          }}
        />
      ) : null}

      {deploymentCreateOpen ? (
        <CreateDeploymentDialog
          agents={agents}
          environments={environments}
          vaults={vaults}
          saving={deploymentSaving}
          onClose={() => setDeploymentCreateOpen(false)}
          onCreate={async (payload) => {
            await createDeployment(payload);
            setDeploymentCreateOpen(false);
          }}
        />
      ) : null}

      {secretCreateVault ? (
        <CreateSecretDialog
          vault={secretCreateVault}
          saving={vaultSaving}
          onClose={() => setSecretCreateVault(null)}
          onCreate={async (payload) => {
            await createVaultCredential(secretCreateVault.id, payload);
            setSecretCreateVault(null);
          }}
        />
      ) : null}

      {selectedAgent ? (
        <AgentDetailsDialog
          record={selectedAgent}
          auth={auth}
          agents={agents}
          members={members}
          registeredMcpServers={selectedAgentMcpServers}
          projectCanEdit={
            agentProjectIdsFromMetadata(selectedAgent.agent.metadata).some((projectId) =>
              canEditProject(projects.find((project) => project.id === projectId) ?? ({ current_user_role: "viewer" } as ProjectRecord)),
            )
          }
          projects={projects}
          selectedProjectId={selectedProject?.id ?? null}
          workspaceRole={currentWorkspaceRole}
          onClose={() => setSelectedAgent(null)}
          onChanged={handleChanged}
          side={!settingsPageOpen && !projectSettingsPageOpen}
        />
      ) : null}
      {selectedDeployment ? (
        <DeploymentDetailsDialog
          deployment={selectedDeployment}
          agents={agents}
          environments={environments}
          vaults={vaults}
          saving={deploymentSaving}
          onClose={() => setSelectedDeployment(null)}
          onUpdate={(payload) => updateDeployment(selectedDeployment.id, payload)}
          onDelete={() => deleteDeployment(selectedDeployment.id)}
        />
      ) : null}
      <RunningSessionsStatus sessions={runningSessions} onOpen={openRunningSession} />
    </main>
  );
}

function SignInView({ onAuth }: { onAuth: (auth: AuthSession) => void }) {
  const buttonRef = React.useRef<HTMLDivElement | null>(null);
  const [ready, setReady] = React.useState(false);
  const [error, setError] = React.useState<string | null>(null);

  React.useEffect(() => {
    if (!googleClientId) {
      setError("Google sign-in is not configured.");
      return;
    }

    const existing = document.querySelector<HTMLScriptElement>("script[data-google-identity]");
    const script = existing ?? document.createElement("script");

    script.src = "https://accounts.google.com/gsi/client";
    script.async = true;
    script.defer = true;
    script.dataset.googleIdentity = "true";
    script.onload = () => setReady(true);
    script.onerror = () => setError("Google sign-in is unavailable.");

    if (!existing) {
      document.head.appendChild(script);
    } else if (window.google) {
      setReady(true);
    }
  }, []);

  React.useEffect(() => {
    if (!ready || !window.google || !buttonRef.current) return;

    window.google.accounts.id.initialize({
      client_id: googleClientId,
      callback: (response) => {
        void handleGoogleCredential(response);
      },
    });

    buttonRef.current.innerHTML = "";
    window.google.accounts.id.renderButton(buttonRef.current, {
      theme: "outline",
      size: "large",
      shape: "rectangular",
      text: "signin_with",
      logo_alignment: "left",
      width: Math.floor(buttonRef.current.getBoundingClientRect().width),
    });
  }, [ready]);

  async function handleGoogleCredential(response: GoogleCredentialResponse) {
    if (!response.credential) {
      setError("Google did not return an ID token.");
      return;
    }

    setError(null);
    try {
      const auth = await publicApiFetch<AuthSession>("/auth/google", {
        method: "POST",
        body: JSON.stringify({ idToken: response.credential }),
      });
      onAuth(auth);
    } catch (authError) {
      setError(errorMessage(authError));
    }
  }

  return (
    <main className="signin-shell" style={themedStyle}>
      <section className="signin-panel">
        <img className="signin-logo" src="/fleeet-logo.png" alt="Fleeet" />
        <p className="signin-tagline">Create agent maps with an easy visual canvas.</p>
        <div className="google-slot" ref={buttonRef}>
          {!error ? <span>Loading Google sign-in</span> : null}
        </div>
        {error ? <div className="notice error">{error}</div> : null}
      </section>
    </main>
  );
}

function ProjectsView({
  projects,
  selectedProjectId,
  agents,
  mcpServers,
  skills,
  apiKeys,
  emailReceivers,
  integrations,
  environments,
  sessions,
  sessionsLoading,
  skillsLoading,
  skillSaving,
  apiKeySaving,
  emailReceiverSaving,
  currentUserId,
  loading,
  error,
  projectSaving,
  onSave,
  onCreate,
  onSelectProject,
  onSubAgentEdgeChange,
  onMcpEdgeChange,
  onSkillEdgeChange,
  onCreateScheduledDeployment,
  onDeleteScheduledDeployment,
  onRunPlay,
  onLoadSessionMessages,
  onCreateAgent,
  onCreateMcpServer,
  onCreateSkill,
  onCreateApiKey,
  onCreateEmailReceiver,
  onRotateApiKey,
  onOpenAgent,
  onOpenMcpServer,
  onOpenSkill,
  onOpenSettings,
  onOpenIntegrations,
  onOpenWorkspace,
  onSignOut,
  canOpenWorkspace,
  createdAgentPlacement,
  onCreatedAgentPlacementConsumed,
}: {
  projects: ProjectRecord[];
  selectedProjectId: string;
  agents: AgentRecord[];
  mcpServers: RegisteredMcpServer[];
  skills: SkillRecord[];
  apiKeys: ApiKeyRecord[];
  emailReceivers: EmailReceiverRecord[];
  integrations: IntegrationTemplate[];
  environments: AnthropicEnvironment[];
  sessions: ManagedSession[];
  sessionsLoading: boolean;
  skillsLoading: boolean;
  skillSaving: boolean;
  apiKeySaving: boolean;
  emailReceiverSaving: boolean;
  currentUserId: string;
  loading: boolean;
  error: string | null;
  projectSaving: boolean;
  onSave: (project: ProjectRecord) => void;
  onCreate: () => void;
  onSelectProject: (projectId: string) => void;
  onSubAgentEdgeChange: (agentId: string, subAgentId: string, enabled: boolean) => void;
  onMcpEdgeChange: (agentId: string, mcpServerId: string, enabled: boolean) => void;
  onSkillEdgeChange: (agentId: string, skillId: string, enabled: boolean) => void;
  onCreateScheduledDeployment: (project: ProjectRecord, graph: ProjectGraph, scheduleNode: ProjectNode, agentNode: ProjectNode) => Promise<string | null>;
  onDeleteScheduledDeployment: (deploymentId: string) => void;
  onRunPlay: (agentIds: string[], prompt: string) => Promise<string[]>;
  onLoadSessionMessages: (sessionId: string) => Promise<ChatMessage[]>;
  onCreateAgent: (position?: { x: number; y: number }) => void;
  onCreateMcpServer: () => void;
  onCreateSkill: (payload: { name: string; description: string; files: File[]; publicUrl: string; projectIds: string[] }) => Promise<void>;
  onCreateApiKey: (name: string) => Promise<{ apiKey: ApiKeyRecord; key: string } | null>;
  onCreateEmailReceiver: (name: string) => Promise<{ emailReceiver: EmailReceiverRecord } | null>;
  onRotateApiKey: (apiKey: ApiKeyRecord) => Promise<{ apiKey: ApiKeyRecord; key: string } | null>;
  onOpenAgent: (record: AgentRecord) => void;
  onOpenMcpServer: (server: RegisteredMcpServer) => void;
  onOpenSkill: (skill: SkillRecord) => void;
  onOpenSettings: () => void;
  onOpenIntegrations: () => void;
  onOpenWorkspace: () => void;
  onSignOut: () => void;
  canOpenWorkspace: boolean;
  createdAgentPlacement: { projectId: string; agentId: string; x: number; y: number; nonce: number } | null;
  onCreatedAgentPlacementConsumed: () => void;
}) {
  const selectedProject = projects.find((project) => project.id === selectedProjectId) ?? projects[0] ?? null;
  const [draft, setDraft] = React.useState<ProjectRecord | null>(selectedProject);
  const [draggingNodeId, setDraggingNodeId] = React.useState<string | null>(null);
  const [connectingFromId, setConnectingFromId] = React.useState<string | null>(null);
  const [lastPaletteTab, setLastPaletteTab] = React.useState<PaletteTab>("triggers");
  const [palette, setPalette] = React.useState<{ x: number; y: number; tab: PaletteTab } | null>(null);
  const [camera, setCamera] = React.useState({ x: 0, y: 0, zoom: 1 });
  const [playPanelNodeId, setPlayPanelNodeId] = React.useState<string | null>(null);
  const [selectedPlaySessionId, setSelectedPlaySessionId] = React.useState("");
  const [playSessionMessages, setPlaySessionMessages] = React.useState<ChatMessage[]>([]);
  const [playSessionLoading, setPlaySessionLoading] = React.useState(false);
  const [playSessionError, setPlaySessionError] = React.useState<string | null>(null);
  const [runningPlayNodeId, setRunningPlayNodeId] = React.useState<string | null>(null);
  const [apiKeyCreateNodeId, setApiKeyCreateNodeId] = React.useState<string | null>(null);
  const [emailReceiverCreateNodeId, setEmailReceiverCreateNodeId] = React.useState<string | null>(null);
  const [apiInfoNodeId, setApiInfoNodeId] = React.useState<string | null>(null);
  const [skillCreateOpen, setSkillCreateOpen] = React.useState(false);
  const [canvasHelpOpen, setCanvasHelpOpen] = React.useState(false);
  const canvasRef = React.useRef<HTMLDivElement | null>(null);
  const connectionPreviewPathRef = React.useRef<SVGPathElement | null>(null);
  const cameraRef = React.useRef(camera);
  const cameraFrameRef = React.useRef<number | null>(null);
  const suppressNodeClickRef = React.useRef(false);
  const lastSavedProjectShapeRef = React.useRef<string>("");
  const draftProjectIdRef = React.useRef<string | null>(selectedProject?.id ?? null);

  React.useEffect(() => {
    cameraRef.current = camera;
  }, [camera]);

  React.useEffect(() => {
    const nextProjectId = selectedProject?.id ?? null;
    if (draftProjectIdRef.current !== nextProjectId) {
      draftProjectIdRef.current = nextProjectId;
      setDraft(selectedProject ? cloneProject(selectedProject) : null);
    }
    lastSavedProjectShapeRef.current = selectedProject ? JSON.stringify(projectEditableShape(selectedProject)) : "";
  }, [selectedProject]);

  const graph = draft?.graph ?? createDefaultProjectGraph();
  const canEditCurrentProject = draft ? canEditProject(draft) : false;

  React.useEffect(() => {
    if (!draft || !canEditCurrentProject) return;
    const shape = JSON.stringify(projectEditableShape(draft));
    if (!lastSavedProjectShapeRef.current || shape === lastSavedProjectShapeRef.current) return;
    const timeout = window.setTimeout(() => {
      lastSavedProjectShapeRef.current = shape;
      onSave(draft);
    }, 500);
    return () => window.clearTimeout(timeout);
  }, [canEditCurrentProject, draft, onSave]);

  const ownedApiKeys = React.useMemo(() => apiKeys.filter((apiKey) => apiKey.creator_uuid === currentUserId), [apiKeys, currentUserId]);

  function updateDraft(updater: (current: ProjectRecord) => ProjectRecord) {
    if (!canEditCurrentProject) return;
    setDraft((current) => (current ? updater(current) : current));
  }

  React.useEffect(() => {
    if (!createdAgentPlacement) return;
    const agentExists = agents.some((record) => record.id === createdAgentPlacement.agentId);
    if (!agentExists) return;

    setDraft((current) => {
      if (!canEditCurrentProject) return current;
      if (!current || current.id !== createdAgentPlacement.projectId) return current;
      if (current.graph.nodes.some((node) => node.type === "agent" && node.agent_id === createdAgentPlacement.agentId)) return current;

      const nodeId = crypto.randomUUID();
      return {
        ...current,
        graph: syncProjectGraphAgentDependencies(
          {
            ...current.graph,
            nodes: [
              ...current.graph.nodes,
              {
                id: nodeId,
                type: "agent",
                agent_id: createdAgentPlacement.agentId,
                x: createdAgentPlacement.x,
                y: createdAgentPlacement.y,
              },
            ],
          },
          agents,
          mcpServers,
        ),
      };
    });
    onCreatedAgentPlacementConsumed();
  }, [agents, canEditCurrentProject, createdAgentPlacement, mcpServers, onCreatedAgentPlacementConsumed]);

  function screenToWorld(clientX: number, clientY: number, viewCamera = cameraRef.current): { x: number; y: number } {
    const canvas = canvasRef.current;
    if (!canvas) return { x: clientX, y: clientY };
    const rect = canvas.getBoundingClientRect();
    return {
      x: (clientX - rect.left - viewCamera.x) / viewCamera.zoom,
      y: (clientY - rect.top - viewCamera.y) / viewCamera.zoom,
    };
  }

  function canvasCenterToWorld(): { x: number; y: number } | undefined {
    const canvas = canvasRef.current;
    if (!canvas) return undefined;
    const rect = canvas.getBoundingClientRect();
    return screenToWorld(rect.left + rect.width / 2, rect.top + rect.height / 2);
  }

  function scheduleCamera(nextCamera: { x: number; y: number; zoom: number }) {
    cameraRef.current = nextCamera;
    if (cameraFrameRef.current !== null) return;
    cameraFrameRef.current = window.requestAnimationFrame(() => {
      cameraFrameRef.current = null;
      setCamera(cameraRef.current);
    });
  }

  function moveNode(nodeId: string, x: number, y: number) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: current.graph.nodes.map((node) => (node.id === nodeId ? { ...node, x, y } : node)),
      },
    }));
  }

  function addAgentAt(agentId: string, x: number, y: number) {
    if (!agentId) return;
    const nodeId = crypto.randomUUID();
    updateDraft((current) => ({
      ...current,
      graph: syncProjectGraphAgentDependencies(
        {
          ...current.graph,
          nodes: [...current.graph.nodes, { id: nodeId, type: "agent", agent_id: agentId, x, y }],
        },
        agents,
        mcpServers,
      ),
    }));
    setPalette(null);
  }

  function addMcpAt(mcpServerId: string, x: number, y: number) {
    if (!mcpServerId) return;
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: [...current.graph.nodes, { id: crypto.randomUUID(), type: "mcp", mcp_server_id: mcpServerId, x, y }],
      },
    }));
    setPalette(null);
  }

  function addSkillAt(skillId: string, x: number, y: number) {
    if (!skillId) return;
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: [...current.graph.nodes, { id: crypto.randomUUID(), type: "skill", skill_id: skillId, x, y }],
      },
    }));
    setPalette(null);
  }

  function addPlayAt(x: number, y: number) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: [...current.graph.nodes, { id: crypto.randomUUID(), type: "play", x, y }],
      },
    }));
    setPalette(null);
  }

  function addScheduleAt(x: number, y: number) {
    const nodeId = crypto.randomUUID();
    updateDraft((current) => ({
      ...current,
      graph: {
        nodes: [
          ...current.graph.nodes,
          {
            id: nodeId,
            type: "schedule",
            x,
            y,
            schedule: createDefaultScheduleDraft(),
          },
        ],
        edges: current.graph.edges,
      },
    }));
    setPalette(null);
  }

  function addSlackAt(x: number, y: number) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: [
          ...current.graph.nodes,
          {
            id: crypto.randomUUID(),
            type: "slack",
            x,
            y,
            slack_trigger: createDefaultSlackTriggerDraft(),
          },
        ],
      },
    }));
    setPalette(null);
  }

  function addApiAt(x: number, y: number) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: [
          ...current.graph.nodes,
          {
            id: crypto.randomUUID(),
            type: "api",
            x,
            y,
            api_trigger: createDefaultApiTriggerDraft(ownedApiKeys),
          },
        ],
      },
    }));
    setPalette(null);
  }

  function addEmailAt(x: number, y: number) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: [
          ...current.graph.nodes,
          {
            id: crypto.randomUUID(),
            type: "email",
            x,
            y,
            email_trigger: createDefaultEmailTriggerDraft(emailReceivers),
          },
        ],
      },
    }));
    setPalette(null);
  }

  function openAgentCreate() {
    if (!canEditCurrentProject) return;
    const position = palette ? { x: palette.x, y: palette.y } : undefined;
    setPalette(null);
    onCreateAgent(position);
  }

  function openAgentCreateAtCanvasCenter() {
    if (!canEditCurrentProject) return;
    setPalette(null);
    onCreateAgent(canvasCenterToWorld());
  }

  function openMcpServerCreate() {
    if (!canEditCurrentProject) return;
    setPalette(null);
    onCreateMcpServer();
  }

  function openSkillCreate() {
    if (!canEditCurrentProject) return;
    setPalette(null);
    setSkillCreateOpen(true);
  }

  function openAgentDetails(record: AgentRecord) {
    setPalette(null);
    onOpenAgent(record);
  }

  function openMcpServerDetails(server: RegisteredMcpServer) {
    setPalette(null);
    onOpenMcpServer(server);
  }

  function openSettingsPage() {
    setPalette(null);
    onOpenSettings();
  }

  function removeNode(nodeId: string) {
    graph.edges.filter((edge) => edge.source === nodeId || edge.target === nodeId).forEach((edge) => handleEdgeRemoved(edge));
    updateDraft((current) => ({
      ...current,
      graph: {
        nodes: current.graph.nodes.filter((node) => node.id !== nodeId),
        edges: current.graph.edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId),
      },
    }));
  }

  function removeEdge(edgeId: string) {
    const edge = graph.edges.find((item) => item.id === edgeId);
    if (edge) handleEdgeRemoved(edge);
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        edges: current.graph.edges.filter((edge) => edge.id !== edgeId),
      },
    }));
  }

  function updateSchedule(nodeId: string, schedule: ScheduleDraft) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: current.graph.nodes.map((node) => (node.id === nodeId ? { ...node, schedule } : node)),
      },
    }));
  }

  function updateSlackTrigger(nodeId: string, slackTrigger: SlackTriggerDraft) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: current.graph.nodes.map((node) => (node.id === nodeId ? { ...node, slack_trigger: slackTrigger } : node)),
      },
    }));
  }

  function updateApiTrigger(nodeId: string, apiTrigger: ApiTriggerDraft) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: current.graph.nodes.map((node) => (node.id === nodeId ? { ...node, api_trigger: apiTrigger } : node)),
      },
    }));
  }

  function updateEmailTrigger(nodeId: string, emailTrigger: EmailTriggerDraft) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: current.graph.nodes.map((node) => (node.id === nodeId ? { ...node, email_trigger: emailTrigger } : node)),
      },
    }));
  }

  function updateNodeParameterValues(nodeId: string, parameterValues: Record<string, string>) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: current.graph.nodes.map((node) => (node.id === nodeId ? { ...node, parameter_values: parameterValues } : node)),
      },
    }));
  }

  async function rotateApiKeyForNode(node: ProjectNode) {
    const apiKey = ownedApiKeys.find((key) => key.id === node.api_trigger?.api_key_id);
    if (!apiKey) return;
    await onRotateApiKey(apiKey);
  }

  async function createApiKeyForNode(name: string) {
    if (!apiKeyCreateNodeId) return;
    const response = await onCreateApiKey(name);
    if (response) {
      updateApiTrigger(apiKeyCreateNodeId, { api_key_id: response.apiKey.id });
    }
    setApiKeyCreateNodeId(null);
  }

  async function createEmailReceiverForNode(name: string) {
    if (!emailReceiverCreateNodeId) return;
    const response = await onCreateEmailReceiver(name);
    if (response) {
      updateEmailTrigger(emailReceiverCreateNodeId, { receiver_id: response.emailReceiver.id });
    }
    setEmailReceiverCreateNodeId(null);
  }

  function updatePlayPrompt(nodeId: string, prompt: string) {
    updateDraft((current) => ({
      ...current,
      graph: {
        ...current.graph,
        nodes: current.graph.nodes.map((node) => (node.id === nodeId ? { ...node, prompt } : node)),
      },
    }));
  }

  function connectedAgentIdsForTrigger(nodeId: string, sourceGraph = graph): string[] {
    return sourceGraph.edges
      .filter((edge) => edge.source === nodeId && (edge.type === "runs" || edge.type === "schedules" || edge.type === "slack_triggers" || edge.type === "api_triggers" || edge.type === "email_triggers"))
      .map((edge) => sourceGraph.nodes.find((node) => node.id === edge.target && node.type === "agent")?.agent_id)
      .filter((agentId): agentId is string => Boolean(agentId));
  }

  function sessionsForTrigger(nodeId: string): ManagedSession[] {
    const agentIds = new Set(connectedAgentIdsForTrigger(nodeId));
    return sessions.filter((session) => agentIds.has(session.agent.id));
  }

  async function runPlay(node: ProjectNode) {
    if (!canEditCurrentProject) return;
    const agentIds = connectedAgentIdsForTrigger(node.id);
    if (agentIds.length === 0) {
      setPlaySessionError("Connect at least one agent to this play card.");
      setPlayPanelNodeId(node.id);
      return;
    }
    setRunningPlayNodeId(node.id);
    setPlaySessionError(null);
    const sessionIds = await onRunPlay(agentIds, node.prompt ?? "");
    setRunningPlayNodeId(null);
    setPlayPanelNodeId(node.id);
    if (sessionIds[0]) {
      await selectPlaySession(sessionIds[0]);
    }
  }

  async function selectPlaySession(sessionId: string) {
    if (!sessionId) {
      setSelectedPlaySessionId("");
      setPlaySessionMessages([]);
      return;
    }
    setSelectedPlaySessionId(sessionId);
    setPlaySessionLoading(true);
    setPlaySessionError(null);
    try {
      setPlaySessionMessages(await onLoadSessionMessages(sessionId));
    } catch (loadError) {
      setPlaySessionError(errorMessage(loadError));
    } finally {
      setPlaySessionLoading(false);
    }
  }

  React.useEffect(() => {
    setDraft((current) => {
      if (!canEditCurrentProject) return current;
      if (!current) return current;
      const syncedGraph = syncProjectGraphAgentDependencies(current.graph, agents, mcpServers);
      if (JSON.stringify(syncedGraph) === JSON.stringify(current.graph)) return current;
      return { ...current, graph: syncedGraph };
    });
  }, [agents, canEditCurrentProject, mcpServers]);

  React.useEffect(() => {
    if (!draft || !canEditCurrentProject || canvasHelpOpen || apiInfoNodeId || apiKeyCreateNodeId || emailReceiverCreateNodeId) return;

    function onKeyDown(event: KeyboardEvent) {
      const target = event.target;
      if (
        target instanceof HTMLElement &&
        target.closest("input, textarea, select, button, [contenteditable], [role='dialog']")
      ) {
        return;
      }
      if (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey && event.key.toLowerCase() === "a") {
        event.preventDefault();
        openAgentCreateAtCanvasCenter();
      }
    }

    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [apiInfoNodeId, apiKeyCreateNodeId, canEditCurrentProject, canvasHelpOpen, draft, emailReceiverCreateNodeId]);

  function pointerDown(event: React.PointerEvent, nodeId: string) {
    if (!canEditCurrentProject) return;
    if ((event.target as HTMLElement).closest("button, input, select, .project-connector")) return;
    const node = graph.nodes.find((item) => item.id === nodeId);
    if (!node) return;
    const startWorld = screenToWorld(event.clientX, event.clientY);
    const offsetX = startWorld.x - node.x;
    const offsetY = startWorld.y - node.y;
    const startX = event.clientX;
    const startY = event.clientY;
    suppressNodeClickRef.current = false;
    setDraggingNodeId(nodeId);
    event.currentTarget.setPointerCapture(event.pointerId);

    function onMove(moveEvent: PointerEvent) {
      if (Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) > 4) {
        suppressNodeClickRef.current = true;
      }
      const nextWorld = screenToWorld(moveEvent.clientX, moveEvent.clientY);
      moveNode(nodeId, nextWorld.x - offsetX, nextWorld.y - offsetY);
    }

    function onUp() {
      setDraggingNodeId(null);
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
    }

    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
  }

  function suppressesNodeClick(): boolean {
    if (!suppressNodeClickRef.current) return false;
    window.setTimeout(() => {
      suppressNodeClickRef.current = false;
    }, 0);
    return true;
  }

  function beginConnection(event: React.PointerEvent, nodeId: string) {
    if (!canEditCurrentProject) return;
    event.preventDefault();
    event.stopPropagation();
    setPalette(null);
    setConnectingFromId(nodeId);
    updateConnectionPreview(event.clientX, event.clientY);

    function updateConnectionPreview(clientX: number, clientY: number) {
      const source = graph.nodes.find((node) => node.id === nodeId);
      const path = connectionPreviewPathRef.current;
      if (!source || !path) return;
      path.setAttribute("d", connectionPreviewPath(source, screenToWorld(clientX, clientY)));
      path.style.display = "block";
    }

    function onMove(moveEvent: PointerEvent) {
      updateConnectionPreview(moveEvent.clientX, moveEvent.clientY);
    }

    function onUp(upEvent: PointerEvent) {
      const targetElement = document.elementFromPoint(upEvent.clientX, upEvent.clientY);
      const targetNodeId = targetElement instanceof HTMLElement ? targetElement.closest<HTMLElement>("[data-project-node-id]")?.dataset.projectNodeId : undefined;
      if (targetNodeId && targetNodeId !== nodeId) connectNodes(nodeId, targetNodeId);
      setConnectingFromId(null);
      if (connectionPreviewPathRef.current) {
        connectionPreviewPathRef.current.style.display = "none";
        connectionPreviewPathRef.current.setAttribute("d", "");
      }
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
    }

    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
  }

  function connectNodes(sourceId: string, targetId: string) {
    if (!canEditCurrentProject) return;
    const source = graph.nodes.find((node) => node.id === sourceId);
    const target = graph.nodes.find((node) => node.id === targetId);
    if (!source || !target) return;
    const type = projectEdgeTypeFor(source, target);
    if (!type) return;
    if ((type === "sub_agent" && isGlobalAgentNode(source, agents)) || ((type === "uses_mcp" || type === "uses_skill") && isGlobalAgentNode(target, agents))) {
      return;
    }
    const exists = graph.edges.some((edge) => edge.source === sourceId && edge.target === targetId && edge.type === type);
    if (exists) return;
    const newEdge: ProjectEdge = { id: crypto.randomUUID(), source: sourceId, target: targetId, type };
    const nextGraph = {
      ...graph,
      edges: [...graph.edges, newEdge],
    };
    updateDraft((current) => {
      return {
        ...current,
        graph: nextGraph,
      };
    });
    void handleEdgeAdded(newEdge, nextGraph);
  }

  async function handleEdgeAdded(edge: ProjectEdge, nextGraph: ProjectGraph) {
    const source = nextGraph.nodes.find((node) => node.id === edge.source);
    const target = nextGraph.nodes.find((node) => node.id === edge.target);
    if (!source || !target || !draft) return;

    if (edge.type === "sub_agent" && source.agent_id && target.agent_id) {
      onSubAgentEdgeChange(source.agent_id, target.agent_id, true);
      return;
    }

    if (edge.type === "uses_mcp" && source.mcp_server_id && target.agent_id) {
      onMcpEdgeChange(target.agent_id, source.mcp_server_id, true);
      return;
    }

    if (edge.type === "uses_skill" && source.skill_id && target.agent_id) {
      onSkillEdgeChange(target.agent_id, source.skill_id, true);
      return;
    }

    if (edge.type === "schedules" && source.type === "schedule" && target.type === "agent") {
      const deploymentId = await onCreateScheduledDeployment(draft, nextGraph, source, target);
      if (deploymentId) attachDeploymentToEdge(edge.id, deploymentId);
      return;
    }
  }

  function handleEdgeRemoved(edge: ProjectEdge) {
    const source = graph.nodes.find((node) => node.id === edge.source);
    const target = graph.nodes.find((node) => node.id === edge.target);
    if (!source || !target) return;

    if (edge.type === "sub_agent" && source.agent_id && target.agent_id) {
      onSubAgentEdgeChange(source.agent_id, target.agent_id, false);
      return;
    }

    if (edge.type === "uses_mcp" && source.mcp_server_id && target.agent_id) {
      onMcpEdgeChange(target.agent_id, source.mcp_server_id, false);
      return;
    }

    if (edge.type === "uses_skill" && source.skill_id && target.agent_id) {
      onSkillEdgeChange(target.agent_id, source.skill_id, false);
      return;
    }

    if (edge.type === "schedules" && edge.deployment_id) {
      onDeleteScheduledDeployment(edge.deployment_id);
      return;
    }

    if (edge.type === "runs") {
      const scheduleEdges = graph.edges.filter((item) => item.target === edge.source && item.type === "schedules" && item.deployment_id);
      scheduleEdges.forEach((scheduleEdge) => onDeleteScheduledDeployment(scheduleEdge.deployment_id as string));
      const scheduleEdgeIds = new Set(scheduleEdges.map((item) => item.id));
      if (scheduleEdgeIds.size > 0) {
        updateDraft((current) =>
          current
            ? {
                ...current,
                graph: {
                  ...current.graph,
                  edges: current.graph.edges.map((item) => (scheduleEdgeIds.has(item.id) ? { ...item, deployment_id: undefined } : item)),
                },
              }
            : current,
        );
      }
    }
  }

  function attachDeploymentToEdge(edgeId: string, deploymentId: string) {
    updateDraft((current) =>
      current
        ? {
            ...current,
            graph: {
              ...current.graph,
              edges: current.graph.edges.map((edge) => (edge.id === edgeId ? { ...edge, deployment_id: deploymentId } : edge)),
            },
          }
        : current,
    );
  }

  function handleCanvasPointerDown(event: React.PointerEvent<HTMLDivElement>) {
    if ((event.target as HTMLElement).closest(".project-node, .project-controls-overlay, .project-workspace-overlay, .project-card-palette")) return;
    if (canEditCurrentProject && event.metaKey) {
      const point = screenToWorld(event.clientX, event.clientY);
      setPalette({
        x: point.x,
        y: point.y,
        tab: lastPaletteTab,
      });
      return;
    }

    setPalette(null);
    const startX = event.clientX;
    const startY = event.clientY;
    const startCamera = cameraRef.current;

    function onMove(moveEvent: PointerEvent) {
      scheduleCamera({
        ...startCamera,
        x: startCamera.x + moveEvent.clientX - startX,
        y: startCamera.y + moveEvent.clientY - startY,
      });
    }

    function onUp() {
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
    }

    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
  }

  function handleCanvasWheel(event: React.WheelEvent<HTMLDivElement>) {
    event.preventDefault();
    if (event.metaKey || event.ctrlKey) {
      const zoomFactor = Math.exp(-event.deltaY * 0.001);
      const current = cameraRef.current;
      const nextZoom = Math.min(2.2, Math.max(0.35, current.zoom * zoomFactor));
      const worldPoint = screenToWorld(event.clientX, event.clientY, current);
      const canvas = canvasRef.current;
      if (!canvas) return;
      const rect = canvas.getBoundingClientRect();
      scheduleCamera({
        x: event.clientX - rect.left - worldPoint.x * nextZoom,
        y: event.clientY - rect.top - worldPoint.y * nextZoom,
        zoom: nextZoom,
      });
      return;
    }

    const current = cameraRef.current;
    scheduleCamera({
      ...current,
      x: current.x - event.deltaX,
      y: current.y - event.deltaY,
    });
  }

  return (
    <section className="projects-view">
      {loading && projects.length === 0 ? (
        <div className="empty-state">
          <Loader2 className="spin" size={24} aria-hidden="true" />
          <span>Loading projects</span>
        </div>
      ) : !draft ? (
        <div className="empty-state">
          <Play size={28} aria-hidden="true" />
          <strong>No projects found</strong>
          <span>Create a project to compose agents visually.</span>
          <button className="primary-button" type="button" onClick={onCreate}>
            <Plus size={16} aria-hidden="true" />
            New project
          </button>
        </div>
      ) : (
        <>
          <div
            className={`project-canvas ${connectingFromId ? "connecting" : ""}`}
            ref={canvasRef}
            onPointerDown={handleCanvasPointerDown}
            onWheel={handleCanvasWheel}
          >
            <div className="project-controls-overlay">
              <select
                className="project-select"
                value={draft.id}
                onChange={(event) => onSelectProject(event.target.value)}
                onPointerDown={(event) => event.stopPropagation()}
                aria-label="Project"
              >
                {projects.map((project) => (
                  <option value={project.id} key={project.id}>
                    {project.name}
                  </option>
                ))}
              </select>
              <button className="icon-button" type="button" onClick={onCreate} disabled={projectSaving} title="Create project" aria-label="Create project">
                {projectSaving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
              </button>
              <button className="icon-button" type="button" onClick={openSettingsPage} title="Project settings" aria-label="Project settings">
                <Settings size={16} aria-hidden="true" />
              </button>
              <button className="icon-button" type="button" onClick={onOpenIntegrations} title="Add integration" aria-label="Add integration">
                <Puzzle size={16} aria-hidden="true" />
              </button>
            </div>
            <div className="project-workspace-overlay">
              <button className="icon-button project-workspace-button" type="button" onClick={() => setCanvasHelpOpen(true)} title="Canvas controls" aria-label="Canvas controls">
                <Info size={18} aria-hidden="true" />
              </button>
              {canOpenWorkspace ? (
                <button className="icon-button project-workspace-button" type="button" onClick={onOpenWorkspace} title="Open workspace" aria-label="Open workspace">
                  <Shield size={18} aria-hidden="true" />
                </button>
              ) : null}
              <button className="icon-button project-workspace-button" type="button" onClick={onSignOut} title="Sign out" aria-label="Sign out">
                <LogOut size={18} aria-hidden="true" />
              </button>
            </div>

            {error ? <div className="project-error-overlay notice error">{error}</div> : null}

            <div className="project-world" style={{ transform: `translate(${camera.x}px, ${camera.y}px) scale(${camera.zoom})` }}>
              <svg className="project-edges" aria-hidden="true">
                {graph.edges.map((edge) => {
                  const source = graph.nodes.find((node) => node.id === edge.source);
                  const target = graph.nodes.find((node) => node.id === edge.target);
                  if (!source || !target) return null;
                  const path = edgePath(source, target);
                  return (
                    <g key={edge.id}>
                      <path
                        className="project-edge-hit"
                        d={path}
                        onPointerDown={(event) => event.stopPropagation()}
                        onClick={(event) => {
                          event.stopPropagation();
                          if (!canEditCurrentProject) return;
                          removeEdge(edge.id);
                        }}
                      />
                      <path className={`project-edge ${edge.type}`} d={path} />
                    </g>
                  );
                })}
                <path className="project-edge-preview" ref={connectionPreviewPathRef} style={{ display: "none" }} />
              </svg>

              {graph.nodes.map((node) => {
                const connectingFrom = connectingFromId ? graph.nodes.find((item) => item.id === connectingFromId) : undefined;
                const connectionState =
                  connectingFrom && connectingFrom.id === node.id
                    ? "source"
                    : connectingFrom
                      ? projectEdgeTypeFor(connectingFrom, node)
                        ? "valid"
                        : "invalid"
                      : "idle";
                return (
                  <ProjectNodeCard
                    key={node.id}
                    node={node}
                    agents={agents}
                    mcpServers={mcpServers}
                    skills={skills}
                    apiKeys={ownedApiKeys}
                    emailReceivers={emailReceivers}
                    environments={environments}
                    dragging={draggingNodeId === node.id}
                    connectionState={connectionState}
                    onPointerDown={(event) => pointerDown(event, node.id)}
                    onConnectorPointerDown={(event) => beginConnection(event, node.id)}
                    onRemove={() => removeNode(node.id)}
                    onScheduleChange={(schedule) => updateSchedule(node.id, schedule)}
                    onSlackTriggerChange={(slackTrigger) => updateSlackTrigger(node.id, slackTrigger)}
                    onApiTriggerChange={(apiTrigger) => updateApiTrigger(node.id, apiTrigger)}
                    onEmailTriggerChange={(emailTrigger) => updateEmailTrigger(node.id, emailTrigger)}
                    onParameterValuesChange={(parameterValues) => updateNodeParameterValues(node.id, parameterValues)}
                    onRotateApiKey={() => {
                      void rotateApiKeyForNode(node);
                    }}
                    onCreateApiKey={() => setApiKeyCreateNodeId(node.id)}
                    onCreateEmailReceiver={() => setEmailReceiverCreateNodeId(node.id)}
                    onOpenApiInfo={() => setApiInfoNodeId(node.id)}
                    onPlayPromptChange={(prompt) => updatePlayPrompt(node.id, prompt)}
                    onRunPlay={() => void runPlay(node)}
                    onOpenPlay={() => {
                      setPalette(null);
                      setPlayPanelNodeId(node.id);
                      const availableSessions = sessionsForTrigger(node.id);
                      if (availableSessions[0]) void selectPlaySession(availableSessions[0].id);
                    }}
                    onOpenAgent={openAgentDetails}
                    onOpenMcpServer={openMcpServerDetails}
                    onOpenSkill={onOpenSkill}
                    runningPlay={runningPlayNodeId === node.id}
                    readOnly={!canEditCurrentProject}
                    shouldSuppressClick={suppressesNodeClick}
                  />
                );
              })}

              {palette && canEditCurrentProject ? (
                <ProjectCardPalette
                  palette={palette}
                  agents={agents}
                  mcpServers={mcpServers}
                  skills={skills}
                  skillsLoading={skillsLoading}
                  onTabChange={(tab) => {
                    setLastPaletteTab(tab);
                    setPalette((current) => (current ? { ...current, tab } : current));
                  }}
                  onAddPlay={() => addPlayAt(palette.x, palette.y)}
                  onAddSchedule={() => addScheduleAt(palette.x, palette.y)}
                  onAddSlack={() => addSlackAt(palette.x, palette.y)}
                  onAddApi={() => addApiAt(palette.x, palette.y)}
                  onAddEmail={() => addEmailAt(palette.x, palette.y)}
                  onAddAgent={(agentId) => addAgentAt(agentId, palette.x, palette.y)}
                  onAddMcp={(mcpServerId) => addMcpAt(mcpServerId, palette.x, palette.y)}
                  onAddSkill={(skillId) => addSkillAt(skillId, palette.x, palette.y)}
                  onCreateAgent={openAgentCreate}
                  onCreateMcpServer={openMcpServerCreate}
                  onCreateSkill={openSkillCreate}
                  onClose={() => setPalette(null)}
                />
              ) : null}
            </div>
          </div>

          {playPanelNodeId ? (
            <PlaySessionsPanel
              sessions={sessionsForTrigger(playPanelNodeId)}
              sessionsLoading={sessionsLoading}
              selectedSessionId={selectedPlaySessionId}
              messages={playSessionMessages}
              loading={playSessionLoading}
              error={playSessionError}
              onSelect={(sessionId) => void selectPlaySession(sessionId)}
              onClose={() => {
                setPlayPanelNodeId(null);
                setSelectedPlaySessionId("");
                setPlaySessionMessages([]);
                setPlaySessionError(null);
              }}
            />
          ) : null}

          {apiKeyCreateNodeId ? (
            <CreateApiKeyDialog
              saving={apiKeySaving}
              side
              onClose={() => setApiKeyCreateNodeId(null)}
              onCreate={createApiKeyForNode}
            />
          ) : null}

          {emailReceiverCreateNodeId ? (
            <CreateEmailReceiverDialog
              saving={emailReceiverSaving}
              side
              onClose={() => setEmailReceiverCreateNodeId(null)}
              onCreate={createEmailReceiverForNode}
            />
          ) : null}

          {skillCreateOpen ? (
            <CreateSkillDialog
              projects={projects}
              selectedProjectId={selectedProjectId}
              saving={skillSaving}
              onClose={() => setSkillCreateOpen(false)}
              onCreate={async (payload) => {
                await onCreateSkill(payload);
                setSkillCreateOpen(false);
              }}
            />
          ) : null}

          {apiInfoNodeId ? (
            <ApiTriggerInfoDialog
              apiKey={ownedApiKeys.find((apiKey) => apiKey.id === graph.nodes.find((node) => node.id === apiInfoNodeId)?.api_trigger?.api_key_id) ?? null}
              onClose={() => setApiInfoNodeId(null)}
            />
          ) : null}

          {canvasHelpOpen ? <CanvasHelpDialog onClose={() => setCanvasHelpOpen(false)} /> : null}
        </>
      )}
    </section>
  );
}

function ProjectNodeCard({
  node,
  agents,
  mcpServers,
  skills,
  apiKeys,
  emailReceivers,
  environments,
  dragging,
  connectionState,
  onPointerDown,
  onConnectorPointerDown,
  onRemove,
  onScheduleChange,
  onSlackTriggerChange,
  onApiTriggerChange,
  onEmailTriggerChange,
  onParameterValuesChange,
  onRotateApiKey,
  onCreateApiKey,
  onCreateEmailReceiver,
  onOpenApiInfo,
  onPlayPromptChange,
  onRunPlay,
  onOpenPlay,
  onOpenAgent,
  onOpenMcpServer,
  onOpenSkill,
  runningPlay,
  readOnly,
  shouldSuppressClick,
}: {
  node: ProjectNode;
  agents: AgentRecord[];
  mcpServers: RegisteredMcpServer[];
  skills: SkillRecord[];
  apiKeys: ApiKeyRecord[];
  emailReceivers: EmailReceiverRecord[];
  environments: AnthropicEnvironment[];
  dragging: boolean;
  connectionState: "idle" | "source" | "valid" | "invalid";
  onPointerDown: (event: React.PointerEvent) => void;
  onConnectorPointerDown: (event: React.PointerEvent) => void;
  onRemove: () => void;
  onScheduleChange: (schedule: ScheduleDraft) => void;
  onSlackTriggerChange: (slackTrigger: SlackTriggerDraft) => void;
  onApiTriggerChange: (apiTrigger: ApiTriggerDraft) => void;
  onEmailTriggerChange: (emailTrigger: EmailTriggerDraft) => void;
  onParameterValuesChange: (parameterValues: Record<string, string>) => void;
  onRotateApiKey: () => void;
  onCreateApiKey: () => void;
  onCreateEmailReceiver: () => void;
  onOpenApiInfo: () => void;
  onPlayPromptChange: (prompt: string) => void;
  onRunPlay: () => void;
  onOpenPlay: () => void;
  onOpenAgent: (record: AgentRecord) => void;
  onOpenMcpServer: (server: RegisteredMcpServer) => void;
  onOpenSkill: (skill: SkillRecord) => void;
  runningPlay: boolean;
  readOnly: boolean;
  shouldSuppressClick: () => boolean;
}) {
  const agent = node.agent_id ? agents.find((record) => record.id === node.agent_id) : undefined;
  const parameterConfig = agent ? agentParameterConfigFromMetadata(agent.agent.metadata) : createDefaultAgentParameterConfig();
  const mcpServer = node.mcp_server_id ? mcpServers.find((server) => server.id === node.mcp_server_id) : undefined;
  const skill = node.skill_id ? skills.find((record) => record.id === node.skill_id) : undefined;
  const schedule = node.schedule ?? createDefaultScheduleDraft();
  const slackTrigger = node.slack_trigger ?? createDefaultSlackTriggerDraft();
  const apiTrigger = node.api_trigger ?? createDefaultApiTriggerDraft(apiKeys);
  const emailTrigger = node.email_trigger ?? createDefaultEmailTriggerDraft(emailReceivers);

  return (
    <article
      className={`project-node ${node.type} ${parameterConfig.enabled ? "has-parameters" : ""} ${dragging ? "dragging" : ""} ${readOnly ? "readonly" : ""} connect-${connectionState}`}
      style={{ left: node.x, top: node.y }}
      onPointerDown={onPointerDown}
      onClick={(event) => {
        if ((event.target as HTMLElement).closest("button, input, select, textarea, .project-connector, .node-parameter-editor")) return;
        if (shouldSuppressClick()) return;
        if (node.type === "play" || node.type === "schedule" || node.type === "slack" || node.type === "api" || node.type === "email") {
          onOpenPlay();
          return;
        }
        if (node.type === "agent" && agent) {
          onOpenAgent(agent);
          return;
        }
        if (node.type === "mcp" && mcpServer) {
          onOpenMcpServer(mcpServer);
          return;
        }
        if (node.type === "skill" && skill) {
          onOpenSkill(skill);
        }
      }}
      data-project-node-id={node.id}
    >
      {node.type === "mcp" || node.type === "skill" ? (
        <div className="mcp-node-actions">
          {!readOnly ? <button className="project-connector" type="button" onPointerDown={onConnectorPointerDown} title="Drag to connect" aria-label="Drag to connect" /> : null}
          {!readOnly ? (
            <button className="icon-button compact-icon project-card-remove" type="button" onClick={onRemove} title="Remove card">
              <X size={12} aria-hidden="true" />
            </button>
          ) : null}
        </div>
      ) : (
        <div className="project-node-head">
          <span>{node.type === "play" ? "Play" : node.type === "schedule" ? "Schedule" : node.type === "slack" ? "Slack" : node.type === "api" ? "API" : node.type === "email" ? "Email" : "Agent"}</span>
          {!readOnly ? <button className="project-connector" type="button" onPointerDown={onConnectorPointerDown} title="Drag to connect" aria-label="Drag to connect" /> : null}
          {node.type === "api" ? (
            <button className="icon-button compact-icon project-card-info" type="button" onClick={onOpenApiInfo} title="API trigger help">
              <Info size={12} aria-hidden="true" />
            </button>
          ) : null}
          {!readOnly ? (
            <button className="icon-button compact-icon project-card-remove" type="button" onClick={onRemove} title="Remove card">
              <X size={12} aria-hidden="true" />
            </button>
          ) : null}
        </div>
      )}

      {node.type === "play" ? (
        <div className="project-play-body">
          <input
            className="project-play-prompt"
            value={node.prompt ?? ""}
            onChange={(event) => onPlayPromptChange(event.target.value)}
            disabled={readOnly}
            placeholder="First prompt"
            onPointerDown={(event) => event.stopPropagation()}
          />
          <button
            className="project-play-button"
            type="button"
            title="Start sessions"
            onClick={(event) => {
              event.stopPropagation();
              onRunPlay();
            }}
            disabled={runningPlay || readOnly}
          >
            {runningPlay ? <Loader2 className="spin" size={20} aria-hidden="true" /> : <Play size={22} aria-hidden="true" />}
          </button>
        </div>
      ) : node.type === "agent" ? (
        <>
          <strong>{agent?.agent.name ?? shortId(node.agent_id ?? node.id)}</strong>
          {!agent ? <small>Missing agent</small> : null}
          {agent && parameterConfig.enabled ? (
            <NodeParameterEditor
              config={parameterConfig}
              values={node.parameter_values ?? {}}
              onChange={onParameterValuesChange}
              disabled={readOnly}
            />
          ) : null}
        </>
      ) : node.type === "mcp" ? (
        <div className="mcp-node-icon" title={mcpServer?.name ?? shortId(node.mcp_server_id ?? node.id)}>
          <McpServerIcon server={mcpServer} fallbackSize={34} />
        </div>
      ) : node.type === "skill" ? (
        <div className="skill-node-body" title={skill?.display_title ?? shortId(node.skill_id ?? node.id)}>
          <Sparkles size={18} aria-hidden="true" />
          <span>{skill?.display_title || shortId(node.skill_id ?? node.id)}</span>
        </div>
      ) : node.type === "slack" ? (
        <SlackTriggerMiniEditor slackTrigger={slackTrigger} onChange={onSlackTriggerChange} disabled={readOnly} />
      ) : node.type === "api" ? (
        <ApiTriggerMiniEditor apiTrigger={apiTrigger} apiKeys={apiKeys} onChange={onApiTriggerChange} onRotate={onRotateApiKey} onCreate={onCreateApiKey} disabled={readOnly} />
      ) : node.type === "email" ? (
        <EmailTriggerMiniEditor emailTrigger={emailTrigger} emailReceivers={emailReceivers} onChange={onEmailTriggerChange} onCreate={onCreateEmailReceiver} disabled={readOnly} />
      ) : (
        <ScheduleMiniEditor schedule={schedule} prompt={node.prompt ?? ""} onChange={onScheduleChange} onPromptChange={onPlayPromptChange} disabled={readOnly} />
      )}
    </article>
  );
}

function ProjectCardPalette({
  palette,
  agents,
  mcpServers,
  skills,
  skillsLoading,
  onTabChange,
  onAddPlay,
  onAddSchedule,
  onAddSlack,
  onAddApi,
  onAddEmail,
  onAddAgent,
  onAddMcp,
  onAddSkill,
  onCreateAgent,
  onCreateMcpServer,
  onCreateSkill,
  onClose,
}: {
  palette: { x: number; y: number; tab: PaletteTab };
  agents: AgentRecord[];
  mcpServers: RegisteredMcpServer[];
  skills: SkillRecord[];
  skillsLoading: boolean;
  onTabChange: (tab: PaletteTab) => void;
  onAddPlay: () => void;
  onAddSchedule: () => void;
  onAddSlack: () => void;
  onAddApi: () => void;
  onAddEmail: () => void;
  onAddAgent: (agentId: string) => void;
  onAddMcp: (mcpServerId: string) => void;
  onAddSkill: (skillId: string) => void;
  onCreateAgent: () => void;
  onCreateMcpServer: () => void;
  onCreateSkill: () => void;
  onClose: () => void;
}) {
  const globalAgents = agents.filter((record) => agentIsGlobal(record.agent));
  const customAgents = agents.filter((record) => !agentIsGlobal(record.agent));
  const globalMcpServers = mcpServers.filter(mcpServerIsGlobal);
  const projectMcpServers = mcpServers.filter((server) => !mcpServerIsGlobal(server));
  const builtInSkills = skills.filter((skill) => skill.source === "anthropic");
  const globalSkills = skills.filter((skill) => !skillIsBuiltIn(skill) && skillIsGlobal(skill));
  const projectSkills = skills.filter((skill) => !skillIsBuiltIn(skill) && !skillIsGlobal(skill));
  const [agentSectionsOpen, setAgentSectionsOpen] = React.useState(readPaletteAgentSections);
  const [mcpSectionsOpen, setMcpSectionsOpen] = React.useState(readPaletteMcpSections);
  const [skillSectionsOpen, setSkillSectionsOpen] = React.useState(readPaletteSkillSections);

  function toggleAgentSection(section: "global" | "project") {
    setAgentSectionsOpen((current) => {
      const next = { ...current, [section]: !current[section] };
      writePaletteAgentSections(next);
      return next;
    });
  }

  function toggleMcpSection(section: "global" | "project") {
    setMcpSectionsOpen((current) => {
      const next = { ...current, [section]: !current[section] };
      writePaletteMcpSections(next);
      return next;
    });
  }

  function toggleSkillSection(section: "builtIn" | "global" | "project") {
    setSkillSectionsOpen((current) => {
      const next = { ...current, [section]: !current[section] };
      writePaletteSkillSections(next);
      return next;
    });
  }

  return (
    <div className="project-card-palette" style={{ left: palette.x, top: palette.y }} role="dialog" aria-label="Create project card" onWheel={(event) => event.stopPropagation()}>
      <div className="palette-tabs" role="tablist" aria-label="Card categories">
        <button className={palette.tab === "triggers" ? "active" : ""} type="button" onClick={() => onTabChange("triggers")}>
          Triggers
        </button>
        <button className={palette.tab === "agents" ? "active" : ""} type="button" onClick={() => onTabChange("agents")}>
          Agents
        </button>
        <button className={palette.tab === "mcps" ? "active" : ""} type="button" onClick={() => onTabChange("mcps")}>
          MCPs
        </button>
        <button className={palette.tab === "skills" ? "active" : ""} type="button" onClick={() => onTabChange("skills")}>
          Skills
        </button>
        <button className="icon-button compact-icon" type="button" onClick={onClose} title="Close">
          <X size={14} aria-hidden="true" />
        </button>
      </div>

      {palette.tab === "triggers" ? (
        <div className="palette-list">
          <button type="button" onClick={onAddPlay}>
            <Play size={16} aria-hidden="true" />
            <span>
              <strong>Play</strong>
              <small>Main run card</small>
            </span>
          </button>
          <button type="button" onClick={onAddSchedule}>
            <Calendar size={16} aria-hidden="true" />
            <span>
              <strong>Schedule</strong>
              <small>Timed deployment trigger</small>
            </span>
          </button>
          <button type="button" onClick={onAddSlack}>
            <MessageSquare size={16} aria-hidden="true" />
            <span>
              <strong>Slack</strong>
              <small>User message trigger</small>
            </span>
          </button>
          <button type="button" onClick={onAddApi}>
            <KeyRound size={16} aria-hidden="true" />
            <span>
              <strong>API</strong>
              <small>Keyed API trigger</small>
            </span>
          </button>
          <button type="button" onClick={onAddEmail}>
            <Mail size={16} aria-hidden="true" />
            <span>
              <strong>Email</strong>
              <small>Resend inbound trigger</small>
            </span>
          </button>
        </div>
      ) : palette.tab === "agents" ? (
        <div className="palette-list">
          {agents.length === 0 ? <div className="palette-empty">No agents available</div> : null}
          <button className="palette-section-toggle" type="button" onClick={() => toggleAgentSection("global")} aria-expanded={agentSectionsOpen.global}>
            <span>Legacy global</span>
            {agentSectionsOpen.global ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}
          </button>
          {agentSectionsOpen.global ? (
            globalAgents.length > 0 ? (
              globalAgents.map((record) => <PaletteAgentButton record={record} onAddAgent={onAddAgent} key={record.id} />)
            ) : (
              <div className="palette-empty">No global agents</div>
            )
          ) : null}
          <button className="palette-section-toggle" type="button" onClick={() => toggleAgentSection("project")} aria-expanded={agentSectionsOpen.project}>
            <span>In this project</span>
            {agentSectionsOpen.project ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}
          </button>
          {agentSectionsOpen.project ? (
            customAgents.length > 0 ? (
              customAgents.map((record) => <PaletteAgentButton record={record} onAddAgent={onAddAgent} key={record.id} />)
            ) : (
              <div className="palette-empty">No project agents</div>
            )
          ) : null}
          <button className="palette-create-button" type="button" onClick={onCreateAgent}>
            <Plus size={16} aria-hidden="true" />
            Create new Agent
          </button>
        </div>
      ) : palette.tab === "mcps" ? (
        <div className="palette-list">
          {mcpServers.length === 0 ? <div className="palette-empty">No MCP servers available</div> : null}
          <button className="palette-section-toggle" type="button" onClick={() => toggleMcpSection("global")} aria-expanded={mcpSectionsOpen.global}>
            <span>Global</span>
            {mcpSectionsOpen.global ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}
          </button>
          {mcpSectionsOpen.global ? (
            globalMcpServers.length > 0 ? (
              globalMcpServers.map((server) => <PaletteMcpButton server={server} onAddMcp={onAddMcp} key={server.id} />)
            ) : (
              <div className="palette-empty">No legacy global MCP servers</div>
            )
          ) : null}
          <button className="palette-section-toggle" type="button" onClick={() => toggleMcpSection("project")} aria-expanded={mcpSectionsOpen.project}>
            <span>In this project</span>
            {mcpSectionsOpen.project ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}
          </button>
          {mcpSectionsOpen.project ? (
            projectMcpServers.length > 0 ? (
              projectMcpServers.map((server) => <PaletteMcpButton server={server} onAddMcp={onAddMcp} key={server.id} />)
            ) : (
              <div className="palette-empty">No project MCP servers</div>
            )
          ) : null}
          <button className="palette-create-button" type="button" onClick={onCreateMcpServer}>
            <Plus size={16} aria-hidden="true" />
            Create new MCP
          </button>
        </div>
      ) : (
        <div className="palette-list">
          {skillsLoading ? (
            <div className="palette-empty">
              <Loader2 className="spin" size={15} aria-hidden="true" />
              Loading skills
            </div>
          ) : null}
          {!skillsLoading && skills.length === 0 ? <div className="palette-empty">No skills available</div> : null}
          <button className="palette-section-toggle" type="button" onClick={() => toggleSkillSection("builtIn")} aria-expanded={skillSectionsOpen.builtIn}>
            <span>Built-in</span>
            {skillSectionsOpen.builtIn ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}
          </button>
          {skillSectionsOpen.builtIn ? (
            builtInSkills.length > 0 ? (
              builtInSkills.map((skill) => <PaletteSkillButton skill={skill} onAddSkill={onAddSkill} key={skill.id} />)
            ) : (
              <div className="palette-empty">No built-in skills</div>
            )
          ) : null}
          <button className="palette-section-toggle" type="button" onClick={() => toggleSkillSection("global")} aria-expanded={skillSectionsOpen.global}>
            <span>Global</span>
            {skillSectionsOpen.global ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}
          </button>
          {skillSectionsOpen.global ? (
            globalSkills.length > 0 ? (
              globalSkills.map((skill) => <PaletteSkillButton skill={skill} onAddSkill={onAddSkill} key={skill.id} />)
            ) : (
              <div className="palette-empty">No global skills</div>
            )
          ) : null}
          <button className="palette-section-toggle" type="button" onClick={() => toggleSkillSection("project")} aria-expanded={skillSectionsOpen.project}>
            <span>In this project</span>
            {skillSectionsOpen.project ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}
          </button>
          {skillSectionsOpen.project ? (
            projectSkills.length > 0 ? (
              projectSkills.map((skill) => <PaletteSkillButton skill={skill} onAddSkill={onAddSkill} key={skill.id} />)
            ) : (
              <div className="palette-empty">No project skills</div>
            )
          ) : null}
          <button className="palette-create-button" type="button" onClick={onCreateSkill}>
            <Plus size={16} aria-hidden="true" />
            Create new Skill
          </button>
        </div>
      )}
    </div>
  );
}

function PaletteMcpButton({ server, onAddMcp }: { server: RegisteredMcpServer; onAddMcp: (mcpServerId: string) => void }) {
  return (
    <button type="button" onClick={() => onAddMcp(server.id)} title={server.id}>
      <McpServerIcon className="palette-mcp-icon" server={server} fallbackSize={16} />
      <span>
        <strong>{server.name}</strong>
        <small>{server.description || "No description"}</small>
      </span>
    </button>
  );
}

function PaletteSkillButton({ skill, onAddSkill }: { skill: SkillRecord; onAddSkill: (skillId: string) => void }) {
  return (
    <button type="button" onClick={() => onAddSkill(skill.id)} title={skill.id}>
      <Sparkles size={16} aria-hidden="true" />
      <span>
        <strong>{skill.display_title || skill.id}</strong>
        <small>{skill.description?.trim() || "No description"}</small>
      </span>
    </button>
  );
}

function PaletteAgentButton({ record, onAddAgent }: { record: AgentRecord; onAddAgent: (agentId: string) => void }) {
  return (
    <button type="button" onClick={() => onAddAgent(record.id)}>
      <Bot size={16} aria-hidden="true" />
      <span>
        <strong>{record.agent.name}</strong>
        <small>{record.agent.description || "No description"}</small>
      </span>
    </button>
  );
}

function McpServerIcon({ server, fallbackSize, className }: { server: RegisteredMcpServer | undefined; fallbackSize: number; className?: string }) {
  return server?.icon_data_url ? <img className={className} src={server.icon_data_url} alt="" draggable={false} /> : <Server className={className} size={fallbackSize} aria-hidden="true" />;
}

function ScheduleMiniEditor({
  schedule,
  prompt,
  onChange,
  onPromptChange,
  disabled = false,
}: {
  schedule: ScheduleDraft;
  prompt: string;
  onChange: (schedule: ScheduleDraft) => void;
  onPromptChange: (prompt: string) => void;
  disabled?: boolean;
}) {
  return (
    <div className="schedule-mini">
      <input value={prompt} onChange={(event) => onPromptChange(event.target.value)} disabled={disabled} placeholder="Scheduled prompt" onPointerDown={(event) => event.stopPropagation()} />
      <select value={schedule.mode} onChange={(event) => onChange({ ...schedule, mode: event.target.value as ScheduleMode })} disabled={disabled}>
        <option value="hours">Every X hours</option>
        <option value="days">Every X days</option>
        <option value="weeks">Every X weeks</option>
        <option value="cron">Cron</option>
      </select>
      {schedule.mode === "cron" ? (
        <input value={schedule.expression} onChange={(event) => onChange({ ...schedule, expression: event.target.value })} disabled={disabled} placeholder="0 9 * * 1" />
      ) : (
        <input
          type="number"
          min={1}
          value={schedule.interval}
          onChange={(event) => onChange({ ...schedule, interval: Number(event.target.value) || 1 })}
          disabled={disabled}
        />
      )}
    </div>
  );
}

function SlackTriggerMiniEditor({
  slackTrigger,
  onChange,
  disabled = false,
}: {
  slackTrigger: SlackTriggerDraft;
  onChange: (slackTrigger: SlackTriggerDraft) => void;
  disabled?: boolean;
}) {
  const value =
    slackTrigger.type === "channel"
      ? slackTrigger.channel_id ?? ""
      : slackTrigger.type === "user"
        ? slackTrigger.user_id ?? ""
        : slackTrigger.type === "keyword"
          ? slackTrigger.keyword ?? ""
          : "";
  const valuePlaceholder =
    slackTrigger.type === "channel" ? "Channel ID" : slackTrigger.type === "user" ? "User ID" : slackTrigger.type === "keyword" ? "Keyword" : "";

  return (
    <div className="slack-mini">
      <select
        value={slackTrigger.type}
        onChange={(event) => onChange(createSlackTriggerDraft(event.target.value as SlackTriggerType, slackTrigger))}
        disabled={disabled}
        onPointerDown={(event) => event.stopPropagation()}
      >
        <option value="none">None</option>
        <option value="all">All</option>
        <option value="channel">Channel</option>
        <option value="user">User</option>
        <option value="keyword">Keyword</option>
      </select>
      {slackTrigger.type === "channel" || slackTrigger.type === "user" || slackTrigger.type === "keyword" ? (
        <input
          value={value}
          onChange={(event) => onChange(createSlackTriggerDraft(slackTrigger.type, slackTrigger, event.target.value.trim()))}
          disabled={disabled}
          placeholder={valuePlaceholder}
          onPointerDown={(event) => event.stopPropagation()}
        />
      ) : null}
    </div>
  );
}

function ApiTriggerMiniEditor({
  apiTrigger,
  apiKeys,
  onChange,
  onRotate,
  onCreate,
  disabled = false,
}: {
  apiTrigger: ApiTriggerDraft;
  apiKeys: ApiKeyRecord[];
  onChange: (apiTrigger: ApiTriggerDraft) => void;
  onRotate: () => void;
  onCreate: () => void;
  disabled?: boolean;
}) {
  const canRotate = apiKeys.some((apiKey) => apiKey.id === apiTrigger.api_key_id);

  return (
    <div className="api-mini">
      <select
        value={apiTrigger.api_key_id}
        onChange={(event) => onChange({ api_key_id: event.target.value })}
        disabled={disabled}
        onPointerDown={(event) => event.stopPropagation()}
      >
        <option value="">Select API key</option>
        {apiKeys.map((apiKey) => (
          <option value={apiKey.id} key={apiKey.id}>
            {apiKey.name}
          </option>
        ))}
      </select>
      <div className="api-mini-actions">
        <button className="secondary-button compact-button" type="button" onClick={onRotate} disabled={disabled || !canRotate}>
          <RefreshCw size={14} aria-hidden="true" />
          Rotate
        </button>
        <button className="secondary-button compact-button" type="button" onClick={onCreate} disabled={disabled}>
          <Plus size={14} aria-hidden="true" />
          New
        </button>
      </div>
    </div>
  );
}

function EmailTriggerMiniEditor({
  emailTrigger,
  emailReceivers,
  onChange,
  onCreate,
  disabled = false,
}: {
  emailTrigger: EmailTriggerDraft;
  emailReceivers: EmailReceiverRecord[];
  onChange: (emailTrigger: EmailTriggerDraft) => void;
  onCreate: () => void;
  disabled?: boolean;
}) {
  const selected = emailReceivers.find((receiver) => receiver.id === emailTrigger.receiver_id);

  return (
    <div className="email-mini">
      <select
        value={emailTrigger.receiver_id}
        onChange={(event) => onChange({ receiver_id: event.target.value })}
        disabled={disabled}
        onPointerDown={(event) => event.stopPropagation()}
      >
        <option value="">Select receiver</option>
        {emailReceivers.map((receiver) => (
          <option value={receiver.id} key={receiver.id}>
            {receiver.name}@{receiver.domain}
          </option>
        ))}
      </select>
      <div className="email-mini-row">
        <span>{selected ? `${selected.name}@${selected.domain}` : "No receiver selected"}</span>
        <button className="secondary-button compact-button" type="button" onClick={onCreate} disabled={disabled}>
          <Plus size={14} aria-hidden="true" />
          New
        </button>
      </div>
    </div>
  );
}

function ApiTriggerInfoDialog({ apiKey, onClose }: { apiKey: ApiKeyRecord | null; onClose: () => void }) {
  const [tab, setTab] = React.useState<"curl" | "node">("curl");
  const apiKeyLabel = apiKey ? `${apiKey.name} (${apiKey.key_prefix}...)` : "selected API key";
  const endpoint = `${backendUrl}/api/run`;
  const curlSnippet = `curl -X POST "${endpoint}" \\
  -H "authorization: Bearer <API_KEY>" \\
  -H "content-type: application/json" \\
  -d '{
    "sessionId": "sesn_...",
    "message": "Run this trigger"
  }'`;
  const nodeSnippet = `const response = await fetch("${endpoint}", {
  method: "POST",
  headers: {
    authorization: "Bearer <API_KEY>",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    sessionId: "sesn_...",
    message: "Run this trigger",
  }),
});

if (!response.ok) {
  throw new Error(await response.text());
}

const result = await response.json();
console.log(result);`;

  return (
    <Modal title="Call API trigger" onClose={onClose}>
      <div className="api-trigger-help">
        <p>Use the one-time secret value for the {apiKeyLabel}. The visible prefix is not enough to authenticate.</p>
        <p>Include <code>sessionId</code> to continue an existing session, or omit it to start a new one.</p>
        <div className="snippet-tabs" role="tablist" aria-label="API trigger examples">
          <button className={tab === "curl" ? "active" : ""} type="button" role="tab" aria-selected={tab === "curl"} onClick={() => setTab("curl")}>
            cURL
          </button>
          <button className={tab === "node" ? "active" : ""} type="button" role="tab" aria-selected={tab === "node"} onClick={() => setTab("node")}>
            Node
          </button>
        </div>
        <pre className="snippet-block">
          <code>{tab === "curl" ? curlSnippet : nodeSnippet}</code>
        </pre>
      </div>
    </Modal>
  );
}

function CanvasHelpDialog({ onClose }: { onClose: () => void }) {
  return (
    <Modal title="Canvas controls" onClose={onClose}>
      <div className="canvas-help">
        <div className="shortcut-list" aria-label="Canvas shortcuts">
          <div className="shortcut-row">
            <kbd>⌘</kbd>
            <span>+</span>
            <kbd>Click</kbd>
            <p>Open card options at the clicked canvas position.</p>
          </div>
          <div className="shortcut-row">
            <kbd>Shift</kbd>
            <span>+</span>
            <kbd>A</kbd>
            <p>Open the agent creation window.</p>
          </div>
        </div>
      </div>
    </Modal>
  );
}

function AgentList({
  agents,
  members,
  currentUserEmail,
  loading,
  onSelect,
  onCreate,
}: {
  agents: AgentRecord[];
  members: Member[];
  currentUserEmail: string;
  loading: boolean;
  onSelect: (record: AgentRecord) => void;
  onCreate: () => void;
}) {
  if (loading && agents.length === 0) {
    return (
      <div className="empty-state">
        <Loader2 className="spin" size={24} aria-hidden="true" />
        <span>Loading agents</span>
      </div>
    );
  }

  if (agents.length === 0) {
    return (
      <div className="empty-state">
        <Bot size={28} aria-hidden="true" />
        <strong>No agents found</strong>
        <span>Create the first managed agent for this registry.</span>
        <button className="primary-button" type="button" onClick={onCreate}>
          <Plus size={16} aria-hidden="true" />
          Create agent
        </button>
      </div>
    );
  }

  return (
    <div className="agent-table" role="table" aria-label="Agents">
      <div className="agent-table-head" role="row">
        <span>Name</span>
        <span>Scope</span>
        <span>Model</span>
        <span>Version</span>
        <span>Updated</span>
        <span>Owner</span>
      </div>
      {agents.map((record) => {
        const owned = isAgentCreatorByEmail(record, members, currentUserEmail);
        const projectCount = agentProjectIdsFromMetadata(record.agent.metadata).length;
        const global = projectCount === 0;
        return (
          <button className="agent-row" key={record.id} type="button" onClick={() => onSelect(record)} role="row">
            <span className="agent-name-cell">
              <strong>{record.agent.name}</strong>
              <small>{record.agent.description || record.id}</small>
            </span>
            <span className={global ? "owner-chip mine" : "owner-chip"}>{global ? "Global" : projectCount === 1 ? "Project" : `${projectCount} projects`}</span>
            <span>{modelLabel(record.agent.model)}</span>
            <span className="numeric-cell">v{record.agent.version}</span>
            <span className="numeric-cell">{formatDate(record.agent.updated_at)}</span>
            <span className={owned ? "owner-chip mine" : "owner-chip"}>
              {owned ? <Check size={14} aria-hidden="true" /> : <User size={14} aria-hidden="true" />}
              {owned ? "You" : shortId(record.creator_uuid)}
            </span>
          </button>
        );
      })}
    </div>
  );
}

function PlaySessionsPanel({
  sessions,
  sessionsLoading,
  selectedSessionId,
  messages,
  loading,
  error,
  onSelect,
  onClose,
}: {
  sessions: ManagedSession[];
  sessionsLoading: boolean;
  selectedSessionId: string;
  messages: ChatMessage[];
  loading: boolean;
  error: string | null;
  onSelect: (sessionId: string) => void;
  onClose: () => void;
}) {
  return (
    <Modal title="Trigger sessions" onClose={onClose} side>
      <div className="play-sessions-panel">
        <label>
          <span>Session</span>
          <select value={selectedSessionId} onChange={(event) => onSelect(event.target.value)} disabled={sessions.length === 0 || sessionsLoading}>
            <option value="">Select session</option>
            {sessions.map((session) => (
              <option value={session.id} key={session.id}>
                {session.agent.name} · {formatDateTime(session.updated_at)}
              </option>
            ))}
          </select>
        </label>
        {sessionsLoading ? (
          <div className="empty-state compact-empty">
            <Loader2 className="spin" size={20} aria-hidden="true" />
            <span>Loading sessions</span>
          </div>
        ) : sessions.length === 0 ? (
          <div className="empty-state compact-empty">
            <MessageSquare size={22} aria-hidden="true" />
            <span>No sessions for connected agents</span>
          </div>
        ) : null}
        <ChatMessageList messages={messages} loading={loading} emptyText="Select a session to view its messages." />
        {error ? <div className="notice error">{error}</div> : null}
      </div>
    </Modal>
  );
}

function RunningSessionsStatus({ sessions, onOpen }: { sessions: ManagedSession[]; onOpen: (session: ManagedSession) => void }) {
  if (sessions.length === 0) return null;

  return (
    <aside className="running-sessions-status" aria-label="Running sessions">
      <div className="running-sessions-head">
        <Loader2 className="spin" size={14} aria-hidden="true" />
        <span>{sessions.length === 1 ? "Running" : `${sessions.length} running`}</span>
      </div>
      <div className="running-sessions-list">
        {sessions.map((session) => (
          <button className="running-session-pill" type="button" onClick={() => onOpen(session)} key={session.id} title={`Open ${session.agent.name}`}>
            {session.agent.name}
          </button>
        ))}
      </div>
    </aside>
  );
}

function ChatMessageList({ messages, loading, emptyText }: { messages: ChatMessage[]; loading: boolean; emptyText: string }) {
  return (
    <div className="message-history" aria-live="polite">
      {messages.length === 0 ? (
        <div className="chat-placeholder">
          <MessageSquare size={24} aria-hidden="true" />
          <span>{emptyText}</span>
        </div>
      ) : (
        messages.map((message) => (
          <article className={message.role === "user" ? "chat-message user" : "chat-message assistant"} key={message.id}>
            <span>{message.role === "user" ? "You" : "Agent"}</span>
            {message.role === "assistant" ? <MarkdownMessage content={message.content} /> : <p>{message.content}</p>}
          </article>
        ))
      )}
      {loading ? (
        <article className="chat-message assistant">
          <span>Agent</span>
          <p className="typing-line">
            <Loader2 className="spin" size={16} aria-hidden="true" />
            Loading
          </p>
        </article>
      ) : null}
    </div>
  );
}

function ChatView({
  agents,
  environments,
  vaults,
  sessions,
  sessionsLoading,
  removingSessionId,
  selectedSessionId,
  selectedAgentId,
  selectedEnvironmentId,
  onAgentChange,
  onEnvironmentChange,
  onSessionSelect,
  onSessionRemove,
  messages,
  input,
  onInputChange,
  loading,
  approvalLoadingId,
  error,
  onSubmit,
  onConfirmApproval,
  onCreateAgent,
}: {
  agents: AgentRecord[];
  environments: AnthropicEnvironment[];
  vaults: VaultRecord[];
  sessions: ManagedSession[];
  sessionsLoading: boolean;
  removingSessionId: string | null;
  selectedSessionId: string;
  selectedAgentId: string;
  selectedEnvironmentId: string;
  onAgentChange: (agentId: string) => void;
  onEnvironmentChange: (environmentId: string) => void;
  onSessionSelect: (session: ManagedSession) => void;
  onSessionRemove: (session: ManagedSession) => void;
  messages: ChatMessage[];
  input: string;
  onInputChange: (value: string) => void;
  loading: boolean;
  approvalLoadingId: string | null;
  error: string | null;
  onSubmit: (event: React.FormEvent) => void;
  onConfirmApproval: (message: ChatMessage, result: "allow" | "deny") => void;
  onCreateAgent: () => void;
}) {
  const [settingsOpen, setSettingsOpen] = React.useState(false);

  return (
    <section className="chat-view">
      <div className="chat-panel">
        <div className="chat-controls">
          <div className="chat-agent-control">
            <label>
              <span>Agent</span>
              <select value={selectedAgentId} onChange={(event) => onAgentChange(event.target.value)} disabled={agents.length === 0}>
                {agents.map((record) => (
                  <option value={record.id} key={record.id}>
                    {record.agent.name}
                  </option>
                ))}
              </select>
            </label>
            <button className="icon-button chat-settings-button" type="button" onClick={() => setSettingsOpen(true)} title="Chat settings">
              <Settings size={18} aria-hidden="true" />
            </button>
          </div>
        </div>

        {agents.length === 0 ? (
          <div className="empty-state chat-empty">
            <MessageSquare size={28} aria-hidden="true" />
            <strong>No agents available</strong>
            <span>Create an agent before starting a chat.</span>
            <button className="primary-button" type="button" onClick={onCreateAgent}>
              <Plus size={16} aria-hidden="true" />
              Create agent
            </button>
          </div>
        ) : environments.length === 0 ? (
          <div className="empty-state chat-empty">
            <MonitorCog size={28} aria-hidden="true" />
            <strong>No environments available</strong>
            <span>Create an environment before starting a chat.</span>
          </div>
        ) : (
          <>
            <div className="chat-history-layout">
              <aside className="session-sidebar" aria-label="Sessions">
                <div className="session-sidebar-head">
                  <span>Sessions</span>
                  {sessionsLoading ? <Loader2 className="spin" size={15} aria-hidden="true" /> : null}
                </div>
                <div className="session-list">
                  {sessions.length === 0 ? (
                    <div className="session-empty">No sessions</div>
                  ) : (
                    sessions.map((session) => (
                      <div className={session.id === selectedSessionId ? "session-item active" : "session-item"} key={session.id}>
                        <button className="session-select-button" type="button" onClick={() => onSessionSelect(session)}>
                          <strong>{session.title || session.agent.name}</strong>
                          <span>{session.agent.name}</span>
                          <small>{formatDateTime(session.updated_at)}</small>
                        </button>
                        <button
                          className="session-remove-button"
                          type="button"
                          onClick={() => onSessionRemove(session)}
                          disabled={removingSessionId === session.id}
                          title="Remove session"
                          aria-label={`Remove ${session.title || session.agent.name} session`}
                        >
                          {removingSessionId === session.id ? <Loader2 className="spin" size={14} aria-hidden="true" /> : <X size={14} aria-hidden="true" />}
                        </button>
                      </div>
                    ))
                  )}
                </div>
              </aside>

              <div className="message-history" aria-live="polite">
                {messages.length === 0 ? (
                  <div className="chat-placeholder">
                    <MessageSquare size={24} aria-hidden="true" />
                    <span>Send a message to start this session.</span>
                  </div>
                ) : (
                  messages.map((message) => (
                    <article className={message.role === "user" ? "chat-message user" : "chat-message assistant"} key={message.id}>
                      <span>{message.role === "user" ? "You" : "Agent"}</span>
                      {message.role === "assistant" ? <MarkdownMessage content={message.content} /> : <p>{message.content}</p>}
                      {message.awaitingApproval ? (
                        <div className="approval-actions">
                          {message.approvalStatus === "pending" ? (
                            <>
                              <button className="secondary-button compact-button" type="button" onClick={() => onConfirmApproval(message, "deny")} disabled={Boolean(approvalLoadingId) || loading}>
                                {approvalLoadingId === message.id ? <Loader2 className="spin" size={15} aria-hidden="true" /> : <X size={15} aria-hidden="true" />}
                                Deny
                              </button>
                              <button className="primary-button compact-button" type="button" onClick={() => onConfirmApproval(message, "allow")} disabled={Boolean(approvalLoadingId) || loading}>
                                {approvalLoadingId === message.id ? <Loader2 className="spin" size={15} aria-hidden="true" /> : <Check size={15} aria-hidden="true" />}
                                Approve
                              </button>
                            </>
                          ) : (
                            <span className={message.approvalStatus === "allowed" ? "approval-status allowed" : "approval-status denied"}>
                              {message.approvalStatus === "allowed" ? "Approved" : "Denied"}
                            </span>
                          )}
                        </div>
                      ) : null}
                    </article>
                  ))
                )}
                {loading ? (
                  <article className="chat-message assistant">
                    <span>Agent</span>
                    <p className="typing-line">
                      <Loader2 className="spin" size={16} aria-hidden="true" />
                      Thinking
                    </p>
                  </article>
                ) : null}
              </div>
            </div>

            {error ? <div className="notice error">{error}</div> : null}

            <form className="chat-compose" onSubmit={onSubmit}>
              <textarea
                value={input}
                onChange={(event) => onInputChange(event.target.value)}
                placeholder="Message the selected agent"
                rows={3}
                disabled={loading || !selectedAgentId || !selectedEnvironmentId}
              />
              <button className="primary-button" type="submit" disabled={loading || !input.trim() || !selectedAgentId || !selectedEnvironmentId}>
                {loading ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Send size={16} aria-hidden="true" />}
                Send
              </button>
            </form>
          </>
        )}
      </div>

      {settingsOpen ? (
        <ChatSettingsDialog
          environments={environments}
          vaults={vaults}
          selectedEnvironmentId={selectedEnvironmentId}
          onEnvironmentChange={onEnvironmentChange}
          onClose={() => setSettingsOpen(false)}
        />
      ) : null}
    </section>
  );
}

function ChatSettingsDialog({
  environments,
  vaults,
  selectedEnvironmentId,
  onEnvironmentChange,
  onClose,
}: {
  environments: AnthropicEnvironment[];
  vaults: VaultRecord[];
  selectedEnvironmentId: string;
  onEnvironmentChange: (environmentId: string) => void;
  onClose: () => void;
}) {
  return (
    <Modal title="Chat settings" onClose={onClose}>
      <div className="form-grid">
        <FormSection title="Runtime">
          <label>
            <span>Environment</span>
            <select value={selectedEnvironmentId} onChange={(event) => onEnvironmentChange(event.target.value)} disabled={environments.length === 0}>
              {environments.map((environment) => (
                <option value={environment.id} key={environment.id}>
                  {environment.name}
                </option>
              ))}
            </select>
          </label>
        </FormSection>

        <FormSection title="Vaults">
          <fieldset className="vault-selector modal-vault-selector">
            <legend>Managed vaults</legend>
            {vaults.length === 0 ? (
              <span className="vault-selector-empty">No vaults available</span>
            ) : (
              <div className="vault-selector-options">
                {vaults.map((vault) => (
                  <label className="vault-checkbox" key={vault.id}>
                    <input type="checkbox" checked readOnly />
                    <span>{vault.display_name} · {vault.scope ?? vault.type}</span>
                  </label>
                ))}
              </div>
            )}
          </fieldset>
        </FormSection>

        <div className="dialog-actions">
          <button className="primary-button" type="button" onClick={onClose}>
            <Check size={16} aria-hidden="true" />
            Done
          </button>
        </div>
      </div>
    </Modal>
  );
}

function MarkdownMessage({ content }: { content: string }) {
  return (
    <div className="markdown-message">
      <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
    </div>
  );
}

function DeploymentsView({
  deployments,
  agents,
  environments,
  loading,
  runningDeploymentId,
  error,
  onRefresh,
  onOpenCreate,
  onSelect,
  onRun,
}: {
  deployments: AnthropicDeployment[];
  agents: AgentRecord[];
  environments: AnthropicEnvironment[];
  loading: boolean;
  runningDeploymentId: string | null;
  error: string | null;
  onRefresh: () => void;
  onOpenCreate: () => void;
  onSelect: (deployment: AnthropicDeployment) => void;
  onRun: (deployment: AnthropicDeployment) => void;
}) {
  return (
    <section className="deployments-view">
      <header className="toolbar">
        <div>
          <h1>Deployments</h1>
          <p>{deployments.length} configured</p>
        </div>
        <div className="toolbar-actions">
          <button className="icon-button" type="button" onClick={onRefresh} disabled={loading} title="Refresh deployments">
            {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
          </button>
          <button className="primary-button" type="button" onClick={onOpenCreate} disabled={agents.length === 0 || environments.length === 0}>
            <Plus size={17} aria-hidden="true" />
            Create
          </button>
        </div>
      </header>

      {error ? <div className="notice error">{error}</div> : null}

      {loading && deployments.length === 0 ? (
        <div className="empty-state">
          <Loader2 className="spin" size={24} aria-hidden="true" />
          <span>Loading deployments</span>
        </div>
      ) : deployments.length === 0 ? (
        <div className="empty-state">
          <Rocket size={28} aria-hidden="true" />
          <strong>No deployments found</strong>
          <span>Create a deployment once an agent and environment exist.</span>
          <button className="primary-button" type="button" onClick={onOpenCreate} disabled={agents.length === 0 || environments.length === 0}>
            <Plus size={16} aria-hidden="true" />
            Create
          </button>
        </div>
      ) : (
        <div className="deployment-table" role="table" aria-label="Deployments">
          <div className="deployment-table-head" role="row">
            <span>Name</span>
            <span>Agent</span>
            <span>Environment</span>
            <span>Status</span>
            <span>Updated</span>
            <span>Run</span>
          </div>
          {deployments.map((deployment) => (
            <div className="deployment-row" key={deployment.id} role="row">
              <button className="deployment-select-button" type="button" onClick={() => onSelect(deployment)}>
                <span className="agent-name-cell">
                  <strong>{deployment.name}</strong>
                  <small>{deployment.description || deployment.id}</small>
                </span>
                <span>{deploymentAgentName(deployment, agents)}</span>
                <span>{environmentNameFor(deployment.environment_id, environments)}</span>
                <span className={deployment.status === "active" ? "owner-chip mine" : "owner-chip"}>{deployment.status}</span>
                <span className="numeric-cell">{formatDate(deployment.updated_at)}</span>
              </button>
              <button
                className="icon-button deployment-run-button"
                type="button"
                onClick={() => onRun(deployment)}
                disabled={Boolean(runningDeploymentId)}
                title="Run deployment now"
                aria-label={`Run ${deployment.name} now`}
              >
                {runningDeploymentId === deployment.id ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Play size={16} aria-hidden="true" />}
              </button>
            </div>
          ))}
        </div>
      )}
    </section>
  );
}

function McpServersView({
  servers,
  loading,
  error,
  onRefresh,
  onOpenCreate,
  onSelect,
}: {
  servers: RegisteredMcpServer[];
  loading: boolean;
  error: string | null;
  onRefresh: () => void;
  onOpenCreate: () => void;
  onSelect: (server: RegisteredMcpServer) => void;
}) {
  return (
    <section className="mcp-servers-view">
      <header className="toolbar">
        <div>
          <h1>MCP Servers</h1>
          <p>{servers.length} configured</p>
        </div>
        <div className="toolbar-actions">
          <button className="icon-button" type="button" onClick={onRefresh} disabled={loading} title="Refresh MCP servers">
            {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
          </button>
          <button className="primary-button" type="button" onClick={onOpenCreate}>
            <Plus size={17} aria-hidden="true" />
            Add
          </button>
        </div>
      </header>

      {error ? <div className="notice error">{error}</div> : null}

      {loading && servers.length === 0 ? (
        <div className="empty-state">
          <Loader2 className="spin" size={24} aria-hidden="true" />
          <span>Loading MCP servers</span>
        </div>
      ) : servers.length === 0 ? (
        <div className="empty-state">
          <Server size={28} aria-hidden="true" />
          <strong>No MCP servers found</strong>
          <span>Add MCP servers before attaching them to agents.</span>
          <button className="primary-button" type="button" onClick={onOpenCreate}>
            <Plus size={16} aria-hidden="true" />
            Add
          </button>
        </div>
      ) : (
        <div className="mcp-server-table" role="table" aria-label="MCP servers">
          <div className="mcp-server-table-head" role="row">
            <span>Icon</span>
            <span>Name</span>
            <span>URL</span>
            <span>Scope</span>
            <span>Auth</span>
            <span>Updated</span>
          </div>
          {servers.map((server) => (
            <button className="mcp-server-row" key={server.id} type="button" role="row" onClick={() => onSelect(server)}>
              <span className="mcp-server-icon-cell">{server.icon_data_url ? <img src={server.icon_data_url} alt="" /> : <Server size={20} aria-hidden="true" />}</span>
              <span className="agent-name-cell">
                <strong>{server.name}</strong>
                <small>{server.description || server.id}</small>
              </span>
              <span>{server.url}</span>
              <span className="owner-chip">{mcpScopeLabel(server)}</span>
              <span className="owner-chip">{mcpAuthLabel(server.auth_type)}</span>
              <span className="numeric-cell">{formatDate(server.updated_at)}</span>
            </button>
          ))}
        </div>
      )}
    </section>
  );
}

function SkillsView({
  skills,
  loading,
  saving,
  error,
  onRefresh,
  onOpenCreate,
  onSelect,
}: {
  skills: SkillRecord[];
  loading: boolean;
  saving: boolean;
  error: string | null;
  onRefresh: () => void;
  onOpenCreate: () => void;
  onSelect: (skill: SkillRecord) => void;
}) {
  const builtInCount = skills.filter((skill) => skill.source === "anthropic").length;
  const customCount = skills.length - builtInCount;
  return (
    <section className="skills-view">
      <header className="toolbar">
        <div>
          <h1>Skills</h1>
          <p>{builtInCount} built-in, {customCount} custom</p>
        </div>
        <div className="toolbar-actions">
          <button className="icon-button" type="button" onClick={onRefresh} disabled={loading} title="Refresh skills">
            {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
          </button>
          <button className="primary-button" type="button" onClick={onOpenCreate} disabled={saving}>
            {saving ? <Loader2 className="spin" size={17} aria-hidden="true" /> : <Plus size={17} aria-hidden="true" />}
            Create
          </button>
        </div>
      </header>

      {error ? <div className="notice error">{error}</div> : null}

      {loading && skills.length === 0 ? (
        <div className="empty-state">
          <Loader2 className="spin" size={24} aria-hidden="true" />
          <span>Loading skills</span>
        </div>
      ) : skills.length === 0 ? (
        <div className="empty-state">
          <Sparkles size={28} aria-hidden="true" />
          <strong>No skills found</strong>
          <span>Create a skill before attaching it to agents.</span>
          <button className="primary-button" type="button" onClick={onOpenCreate} disabled={saving}>
            <Plus size={16} aria-hidden="true" />
            Create
          </button>
        </div>
      ) : (
        <div className="skill-table" role="table" aria-label="Skills">
          <div className="skill-table-head" role="row">
            <span>Name</span>
            <span>Source</span>
            <span>Type</span>
            <span>Version</span>
            <span>Updated</span>
          </div>
          {skills.map((skill) => (
            <button className="skill-row" key={skill.id} type="button" role="row" onClick={() => onSelect(skill)}>
              <span className="agent-name-cell">
                <strong>{skill.display_title || skill.id}</strong>
                <small>{skill.description || skill.id}</small>
              </span>
              <span className="owner-chip">{skill.source}</span>
              <span>{skill.type}</span>
              <span className="numeric-cell">{skill.source === "anthropic" && !skill.latest_version ? "Built-in" : (skill.latest_version ?? "No version")}</span>
              <span className="numeric-cell">{skill.source === "anthropic" ? "Built-in" : formatDate(skill.updated_at)}</span>
            </button>
          ))}
        </div>
      )}
    </section>
  );
}

function EnvironmentsView({
  environments,
  loading,
  error,
  onRefresh,
  onOpenCreate,
  onOpenEdit,
}: {
  environments: AnthropicEnvironment[];
  loading: boolean;
  error: string | null;
  onRefresh: () => void;
  onOpenCreate: () => void;
  onOpenEdit: (environment: AnthropicEnvironment) => void;
}) {
  return (
    <section className="environments-view">
      <header className="toolbar">
        <div>
          <h1>Environments</h1>
          <p>{environments.length} available</p>
        </div>
        <div className="toolbar-actions">
          <button className="icon-button" type="button" onClick={onRefresh} disabled={loading} title="Refresh environments">
            {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
          </button>
          <button className="primary-button" type="button" onClick={onOpenCreate}>
            <Plus size={17} aria-hidden="true" />
            Create
          </button>
        </div>
      </header>

      {error ? <div className="notice error">{error}</div> : null}

      <div className="environment-list">
        {loading && environments.length === 0 ? (
          <div className="empty-state">
            <Loader2 className="spin" size={24} aria-hidden="true" />
            <span>Loading environments</span>
          </div>
        ) : environments.length === 0 ? (
          <div className="empty-state">
            <MonitorCog size={28} aria-hidden="true" />
            <strong>No environments found</strong>
            <span>Create a cloud or self-hosted environment.</span>
            <button className="primary-button" type="button" onClick={onOpenCreate}>
              <Plus size={16} aria-hidden="true" />
              Create
            </button>
          </div>
        ) : (
          <div className="environment-table" role="table" aria-label="Environments">
            <div className="environment-table-head" role="row">
              <span>Name</span>
              <span>Type</span>
              <span>Scope</span>
              <span>Updated</span>
              <span>Actions</span>
            </div>
            {environments.map((environment) => (
              <article className="environment-row" key={environment.id} role="row">
                <span className="agent-name-cell">
                  <strong>{environment.name}</strong>
                  <small>{environmentPackageSummary(environment) || environment.description || environment.id}</small>
                </span>
                <span className="owner-chip">{environment.config.type}</span>
                <span>{environment.scope ?? "account"}</span>
                <span className="numeric-cell">{formatDate(environment.updated_at)}</span>
                <span className="environment-actions">
                  <button className="icon-button" type="button" onClick={() => onOpenEdit(environment)} title="Edit environment">
                    <Pencil size={16} aria-hidden="true" />
                  </button>
                </span>
              </article>
            ))}
          </div>
        )}
      </div>
    </section>
  );
}

function IntegrationsView({
  integrations,
  loading,
  error,
  onRefresh,
  onOpenCreate,
  onSelect,
}: {
  integrations: IntegrationTemplate[];
  loading: boolean;
  error: string | null;
  onRefresh: () => void;
  onOpenCreate: () => void;
  onSelect: (integration: IntegrationTemplate) => void;
}) {
  return (
    <section className="mcp-servers-view">
      <header className="toolbar">
        <div>
          <h1>Integrations</h1>
          <p>{integrations.length} templates</p>
        </div>
        <div className="toolbar-actions">
          <button className="icon-button" type="button" onClick={onRefresh} disabled={loading} title="Refresh integrations">
            {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
          </button>
          <button className="primary-button" type="button" onClick={onOpenCreate}>
            <Plus size={17} aria-hidden="true" />
            Add
          </button>
        </div>
      </header>

      {error ? <div className="notice error">{error}</div> : null}

      {loading && integrations.length === 0 ? (
        <div className="empty-state">
          <Loader2 className="spin" size={24} aria-hidden="true" />
          <span>Loading integrations</span>
        </div>
      ) : integrations.length === 0 ? (
        <div className="empty-state">
          <Puzzle size={28} aria-hidden="true" />
          <strong>No integrations found</strong>
          <span>Add integration templates for project editors to install.</span>
          <button className="primary-button" type="button" onClick={onOpenCreate}>
            <Plus size={16} aria-hidden="true" />
            Add
          </button>
        </div>
      ) : (
        <div className="mcp-server-table" role="table" aria-label="Integrations">
          <div className="mcp-server-table-head" role="row">
            <span>Logo</span>
            <span>Name</span>
            <span>MCP URL</span>
            <span>Auth</span>
            <span>Agent</span>
            <span>Updated</span>
          </div>
          {integrations.map((integration) => (
            <button className="mcp-server-row" key={integration.id} type="button" role="row" onClick={() => onSelect(integration)}>
              <span className="mcp-server-icon-cell">{integration.logo_data_url ? <img src={integration.logo_data_url} alt="" /> : <Puzzle size={20} aria-hidden="true" />}</span>
              <span className="agent-name-cell">
                <strong>{integration.name}</strong>
                <small>{integration.description || integration.id}</small>
              </span>
              <span>{integration.mcp_server_url}</span>
              <span className="owner-chip">{mcpAuthLabel(integration.mcp_auth_type)}</span>
              <span className="agent-name-cell">
                <strong>{integration.agent_name}</strong>
                <small>{integration.agent_model}</small>
              </span>
              <span className="numeric-cell">{formatDate(integration.updated_at)}</span>
            </button>
          ))}
        </div>
      )}
    </section>
  );
}

function SecretsView({
  vaults,
  loading,
  error,
  expandedVaultIds,
  credentialsByVault,
  credentialsLoadingByVault,
  onRefresh,
  onOpenCreateSecret,
  onToggleVault,
  onDeleteVault,
  onDeleteCredential,
}: {
  vaults: VaultRecord[];
  loading: boolean;
  error: string | null;
  expandedVaultIds: Set<string>;
  credentialsByVault: Record<string, VaultCredential[]>;
  credentialsLoadingByVault: Record<string, boolean>;
  onRefresh: () => void;
  onOpenCreateSecret: (vault: VaultRecord) => void;
  onToggleVault: (vaultId: string) => void;
  onDeleteVault: (vaultId: string) => void;
  onDeleteCredential: (vaultId: string, credentialId: string) => void;
}) {
  return (
    <section className="secrets-view">
      <header className="toolbar">
        <div>
          <h1>Secrets</h1>
          <p>{vaults.length} vaults</p>
        </div>
        <div className="toolbar-actions">
          <button className="icon-button" type="button" onClick={onRefresh} disabled={loading} title="Refresh vaults">
            {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
          </button>
        </div>
      </header>

      {error ? <div className="notice error">{error}</div> : null}

      {loading && vaults.length === 0 ? (
        <div className="empty-state">
          <Loader2 className="spin" size={24} aria-hidden="true" />
          <span>Loading vaults</span>
        </div>
      ) : vaults.length === 0 ? (
        <div className="empty-state">
          <LockKeyhole size={28} aria-hidden="true" />
          <strong>No vaults found</strong>
          <span>Managed vaults are provisioned automatically.</span>
        </div>
      ) : (
        <div className="vault-list" aria-label="Vaults">
          {vaults.map((vault) => {
            const expanded = expandedVaultIds.has(vault.id);
            const credentials = credentialsByVault[vault.id] ?? [];
            const credentialsLoading = Boolean(credentialsLoadingByVault[vault.id]);

            return (
              <article className={expanded ? "vault-tile expanded" : "vault-tile"} key={vault.id}>
                <button className="vault-tile-main" type="button" onClick={() => onToggleVault(vault.id)} aria-expanded={expanded}>
                  <span className="vault-expander">{expanded ? <ChevronDown size={16} aria-hidden="true" /> : <ChevronRight size={16} aria-hidden="true" />}</span>
                  <span className="agent-name-cell">
                    <strong>{vault.display_name}</strong>
                    <small>{vault.id}</small>
                  </span>
                  <span className="owner-chip">{vault.type}</span>
                  <span className="numeric-cell">{formatDate(vault.updated_at)}</span>
                </button>
                <button className="danger-button compact-button vault-delete-button" type="button" onClick={() => onDeleteVault(vault.id)}>
                  <Trash2 size={15} aria-hidden="true" />
                  Delete
                </button>

                {expanded ? (
                  <div className="credential-panel">
                    <div className="credential-panel-head">
                      <span>Credentials</span>
                      <div className="credential-panel-actions">
                        <strong>{credentialsLoading ? "Loading" : `${credentials.length}`}</strong>
                        <button className="secondary-button compact-button" type="button" onClick={() => onOpenCreateSecret(vault)}>
                          <Plus size={15} aria-hidden="true" />
                          Add secret
                        </button>
                      </div>
                    </div>
                    {credentialsLoading ? (
                      <div className="structured-empty">
                        <Loader2 className="spin" size={16} aria-hidden="true" />
                        Loading credentials
                      </div>
                    ) : credentials.length === 0 ? (
                      <div className="structured-empty">No credentials in this vault</div>
                    ) : (
                      <div className="credential-list">
                        {credentials.map((credential) => (
                          <div className="credential-row" key={credential.id}>
                            <span className="agent-name-cell">
                              <strong>{credential.display_name || credential.id}</strong>
                              <small>{credentialAuthLabel(credential.auth)}</small>
                            </span>
                            <span className="numeric-cell">{formatDate(credential.updated_at)}</span>
                            <button className="danger-button compact-button" type="button" onClick={() => onDeleteCredential(vault.id, credential.id)}>
                              <Trash2 size={15} aria-hidden="true" />
                              Delete
                            </button>
                          </div>
                        ))}
                      </div>
                    )}
                  </div>
                ) : null}
              </article>
            );
          })}
        </div>
      )}
    </section>
  );
}

function ApiKeysView({
  apiKeys,
  loading,
  saving,
  error,
  onRefresh,
  onOpenCreate,
  onRotate,
  onDelete,
  canEdit,
}: {
  apiKeys: ApiKeyRecord[];
  loading: boolean;
  saving: boolean;
  error: string | null;
  onRefresh: () => void;
  onOpenCreate: () => void;
  onRotate: (apiKey: ApiKeyRecord) => void;
  onDelete: (apiKey: ApiKeyRecord) => void;
  canEdit: boolean;
}) {
  return (
    <section className="api-keys-view">
      <header className="toolbar">
        <div>
          <h1>API Keys</h1>
          <p>{apiKeys.length} available</p>
        </div>
        <div className="toolbar-actions">
          <button className="icon-button" type="button" onClick={onRefresh} disabled={loading} title="Refresh API keys">
            {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
          </button>
          <button className="primary-button" type="button" onClick={onOpenCreate} disabled={!canEdit}>
            <Plus size={17} aria-hidden="true" />
            Create
          </button>
        </div>
      </header>

      {error ? <div className="notice error">{error}</div> : null}

      {loading && apiKeys.length === 0 ? (
        <div className="empty-state">
          <Loader2 className="spin" size={24} aria-hidden="true" />
          <span>Loading API keys</span>
        </div>
      ) : apiKeys.length === 0 ? (
        <div className="empty-state">
          <KeyRound size={28} aria-hidden="true" />
          <strong>No API keys found</strong>
          <span>Create a key for server API authentication.</span>
          <button className="primary-button" type="button" onClick={onOpenCreate} disabled={!canEdit}>
            <Plus size={16} aria-hidden="true" />
            Create
          </button>
        </div>
      ) : (
        <div className="api-key-list" aria-label="API keys">
          {apiKeys.map((apiKey) => (
            <article className="api-key-tile" key={apiKey.id}>
              <div className="api-key-main">
                <span className="api-key-icon">
                  <KeyRound size={18} aria-hidden="true" />
                </span>
                <span className="agent-name-cell">
                  <strong>{apiKey.name}</strong>
                  <small>{apiKey.key_prefix}...</small>
                </span>
                <span className="api-key-meta">
                  <small>Created</small>
                  <strong>{formatDate(apiKey.created_at)}</strong>
                </span>
                <span className="api-key-meta">
                  <small>Last used</small>
                  <strong>{apiKey.last_used_at ? formatDate(apiKey.last_used_at) : "Never"}</strong>
                </span>
                <span className="api-key-meta">
                  <small>Owner</small>
                  <strong>{apiKey.creator_email ?? apiKey.creator_uuid ?? "Unknown"}</strong>
                </span>
              </div>
              <div className="api-key-actions">
                <button className="secondary-button compact-button" type="button" onClick={() => onRotate(apiKey)} disabled={saving || !canEdit}>
                  <RefreshCw size={15} aria-hidden="true" />
                  Rotate
                </button>
                <button className="danger-button compact-button" type="button" onClick={() => onDelete(apiKey)} disabled={saving || !canEdit}>
                  <Trash2 size={15} aria-hidden="true" />
                  Delete
                </button>
              </div>
            </article>
          ))}
        </div>
      )}
    </section>
  );
}

function ProjectSettingsView({
  project,
  saving,
  error,
  onSave,
  onInviteCollaborator,
  onRemoveCollaborator,
  onDelete,
  onClose,
}: {
  project: ProjectRecord;
  saving: boolean;
  error: string | null;
  onSave: (project: ProjectRecord) => void;
  onInviteCollaborator: (project: ProjectRecord, email: string, role: ProjectCollaborator["role"]) => Promise<void>;
  onRemoveCollaborator: (project: ProjectRecord, collaborator: ProjectCollaborator) => void;
  onDelete: (project: ProjectRecord) => void;
  onClose: () => void;
}) {
  const [name, setName] = React.useState(project.name);
  const [description, setDescription] = React.useState(project.description ?? "");
  const [collaboratorEmail, setCollaboratorEmail] = React.useState("");
  const [collaboratorRole, setCollaboratorRole] = React.useState<ProjectCollaborator["role"]>("editor");
  const [collaboratorError, setCollaboratorError] = React.useState<string | null>(null);

  React.useEffect(() => {
    setName(project.name);
    setDescription(project.description ?? "");
  }, [project.id, project.name, project.description]);

  const canEdit = canEditProject(project);
  const dirty = name.trim() !== project.name || description.trim() !== (project.description ?? "");

  function submit(event: React.FormEvent) {
    event.preventDefault();
    if (!canEdit) return;
    const trimmedName = name.trim();
    if (!trimmedName) return;
    onSave({ ...project, name: trimmedName, description: description.trim() || null, is_public: false });
  }

  async function inviteCollaborator() {
    const email = collaboratorEmail.trim();
    if (!email) return;
    setCollaboratorError(null);
    try {
      await onInviteCollaborator(project, email, collaboratorRole);
      setCollaboratorEmail("");
    } catch (inviteError) {
      setCollaboratorError(errorMessage(inviteError));
    }
  }

  return (
    <section className="project-settings-view">
      <header className="toolbar">
        <div>
          <h1>Project</h1>
          <p>Manage this canvas workspace.</p>
        </div>
        <button className="icon-button" type="button" onClick={onClose} title="Back to canvas">
          <X size={16} aria-hidden="true" />
        </button>
      </header>

      {error ? <div className="notice error">{error}</div> : null}
      {!canEdit ? <div className="notice">You can view this project, but only editors can change settings.</div> : null}

      <form className="form-grid project-settings-form" onSubmit={submit}>
        <FormSection title="Details">
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} disabled={!canEdit} required />
          </label>
          <label>
            <span>Description</span>
            <textarea value={description} onChange={(event) => setDescription(event.target.value)} disabled={!canEdit} rows={3} />
          </label>
        </FormSection>
        <FormSection title="Collaborators">
          <div className="collaborator-add-row project-collaborator-add-row">
            <label>
              <span>Email</span>
              <input value={collaboratorEmail} onChange={(event) => setCollaboratorEmail(event.target.value)} disabled={!canEdit} placeholder="teammate@example.com" type="email" />
            </label>
            <label>
              <span>Role</span>
              <select value={collaboratorRole} onChange={(event) => setCollaboratorRole(event.target.value as ProjectCollaborator["role"])} disabled={!canEdit}>
                <option value="editor">Editor</option>
                <option value="viewer">Viewer</option>
              </select>
            </label>
            <button className="secondary-button compact-button" type="button" onClick={() => void inviteCollaborator()} disabled={saving || !canEdit || !collaboratorEmail.trim()}>
              <Plus size={15} aria-hidden="true" />
              Invite
            </button>
          </div>
          {collaboratorError ? <div className="notice error">{collaboratorError}</div> : null}
          {project.collaborators.length === 0 ? <div className="structured-empty">No collaborators invited</div> : null}
          {project.collaborators.map((collaborator) => (
            <div className="collaborator-row" key={collaborator.email}>
              <span>
                <strong>{collaborator.email}</strong>
                <small>{collaborator.role === "editor" ? "Editor" : "Viewer"} · {collaborator.user_uuid ? "Signed in" : "Pending sign-in"}</small>
              </span>
              <button className="icon-button row-remove-button" type="button" onClick={() => onRemoveCollaborator(project, collaborator)} disabled={saving || !canEdit} title="Remove collaborator">
                <X size={16} aria-hidden="true" />
              </button>
            </div>
          ))}
        </FormSection>
        <div className="dialog-actions">
          <button className="danger-button" type="button" onClick={() => onDelete(project)} disabled={saving || !canEdit}>
            <Trash2 size={16} aria-hidden="true" />
            Delete project
          </button>
          <button className="primary-button" type="submit" disabled={saving || !canEdit || !dirty || !name.trim()}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Save size={16} aria-hidden="true" />}
            Save
          </button>
        </div>
      </form>
    </section>
  );
}

const projectIntroExamples = [
  "Build a support workflow that triages customer requests and escalates urgent issues.",
  "Create a sales research map that enriches leads, drafts outreach, and logs follow-ups.",
  "Set up a release operations canvas that checks readiness and posts status updates.",
];

function CreateProjectIntroDialog({
  saving,
  onClose,
  onGenerate,
  onConfirm,
  onSkip,
}: {
  saving: boolean;
  onClose: () => void;
  onGenerate: (prompt: string) => Promise<GeneratedProjectPlan>;
  onConfirm: (plan: GeneratedProjectPlan) => Promise<void>;
  onSkip: () => Promise<void>;
}) {
  const [prompt, setPrompt] = React.useState("");
  const [plan, setPlan] = React.useState<GeneratedProjectPlan | null>(null);
  const [error, setError] = React.useState<string | null>(null);
  const [generating, setGenerating] = React.useState(false);

  function playExample(example: string) {
    setPrompt(example);
    setPlan(null);
    setError(null);
  }

  async function runPrompt() {
    const trimmedPrompt = prompt.trim();
    if (!trimmedPrompt) return;
    setError(null);
    setPlan(null);
    setGenerating(true);
    try {
      setPlan(await onGenerate(trimmedPrompt));
    } catch (generateError) {
      setError(errorMessage(generateError));
    } finally {
      setGenerating(false);
    }
  }

  async function confirmPlan() {
    if (!plan) return;
    setError(null);
    try {
      await onConfirm(plan);
    } catch (confirmError) {
      setError(errorMessage(confirmError));
    }
  }

  if (generating) {
    return (
      <Modal title="New Project" onClose={onClose} plainHeader>
        <div className="project-crafting">
          <div className="project-crafting-icon">
            <Bot size={42} aria-hidden="true" />
          </div>
          <strong>Crafting...</strong>
        </div>
      </Modal>
    );
  }

  if (plan) {
    return (
      <Modal title="Proposed setup" onClose={onClose} plainHeader className="project-proposal-modal">
        <div className="project-intro project-proposal">
          <div className="project-proposal-scroll">
            <GeneratedProjectPlanPreview plan={plan} />
            {error ? <div className="notice error">{error}</div> : null}
          </div>
          <div className="dialog-actions project-proposal-actions">
            <button className="secondary-button" type="button" onClick={() => setPlan(null)} disabled={saving}>
              <ChevronRight className="back-icon" size={16} aria-hidden="true" />
              Back
            </button>
            <button className="primary-button" type="button" onClick={() => void confirmPlan()} disabled={saving}>
              {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Check size={16} aria-hidden="true" />}
              Create project
            </button>
          </div>
        </div>
      </Modal>
    );
  }

  return (
    <Modal title="New Project" onClose={onClose} plainHeader>
      <div className="project-intro">
        <div className="project-intro-start">
          <div className="project-intro-label">Get started with</div>
          <div className="project-intro-examples">
            {projectIntroExamples.map((example) => (
              <div className="project-intro-example" key={example}>
                <Sparkles size={16} aria-hidden="true" />
                <span>{example}</span>
                <button className="icon-button" type="button" onClick={() => playExample(example)} title="Use prompt">
                  <Play size={16} aria-hidden="true" />
                </button>
              </div>
            ))}
          </div>
        </div>

        <div className="project-intro-prompt">
          <input value={prompt} onChange={(event) => setPrompt(event.target.value)} placeholder="Describe the agent map you want" />
          <button className="primary-button" type="button" onClick={() => void runPrompt()} disabled={saving || !prompt.trim()}>
            <Sparkles size={16} aria-hidden="true" />
            Generate
          </button>
        </div>

        {error ? <div className="notice error">{error}</div> : null}

        <button className="project-intro-skip" type="button" onClick={() => void onSkip()} disabled={saving}>
          {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : null}
          Continue with a blank project.
        </button>
      </div>
    </Modal>
  );
}

function GeneratedProjectPlanPreview({ plan }: { plan: GeneratedProjectPlan }) {
  const childIdsByAgent = React.useMemo(() => {
    const result = new Map<string, string[]>();
    for (const connection of plan.connections) {
      if (connection.type !== "sub_agent") continue;
      result.set(connection.from, [...(result.get(connection.from) ?? []), connection.to]);
    }
    return result;
  }, [plan.connections]);
  const rootAgents = React.useMemo(() => {
    const childIds = new Set(Array.from(childIdsByAgent.values()).flat());
    const roots = plan.agents.filter((agent) => !childIds.has(agent.id));
    return roots.length > 0 ? roots : plan.agents;
  }, [childIdsByAgent, plan.agents]);
  const [expandedAgentIds, setExpandedAgentIds] = React.useState<Set<string>>(() => new Set());

  function toggleAgent(agentId: string) {
    setExpandedAgentIds((current) => {
      const next = new Set(current);
      if (next.has(agentId)) {
        next.delete(agentId);
      } else {
        next.add(agentId);
      }
      return next;
    });
  }

  return (
    <div className="generated-plan-preview">
      <h3 className="generated-plan-section-title">Canvas</h3>
      <div className="generated-plan-head">
        <strong>{plan.project.name}</strong>
        <span>{plan.project.description}</span>
      </div>
      {plan.triggers?.length > 0 ? (
        <>
          <h3 className="generated-plan-section-title">Triggers</h3>
          <div className="generated-agent-details">
            {plan.triggers.map((trigger) => (
              <InfoRow icon={generatedTriggerIcon(trigger.type)} label={generatedTriggerTypeLabel(trigger.type)} value={`${trigger.name}${trigger.description ? ` - ${trigger.description}` : ""}`} key={trigger.id} />
            ))}
          </div>
        </>
      ) : null}
      <h3 className="generated-plan-section-title">Agents</h3>
      <div className="generated-agent-tree" role="tree" aria-label="Proposed agents">
        {rootAgents.map((agent) => (
          <GeneratedAgentTreeItem
            agent={agent}
            depth={0}
            expandedAgentIds={expandedAgentIds}
            childIdsByAgent={childIdsByAgent}
            plan={plan}
            onToggle={toggleAgent}
            ancestorIds={new Set()}
            key={agent.id}
          />
        ))}
      </div>
    </div>
  );
}

function generatedTriggerTypeLabel(type: GeneratedProjectPlan["triggers"][number]["type"]): string {
  if (type === "play") return "Play";
  if (type === "schedule") return "Schedule";
  if (type === "slack") return "Slack";
  if (type === "api") return "API";
  return "Email";
}

function generatedTriggerIcon(type: GeneratedProjectPlan["triggers"][number]["type"]): React.ReactNode {
  if (type === "play") return <Play size={15} />;
  if (type === "schedule") return <Calendar size={15} />;
  if (type === "slack") return <MessageSquare size={15} />;
  if (type === "api") return <KeyRound size={15} />;
  return <Mail size={15} />;
}

function GeneratedAgentTreeItem({
  agent,
  depth,
  expandedAgentIds,
  childIdsByAgent,
  plan,
  onToggle,
  ancestorIds,
}: {
  agent: GeneratedProjectPlan["agents"][number];
  depth: number;
  expandedAgentIds: Set<string>;
  childIdsByAgent: Map<string, string[]>;
  plan: GeneratedProjectPlan;
  onToggle: (agentId: string) => void;
  ancestorIds: Set<string>;
}) {
  const expanded = expandedAgentIds.has(agent.id);
  const childAgents = (childIdsByAgent.get(agent.id) ?? []).flatMap((childId) => {
    if (ancestorIds.has(childId) || childId === agent.id) return [];
    const childAgent = plan.agents.find((candidate) => candidate.id === childId);
    return childAgent ? [childAgent] : [];
  });
  const mcps = plan.connections
    .filter((connection) => connection.type === "uses_mcp" && connection.from === agent.id)
    .flatMap((connection) => {
      const mcp = plan.mcps.find((candidate) => candidate.id === connection.to);
      return mcp ? [mcp] : [];
    });
  const skillIds = new Set([...(agent.skill_ids ?? []), ...plan.connections.filter((connection) => connection.type === "uses_skill" && connection.from === agent.id).map((connection) => connection.to)]);
  const skills = plan.skills.filter((skill) => skillIds.has(skill.id));
  const nextAncestorIds = new Set([...ancestorIds, agent.id]);

  return (
    <div className="generated-agent-tree-row" style={{ "--tree-depth": depth } as React.CSSProperties} role="treeitem" aria-expanded={expanded}>
      <button className="generated-agent-tile" type="button" onClick={() => onToggle(agent.id)}>
        <ChevronRight className={expanded ? "tree-chevron expanded" : "tree-chevron"} size={16} aria-hidden="true" />
        <span>
          <strong>{agent.name}</strong>
          <small>{agent.description}</small>
        </span>
      </button>
      {expanded ? (
        <div className="generated-agent-details">
          <InfoRow icon={<Bot size={15} />} label="Name" value={agent.name} />
          <InfoRow icon={<Info size={15} />} label="Description" value={agent.description || "None"} />
          <InfoRow icon={<Server size={15} />} label="Model" value={agent.model || defaultAgentModel} />
          <div className="generated-agent-field boxed">
            <span>System prompt</span>
            <p>{agent.system_prompt}</p>
          </div>
          <div className="generated-agent-field boxed">
            <span>MCPs</span>
            {mcps.length === 0 ? <p>None</p> : <p>{mcps.map((mcp) => mcp.name).join(", ")}</p>}
          </div>
          <div className="generated-agent-field boxed">
            <span>Skills</span>
            {skills.length === 0 ? <p>None</p> : <p>{skills.map((skill) => skill.name).join(", ")}</p>}
          </div>
        </div>
      ) : null}
      {childAgents.length > 0 ? (
        <div className="generated-agent-children" role="group">
          {childAgents.map((childAgent) => (
            <GeneratedAgentTreeItem
              agent={childAgent}
              depth={depth + 1}
              expandedAgentIds={expandedAgentIds}
              childIdsByAgent={childIdsByAgent}
              plan={plan}
              onToggle={onToggle}
              ancestorIds={nextAncestorIds}
              key={childAgent.id}
            />
          ))}
        </div>
      ) : null}
    </div>
  );
}

function ApiKeyRevealToast({ revealedApiKey, onClose }: { revealedApiKey: { name: string; key: string }; onClose: () => void }) {
  const [copied, setCopied] = React.useState(false);

  async function copyKey() {
    await navigator.clipboard.writeText(revealedApiKey.key);
    setCopied(true);
    window.setTimeout(() => setCopied(false), 1400);
  }

  return (
    <div className="api-key-toast" role="status" aria-live="polite">
      <div>
        <strong>{revealedApiKey.name}</strong>
        <span>Store this key now. It will not be shown again.</span>
      </div>
      <input className="code-input" value={revealedApiKey.key} readOnly onFocus={(event) => event.currentTarget.select()} aria-label="New API key" />
      <div className="api-key-toast-actions">
        <button className="secondary-button compact-button" type="button" onClick={() => void copyKey()}>
          <Copy size={15} aria-hidden="true" />
          {copied ? "Copied" : "Copy"}
        </button>
        <button className="icon-button" type="button" onClick={onClose} title="Close">
          <X size={16} aria-hidden="true" />
        </button>
      </div>
    </div>
  );
}

function MembersView({
  members,
  auth,
  loading,
  error,
  savingRoleUuid,
  onRefresh,
  onRoleChange,
}: {
  members: Member[];
  auth: AuthSession;
  loading: boolean;
  error: string | null;
  savingRoleUuid: string | null;
  onRefresh: () => void;
  onRoleChange: (member: Member, role: WorkspaceRole) => void;
}) {
  const localMember = members.find((member) => member.uuid === auth.uuid) ?? { uuid: auth.uuid, email: auth.email, role: auth.role ?? "member" };
  const orderedMembers = [localMember, ...members.filter((member) => member.uuid !== auth.uuid)];
  const canManageRoles = localMember.role === "admin";

  return (
    <section className="members-view">
      <header className="toolbar">
        <div>
          <h1>Members</h1>
          <p>{members.length} signed in</p>
        </div>
        <div className="toolbar-actions">
          <button className="icon-button" type="button" onClick={onRefresh} disabled={loading} title="Refresh members">
            {loading ? <Loader2 className="spin" size={18} aria-hidden="true" /> : <RefreshCw size={18} aria-hidden="true" />}
          </button>
        </div>
      </header>

      {error ? <div className="notice error">{error}</div> : null}

      {loading && orderedMembers.length === 0 ? (
        <div className="empty-state">
          <Loader2 className="spin" size={24} aria-hidden="true" />
          <span>Loading members</span>
        </div>
      ) : (
        <div className="member-table" role="table" aria-label="Members">
          <div className="member-table-head" role="row">
            <span>Member</span>
            <span>Role</span>
          </div>
          {orderedMembers.map((member) => {
            const isLocal = member.uuid === auth.uuid;
            return (
              <article className={isLocal ? "member-row local" : "member-row"} key={member.uuid} role="row">
                <span className="agent-name-cell">
                  <strong>
                    {member.email}
                    {isLocal ? <small className="inline-you-label"> (You)</small> : null}
                  </strong>
                  <small>{member.uuid}</small>
                </span>
                <span className="member-role-cell">
                  {canManageRoles ? (
                    <select
                      value={member.role}
                      onChange={(event) => onRoleChange(member, event.target.value as WorkspaceRole)}
                      disabled={savingRoleUuid === member.uuid}
                      aria-label={`Role for ${member.email}`}
                    >
                      <option value="admin">Admin</option>
                      <option value="member">Member</option>
                    </select>
                  ) : (
                    <strong>{workspaceRoleLabel(member.role)}</strong>
                  )}
                </span>
              </article>
            );
          })}
          {orderedMembers.length === 1 && members.length === 0 ? (
            <article className="member-row muted" role="row">
              <span className="agent-name-cell">
                <strong>No other members found</strong>
              </span>
              <span />
            </article>
          ) : null}
        </div>
      )}
    </section>
  );
}

function CreateVaultDialog({
  saving,
  onClose,
  onCreate,
}: {
  saving: boolean;
  onClose: () => void;
  onCreate: (payload: { display_name: string }) => Promise<void>;
}) {
  const [name, setName] = React.useState("");
  const [formError, setFormError] = React.useState<string | null>(null);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setFormError(null);

    try {
      const displayName = name.trim();
      if (!displayName) throw new Error("Vault name is required.");
      await onCreate({ display_name: displayName });
    } catch (submitError) {
      setFormError(errorMessage(submitError));
    }
  }

  return (
    <Modal title="Create vault" onClose={onClose}>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="Basics">
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} required />
          </label>
        </FormSection>
        {formError ? <div className="notice error">{formError}</div> : null}
        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={onClose}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
            Create
          </button>
        </div>
      </form>
    </Modal>
  );
}

function CreateApiKeyDialog({
  saving,
  onClose,
  onCreate,
  side,
}: {
  saving: boolean;
  onClose: () => void;
  onCreate: (name: string) => Promise<void>;
  side?: boolean;
}) {
  const [name, setName] = React.useState("");
  const [formError, setFormError] = React.useState<string | null>(null);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setFormError(null);

    try {
      const trimmedName = name.trim();
      if (!trimmedName) throw new Error("API key name is required.");
      await onCreate(trimmedName);
    } catch (submitError) {
      setFormError(errorMessage(submitError));
    }
  }

  return (
    <Modal title="Create API key" onClose={onClose} side={side}>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="Basics">
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} required autoFocus />
          </label>
        </FormSection>
        {formError ? <div className="notice error">{formError}</div> : null}
        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={onClose}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
            Create
          </button>
        </div>
      </form>
    </Modal>
  );
}

function CreateEmailReceiverDialog({
  saving,
  onClose,
  onCreate,
  side,
}: {
  saving: boolean;
  onClose: () => void;
  onCreate: (name: string) => Promise<void>;
  side?: boolean;
}) {
  const [name, setName] = React.useState("");
  const [formError, setFormError] = React.useState<string | null>(null);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setFormError(null);

    try {
      const trimmedName = name.trim().toLowerCase();
      if (!trimmedName) throw new Error("Receiver name is required.");
      await onCreate(trimmedName);
    } catch (submitError) {
      setFormError(errorMessage(submitError));
    }
  }

  return (
    <Modal title="Create email receiver" onClose={onClose} side={side}>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="Receiver">
          <label>
            <span>Name</span>
            <input
              value={name}
              onChange={(event) => setName(event.target.value.toLowerCase())}
              required
              autoFocus
              placeholder="bob"
              pattern="[a-z0-9][a-z0-9._-]*[a-z0-9]|[a-z0-9]"
            />
          </label>
        </FormSection>
        {formError ? <div className="notice error">{formError}</div> : null}
        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={onClose}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
            Create
          </button>
        </div>
      </form>
    </Modal>
  );
}

function CreateSkillDialog({
  projects,
  selectedProjectId,
  saving,
  onClose,
  onCreate,
}: {
  projects: ProjectRecord[];
  selectedProjectId: string | null;
  saving: boolean;
  onClose: () => void;
  onCreate: (payload: { name: string; description: string; files: File[]; publicUrl: string; projectIds: string[] }) => Promise<void>;
}) {
  const [name, setName] = React.useState("");
  const [description, setDescription] = React.useState("");
  const [global, setGlobal] = React.useState(false);
  const [projectIds, setProjectIds] = React.useState<string[]>(selectedProjectId ? [selectedProjectId] : []);
  const [projectsOpen, setProjectsOpen] = React.useState(false);
  const [publicUrl, setPublicUrl] = React.useState("");
  const [files, setFiles] = React.useState<File[]>([]);
  const [error, setError] = React.useState<string | null>(null);
  const fileInputRef = React.useRef<HTMLInputElement | null>(null);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    if (!name.trim()) {
      setError("Skill name is required.");
      return;
    }
    if (!description.trim()) {
      setError("Skill description is required.");
      return;
    }
    if (!global && projectIds.length === 0) {
      setError("Select at least one project or make this skill global.");
      return;
    }
    setError(null);
    try {
      await onCreate({ name, description, files, publicUrl, projectIds: global ? [] : projectIds });
    } catch (createError) {
      setError(errorMessage(createError));
    }
  }

  function toggleProject(projectId: string, enabled: boolean) {
    setProjectIds((current) => (enabled ? uniqueStrings([...current, projectId]) : current.filter((id) => id !== projectId)));
  }

  return (
    <Modal title="Create skill" onClose={onClose} side>
      <form className="form-grid" onSubmit={submit}>
        <label>
          <span>Name</span>
          <input value={name} onChange={(event) => setName(event.target.value)} placeholder="Brand research" required />
        </label>
        <label>
          <span>Description</span>
          <textarea value={description} onChange={(event) => setDescription(event.target.value)} placeholder="When to use this skill and what it should do" rows={4} required />
        </label>
        <CollapsibleSection title="Projects" open={projectsOpen} onToggle={() => setProjectsOpen((value) => !value)}>
          <label className="toggle-row">
            <input type="checkbox" checked={global} onChange={(event) => setGlobal(event.target.checked)} />
            <span>Available globally</span>
          </label>
          {!global ? (
            <div className="project-checkbox-list">
              {projects.length === 0 ? <div className="structured-empty">No projects available</div> : null}
              {projects.map((project) => (
                <label className="checkbox-row" key={project.id}>
                  <input type="checkbox" checked={projectIds.includes(project.id)} onChange={(event) => toggleProject(project.id, event.target.checked)} />
                  <span>{project.name}</span>
                </label>
              ))}
            </div>
          ) : null}
        </CollapsibleSection>
        <label>
          <span>Public URL</span>
          <input value={publicUrl} onChange={(event) => setPublicUrl(event.target.value)} placeholder="https://example.com/skill.zip" inputMode="url" />
        </label>
        <div className="skill-upload-field">
          <span>Upload</span>
          <input
            ref={fileInputRef}
            className="visually-hidden-file"
            type="file"
            multiple
            onChange={(event) => setFiles(Array.from(event.target.files ?? []))}
            {...({ webkitdirectory: "true" } as Record<string, string>)}
          />
          <button className="secondary-button" type="button" onClick={() => fileInputRef.current?.click()}>
            <Upload size={16} aria-hidden="true" />
            {files.length > 0 ? `${files.length} files selected` : "Upload skill"}
          </button>
        </div>

        {error ? <div className="notice error">{error}</div> : null}

        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={onClose} disabled={saving}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
            Create
          </button>
        </div>
      </form>
    </Modal>
  );
}

function SkillDetailsDialog({
  skill,
  saving,
  onClose,
  onSaveMetadata,
  onCreateVersion,
  projects,
  selectedProjectId,
}: {
  skill: SkillRecord;
  saving: boolean;
  onClose: () => void;
  onSaveMetadata: (payload: { name: string; description: string; projectIds: string[] }) => Promise<void>;
  onCreateVersion: (payload: { files: File[]; publicUrl: string }) => Promise<void>;
  projects: ProjectRecord[];
  selectedProjectId: string | null;
}) {
  const isBuiltInSkill = skill.source === "anthropic";
  const initialProjectIds = skillProjectIds(skill);
  const fallbackProjectIds = initialProjectIds.length > 0 ? initialProjectIds : selectedProjectId ? [selectedProjectId] : [];
  const [name, setName] = React.useState(skill.display_title ?? "");
  const [description, setDescription] = React.useState(skill.description ?? "");
  const [global, setGlobal] = React.useState(skillIsGlobal(skill));
  const [projectIds, setProjectIds] = React.useState<string[]>(fallbackProjectIds);
  const [projectsOpen, setProjectsOpen] = React.useState(false);
  const [publicUrl, setPublicUrl] = React.useState("");
  const [files, setFiles] = React.useState<File[]>([]);
  const [newVersionOpen, setNewVersionOpen] = React.useState(false);
  const [detailsOpen, setDetailsOpen] = React.useState(false);
  const [error, setError] = React.useState<string | null>(null);
  const fileInputRef = React.useRef<HTMLInputElement | null>(null);
  const metadataDirty =
    name.trim() !== (skill.display_title ?? "") ||
    description.trim() !== (skill.description ?? "") ||
    (!isBuiltInSkill && (global !== skillIsGlobal(skill) || JSON.stringify(projectIds) !== JSON.stringify(fallbackProjectIds)));
  const versionDirty = !isBuiltInSkill && (files.length > 0 || Boolean(publicUrl.trim()));
  const canSave = metadataDirty || versionDirty;

  React.useEffect(() => {
    setName(skill.display_title ?? "");
    setDescription(skill.description ?? "");
    setGlobal(skillIsGlobal(skill));
    setProjectIds(fallbackProjectIds);
  }, [skill.id, skill.display_title, skill.description]);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setError(null);
    try {
      if (!name.trim()) throw new Error("Skill name is required.");
      if (!isBuiltInSkill && !global && projectIds.length === 0) throw new Error("Select at least one project or make this skill global.");
      if (!canSave) return;
      if (metadataDirty) await onSaveMetadata({ name, description, projectIds: isBuiltInSkill || global ? [] : projectIds });
      if (versionDirty) {
        await onCreateVersion({ files, publicUrl });
        setFiles([]);
        setPublicUrl("");
      }
    } catch (updateError) {
      setError(errorMessage(updateError));
    }
  }

  function toggleProject(projectId: string, enabled: boolean) {
    setProjectIds((current) => (enabled ? uniqueStrings([...current, projectId]) : current.filter((id) => id !== projectId)));
  }

  return (
    <Modal title="Skill details" onClose={onClose} side>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="Skill">
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} required />
          </label>
          <label>
            <span>Description</span>
            <textarea value={description} onChange={(event) => setDescription(event.target.value)} rows={4} />
          </label>
        </FormSection>

        {isBuiltInSkill ? null : (
          <CollapsibleSection title="Projects" open={projectsOpen} onToggle={() => setProjectsOpen((value) => !value)}>
            <label className="toggle-row">
              <input type="checkbox" checked={global} onChange={(event) => setGlobal(event.target.checked)} />
              <span>Available globally</span>
            </label>
            {!global ? (
              <div className="project-checkbox-list">
                {projects.length === 0 ? <div className="structured-empty">No projects available</div> : null}
                {projects.map((project) => (
                  <label className="checkbox-row" key={project.id}>
                    <input type="checkbox" checked={projectIds.includes(project.id)} onChange={(event) => toggleProject(project.id, event.target.checked)} />
                    <span>{project.name}</span>
                  </label>
                ))}
              </div>
            ) : null}
          </CollapsibleSection>
        )}

        {isBuiltInSkill ? null : (
          <CollapsibleSection title="New version" open={newVersionOpen} onToggle={() => setNewVersionOpen((value) => !value)}>
            <label>
              <span>Public URL</span>
              <input value={publicUrl} onChange={(event) => setPublicUrl(event.target.value)} placeholder="https://example.com/skill.zip" inputMode="url" />
            </label>
            <div className="skill-upload-field">
              <span>Upload</span>
              <input
                ref={fileInputRef}
                className="visually-hidden-file"
                type="file"
                multiple
                onChange={(event) => setFiles(Array.from(event.target.files ?? []))}
                {...({ webkitdirectory: "true" } as Record<string, string>)}
              />
              <button className="secondary-button" type="button" onClick={() => fileInputRef.current?.click()}>
                <Upload size={16} aria-hidden="true" />
                {files.length > 0 ? `${files.length} files selected` : "Upload skill"}
              </button>
            </div>
          </CollapsibleSection>
        )}

        <CollapsibleSection title="Details" open={detailsOpen} onToggle={() => setDetailsOpen((value) => !value)}>
          <div className="details-info-grid">
            <InfoRow icon={<Info size={15} />} label="Skill ID" value={skill.id} />
            <InfoRow icon={<Archive size={15} />} label="Source" value={skill.source} />
            <InfoRow icon={<Calendar size={15} />} label="Version" value={isBuiltInSkill && !skill.latest_version ? "Built-in" : (skill.latest_version ?? "No version")} />
          </div>
        </CollapsibleSection>

        {error ? <div className="notice error">{error}</div> : null}

        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={onClose} disabled={saving}>
            <X size={16} aria-hidden="true" />
            Close
          </button>
          <button className="primary-button" type="submit" disabled={saving || !canSave}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Save size={16} aria-hidden="true" />}
            Save
          </button>
        </div>
      </form>
    </Modal>
  );
}

function CreateSecretDialog({
  vault,
  saving,
  onClose,
  onCreate,
}: {
  vault: VaultRecord;
  saving: boolean;
  onClose: () => void;
  onCreate: (payload: JsonObject) => Promise<void>;
}) {
  const [kind, setKind] = React.useState<SecretKind>("static_bearer");
  const [displayName, setDisplayName] = React.useState("");
  const [mcpServerUrl, setMcpServerUrl] = React.useState("");
  const [token, setToken] = React.useState("");
  const [secretName, setSecretName] = React.useState("");
  const [secretValue, setSecretValue] = React.useState("");
  const [allowedHosts, setAllowedHosts] = React.useState("");
  const [formError, setFormError] = React.useState<string | null>(null);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setFormError(null);

    try {
      const payload: JsonObject = {
        display_name: displayName.trim() || null,
        auth:
          kind === "static_bearer"
            ? {
                type: "static_bearer",
                mcp_server_url: mcpServerUrl.trim(),
                token,
              }
            : {
                type: "environment_variable",
                secret_name: secretName.trim(),
                secret_value: secretValue,
                allowed_hosts: allowedHosts
                  .split(",")
                  .map((host) => host.trim())
                  .filter(Boolean),
              },
      };
      await onCreate(payload);
    } catch (submitError) {
      setFormError(errorMessage(submitError));
    }
  }

  return (
    <Modal title="Add secret" onClose={onClose}>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title={vault.display_name}>
          <label>
            <span>Type</span>
            <select value={kind} onChange={(event) => setKind(event.target.value as SecretKind)}>
              <option value="static_bearer">Static bearer</option>
              <option value="environment_variable">Environment variable</option>
            </select>
          </label>
          <label>
            <span>Display name</span>
            <input value={displayName} onChange={(event) => setDisplayName(event.target.value)} placeholder="Optional" />
          </label>
        </FormSection>

        {kind === "static_bearer" ? (
          <FormSection title="Bearer credential">
            <label>
              <span>MCP server URL</span>
              <input value={mcpServerUrl} onChange={(event) => setMcpServerUrl(event.target.value)} placeholder="https://example.com/mcp/" required />
            </label>
            <label>
              <span>Bearer token</span>
              <input value={token} onChange={(event) => setToken(event.target.value)} type="password" required />
            </label>
          </FormSection>
        ) : (
          <FormSection title="Environment variable">
            <label>
              <span>Secret name</span>
              <input value={secretName} onChange={(event) => setSecretName(event.target.value)} placeholder="API_KEY" required />
            </label>
            <label>
              <span>Secret value</span>
              <input value={secretValue} onChange={(event) => setSecretValue(event.target.value)} type="password" required />
            </label>
            <label>
              <span>Allowed hosts</span>
              <input value={allowedHosts} onChange={(event) => setAllowedHosts(event.target.value)} placeholder="api.example.com, *.example.com" />
            </label>
          </FormSection>
        )}

        {formError ? <div className="notice error">{formError}</div> : null}
        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={onClose}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
            Add secret
          </button>
        </div>
      </form>
    </Modal>
  );
}

function CreateMcpServerDialog({
  server,
  projects,
  selectedProjectId,
  saving,
  onClose,
  onSubmit,
  onDelete,
  side,
}: {
  server?: RegisteredMcpServer;
  projects: ProjectRecord[];
  selectedProjectId: string | null;
  saving: boolean;
  onClose: () => void;
  onSubmit: (payload: JsonObject) => Promise<void>;
  onDelete?: () => Promise<void>;
  side?: boolean;
}) {
  const editing = Boolean(server);
  const initialProjectIds = server
    ? server.project_ids.length > 0
      ? server.project_ids
      : selectedProjectId
        ? [selectedProjectId]
        : []
    : selectedProjectId
      ? [selectedProjectId]
      : [];
  const [name, setName] = React.useState(server?.name ?? "");
  const [description, setDescription] = React.useState(server?.description ?? "");
  const [url, setUrl] = React.useState(server?.url ?? "");
  const [iconDataUrl, setIconDataUrl] = React.useState<string | null>(server?.icon_data_url ?? null);
  const [global, setGlobal] = React.useState(server ? mcpServerIsGlobal(server) : false);
  const [projectIds, setProjectIds] = React.useState<string[]>(initialProjectIds);
  const [authKind, setAuthKind] = React.useState<McpAuthEditKind>(editing ? "unchanged" : "no_auth");
  const [projectsOpen, setProjectsOpen] = React.useState(false);
  const [authenticationOpen, setAuthenticationOpen] = React.useState(false);
  const [token, setToken] = React.useState("");
  const [environmentVariables, setEnvironmentVariables] = React.useState<Array<{ id: string; secretName: string; secretValue: string }>>([
    { id: crypto.randomUUID(), secretName: "", secretValue: "" },
  ]);
  const [formError, setFormError] = React.useState<string | null>(null);
  const [confirmDelete, setConfirmDelete] = React.useState(false);

  function updateEnvironmentVariable(id: string, patch: Partial<{ secretName: string; secretValue: string }>) {
    setEnvironmentVariables((current) => current.map((variable) => (variable.id === id ? { ...variable, ...patch } : variable)));
  }

  function removeEnvironmentVariable(id: string) {
    setEnvironmentVariables((current) => (current.length === 1 ? current : current.filter((variable) => variable.id !== id)));
  }

  function addEnvironmentVariable() {
    setEnvironmentVariables((current) => [...current, { id: crypto.randomUUID(), secretName: "", secretValue: "" }]);
  }

  function toggleProject(projectId: string, enabled: boolean) {
    setProjectIds((current) => (enabled ? uniqueStrings([...current, projectId]) : current.filter((id) => id !== projectId)));
  }

  async function uploadIcon(file: File | undefined) {
    if (!file) return;
    setFormError(null);
    try {
      setIconDataUrl(await readIconFile(file));
    } catch (uploadError) {
      setFormError(errorMessage(uploadError));
    }
  }

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setFormError(null);

    try {
      const payload: JsonObject = {
        name: name.trim(),
        description: nullableText(description),
        url: url.trim(),
        icon_data_url: iconDataUrl,
        project_ids: global ? [] : projectIds,
      };
      if (!global && projectIds.length === 0) {
        throw new Error("Select at least one project or make this MCP global.");
      }
      if (authKind !== "unchanged") {
        payload.auth =
          authKind === "no_auth"
            ? { type: "no_auth" }
            : authKind === "static_bearer"
              ? { type: "static_bearer", token }
              : {
                  type: "environment_variable",
                  variables: environmentVariables.map((variable) => ({
                    secret_name: variable.secretName.trim(),
                    secret_value: variable.secretValue,
                  })),
                };
      }
      await onSubmit(payload);
    } catch (submitError) {
      setFormError(errorMessage(submitError));
    }
  }

  return (
    <Modal title={editing ? "Edit MCP server" : "Add MCP server"} onClose={onClose} side={side}>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="Basics">
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} required />
          </label>
          <label>
            <span>Description</span>
            <input value={description} onChange={(event) => setDescription(event.target.value)} />
          </label>
          <label>
            <span>URL</span>
            <input value={url} onChange={(event) => setUrl(event.target.value)} placeholder="https://example.com/mcp" required />
          </label>
          <div className="icon-upload-field">
            <span>Icon</span>
            <div className="icon-upload-row">
              <div className="mcp-icon-preview" aria-label="MCP icon preview">
                {iconDataUrl ? <img src={iconDataUrl} alt="" /> : <ImageIcon size={24} aria-hidden="true" />}
              </div>
              <label className="secondary-button compact-button">
                <Upload size={15} aria-hidden="true" />
                Upload
                <input
                  className="visually-hidden-file"
                  type="file"
                  accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml"
                  onChange={(event) => {
                    void uploadIcon(event.target.files?.[0]);
                    event.target.value = "";
                  }}
                />
              </label>
              {iconDataUrl ? (
                <button className="secondary-button compact-button" type="button" onClick={() => setIconDataUrl(null)}>
                  <X size={15} aria-hidden="true" />
                  Remove
                </button>
              ) : null}
            </div>
          </div>
        </FormSection>

        <CollapsibleSection title="Projects" open={projectsOpen} onToggle={() => setProjectsOpen((value) => !value)}>
          <label className="toggle-row">
            <input type="checkbox" checked={global} onChange={(event) => setGlobal(event.target.checked)} />
            <span>Available globally</span>
          </label>
          {!global ? (
            <div className="project-checkbox-list">
              {projects.length === 0 ? <div className="structured-empty">No projects available</div> : null}
              {projects.map((project) => (
                <label className="checkbox-row" key={project.id}>
                  <input type="checkbox" checked={projectIds.includes(project.id)} onChange={(event) => toggleProject(project.id, event.target.checked)} />
                  <span>{project.name}</span>
                </label>
              ))}
            </div>
          ) : null}
        </CollapsibleSection>

        <CollapsibleSection title="Authentication" open={authenticationOpen} onToggle={() => setAuthenticationOpen((value) => !value)}>
          <label>
            <span>Type</span>
            <select value={authKind} onChange={(event) => setAuthKind(event.target.value as McpAuthEditKind)}>
              {editing ? <option value="unchanged">Keep current auth ({mcpAuthLabel(server?.auth_type ?? "no_auth")})</option> : null}
              <option value="no_auth">No auth</option>
              <option value="static_bearer">Static bearer</option>
              <option value="environment_variable">Environment value</option>
            </select>
          </label>
          {authKind === "static_bearer" ? (
            <label>
              <span>Bearer token</span>
              <input value={token} onChange={(event) => setToken(event.target.value)} type="password" required />
            </label>
          ) : authKind === "environment_variable" ? (
            <div className="structured-editor">
              <div className="structured-editor-head">
                <span>Environment variables</span>
                <button className="secondary-button compact-button" type="button" onClick={addEnvironmentVariable}>
                  <Plus size={15} aria-hidden="true" />
                  Add variable
                </button>
              </div>
              {environmentVariables.map((variable) => (
                <div className="structured-row mcp-env-row" key={variable.id}>
                  <label>
                    <span>Name</span>
                    <input value={variable.secretName} onChange={(event) => updateEnvironmentVariable(variable.id, { secretName: event.target.value })} placeholder="API_KEY" required />
                  </label>
                  <label>
                    <span>Value</span>
                    <input value={variable.secretValue} onChange={(event) => updateEnvironmentVariable(variable.id, { secretValue: event.target.value })} type="password" required />
                  </label>
                  <button className="icon-button row-remove-button" type="button" onClick={() => removeEnvironmentVariable(variable.id)} disabled={environmentVariables.length === 1} title="Remove variable">
                    <X size={16} aria-hidden="true" />
                  </button>
                </div>
              ))}
            </div>
          ) : null}
        </CollapsibleSection>

        {formError ? <div className="notice error">{formError}</div> : null}

        <div className="dialog-actions">
          {onDelete ? (
            confirmDelete ? (
              <button className="danger-button" type="button" onClick={onDelete} disabled={saving}>
                <Trash2 size={16} aria-hidden="true" />
                Confirm remove
              </button>
            ) : (
              <button className="danger-button" type="button" onClick={() => setConfirmDelete(true)} disabled={saving}>
                <Trash2 size={16} aria-hidden="true" />
                Remove
              </button>
            )
          ) : null}
          <button className="secondary-button" type="button" onClick={onClose}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : editing ? <Save size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
            {editing ? "Save" : "Add"}
          </button>
        </div>
      </form>
    </Modal>
  );
}

function IntegrationTemplateDialog({
  integration,
  saving,
  onClose,
  onSubmit,
  onDelete,
}: {
  integration?: IntegrationTemplate;
  saving: boolean;
  onClose: () => void;
  onSubmit: (payload: JsonObject) => Promise<void>;
  onDelete?: () => Promise<void>;
}) {
  const editing = Boolean(integration);
  const [logoDataUrl, setLogoDataUrl] = React.useState<string | null>(integration?.logo_data_url ?? null);
  const [name, setName] = React.useState(integration?.name ?? "");
  const [description, setDescription] = React.useState(integration?.description ?? "");
  const [mcpServerUrl, setMcpServerUrl] = React.useState(integration?.mcp_server_url ?? "");
  const [mcpAuthType, setMcpAuthType] = React.useState<SecretKind>(integration?.mcp_auth_type ?? "static_bearer");
  const [agentName, setAgentName] = React.useState(integration?.agent_name ?? "");
  const [agentDescription, setAgentDescription] = React.useState(integration?.agent_description ?? "");
  const [agentSystemPrompt, setAgentSystemPrompt] = React.useState(integration?.agent_system_prompt ?? "");
  const [agentModel, setAgentModel] = React.useState(integration?.agent_model ?? defaultAgentModel);
  const [formError, setFormError] = React.useState<string | null>(null);
  const [confirmDelete, setConfirmDelete] = React.useState(false);

  async function uploadLogo(file: File | undefined) {
    if (!file) return;
    setFormError(null);
    try {
      setLogoDataUrl(await readIconFile(file));
    } catch (uploadError) {
      setFormError(errorMessage(uploadError));
    }
  }

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setFormError(null);
    try {
      await onSubmit({
        logo_data_url: logoDataUrl,
        name: name.trim(),
        description: nullableText(description),
        mcp_server_url: mcpServerUrl.trim(),
        mcp_auth_type: mcpAuthType,
        agent_name: agentName.trim(),
        agent_description: nullableText(agentDescription),
        agent_system_prompt: nullableText(agentSystemPrompt),
        agent_model: agentModel.trim(),
      });
    } catch (submitError) {
      setFormError(errorMessage(submitError));
    }
  }

  return (
    <Modal title={editing ? "Edit integration" : "Add integration"} onClose={onClose}>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="General info">
          <div className="icon-upload-field">
            <span>Logo image</span>
            <div className="icon-upload-row">
              <div className="mcp-icon-preview" aria-label="Integration logo preview">
                {logoDataUrl ? <img src={logoDataUrl} alt="" /> : <Puzzle size={24} aria-hidden="true" />}
              </div>
              <label className="secondary-button compact-button">
                <Upload size={15} aria-hidden="true" />
                Upload
                <input
                  className="visually-hidden-file"
                  type="file"
                  accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml"
                  onChange={(event) => {
                    void uploadLogo(event.target.files?.[0]);
                    event.target.value = "";
                  }}
                />
              </label>
              {logoDataUrl ? (
                <button className="secondary-button compact-button" type="button" onClick={() => setLogoDataUrl(null)}>
                  <X size={15} aria-hidden="true" />
                  Remove
                </button>
              ) : null}
            </div>
          </div>
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} required />
          </label>
          <label>
            <span>Description</span>
            <input value={description} onChange={(event) => setDescription(event.target.value)} />
          </label>
        </FormSection>

        <FormSection title="MCP server">
          <label>
            <span>MCP server URL</span>
            <input value={mcpServerUrl} onChange={(event) => setMcpServerUrl(event.target.value)} placeholder="https://example.com/mcp" required />
          </label>
          <label>
            <span>Authentication</span>
            <select value={mcpAuthType} onChange={(event) => setMcpAuthType(event.target.value as SecretKind)}>
              <option value="static_bearer">Static bearer</option>
              <option value="environment_variable">Environment variable</option>
            </select>
          </label>
        </FormSection>

        <FormSection title="Agent">
          <label>
            <span>Name</span>
            <input value={agentName} onChange={(event) => setAgentName(event.target.value)} required />
          </label>
          <label>
            <span>Description</span>
            <input value={agentDescription} onChange={(event) => setAgentDescription(event.target.value)} />
          </label>
          <label>
            <span>System prompt</span>
            <textarea value={agentSystemPrompt} onChange={(event) => setAgentSystemPrompt(event.target.value)} rows={5} />
          </label>
          <label>
            <span>Model</span>
            <AgentModelSelect value={agentModel} onChange={setAgentModel} required />
          </label>
        </FormSection>

        {formError ? <div className="notice error">{formError}</div> : null}

        <div className="dialog-actions">
          {onDelete ? (
            confirmDelete ? (
              <button className="danger-button" type="button" onClick={onDelete} disabled={saving}>
                <Trash2 size={16} aria-hidden="true" />
                Confirm delete
              </button>
            ) : (
              <button className="danger-button" type="button" onClick={() => setConfirmDelete(true)} disabled={saving}>
                <Trash2 size={16} aria-hidden="true" />
                Delete
              </button>
            )
          ) : null}
          <button className="secondary-button" type="button" onClick={onClose} disabled={saving}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Save size={16} aria-hidden="true" />}
            Save
          </button>
        </div>
      </form>
    </Modal>
  );
}

function ProjectIntegrationsDialog({
  integrations,
  saving,
  onClose,
  onInstall,
}: {
  integrations: IntegrationTemplate[];
  saving: boolean;
  onClose: () => void;
  onInstall: (integration: IntegrationTemplate, payload: JsonObject) => Promise<void>;
}) {
  const tutorials = integrationTutorials();
  const [tab, setTab] = React.useState<"agents" | "tutorials">("agents");
  const [selectedId, setSelectedId] = React.useState("");
  const [selectedTutorialId, setSelectedTutorialId] = React.useState("");
  const [token, setToken] = React.useState("");
  const [secretName, setSecretName] = React.useState("");
  const [secretValue, setSecretValue] = React.useState("");
  const [formError, setFormError] = React.useState<string | null>(null);
  const selected = integrations.find((integration) => integration.id === selectedId) ?? null;
  const selectedTutorial = tutorials.find((tutorial) => tutorial.id === selectedTutorialId) ?? null;

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    if (!selected) return;
    setFormError(null);
    try {
      await onInstall(
        selected,
        selected.mcp_auth_type === "static_bearer"
          ? { token }
          : {
              secret_name: secretName.trim(),
              secret_value: secretValue,
            },
      );
    } catch (installError) {
      setFormError(errorMessage(installError));
    }
  }

  return (
    <Modal title={selected?.name ?? selectedTutorial?.title ?? "Add integration"} onClose={onClose} wide className="integration-modal">
      <form className="form-grid" onSubmit={submit}>
        {!selected && !selectedTutorial ? (
          <div className="integration-browser">
            <div className="integration-browser-head">
              <div className="snippet-tabs" role="tablist" aria-label="Integration categories">
                <button
                  className={tab === "agents" ? "active" : ""}
                  type="button"
                  role="tab"
                  aria-selected={tab === "agents"}
                  onClick={() => setTab("agents")}
                >
                  Agents
                </button>
                <button
                  className={tab === "tutorials" ? "active" : ""}
                  type="button"
                  role="tab"
                  aria-selected={tab === "tutorials"}
                  onClick={() => setTab("tutorials")}
                >
                  Tutorials
                </button>
              </div>
            </div>
            {tab === "agents" ? (
              integrations.length === 0 ? (
                <div className="structured-empty">No integrations available</div>
              ) : (
                <div className="integration-tile-grid">
                  {integrations.map((integration) => (
                    <button className="integration-tile" key={integration.id} type="button" onClick={() => setSelectedId(integration.id)}>
                      <span className="integration-tile-logo">{integration.logo_data_url ? <img src={integration.logo_data_url} alt="" /> : <Puzzle size={24} aria-hidden="true" />}</span>
                      <span className="integration-tile-copy">
                        <strong>{integration.name}</strong>
                        <small>{integration.description || integration.agent_description || integration.mcp_server_url}</small>
                      </span>
                    </button>
                  ))}
                </div>
              )
            ) : (
              <div className="integration-tile-grid">
                {tutorials.map((tutorial) => (
                  <button className="integration-tile" key={tutorial.id} type="button" onClick={() => setSelectedTutorialId(tutorial.id)}>
                    <span className="integration-tile-logo">{tutorial.icon}</span>
                    <span className="integration-tile-copy">
                      <strong>{tutorial.title}</strong>
                      <small>{tutorial.description}</small>
                    </span>
                  </button>
                ))}
              </div>
            )}
          </div>
        ) : selected ? (
          <>
            <FormSection title="What you get">
              <div className="integration-detail-head">
                <span className="integration-tile-logo">{selected.logo_data_url ? <img src={selected.logo_data_url} alt="" /> : <Puzzle size={24} aria-hidden="true" />}</span>
                <span>
                  <strong>{selected.name}</strong>
                  <small>{selected.description || "Integration template"}</small>
                </span>
              </div>
              <InfoRow icon={<Bot size={15} />} label="Agent" value={selected.agent_name} />
              <InfoRow icon={<Server size={15} />} label="MCP" value={selected.mcp_server_url} />
              <InfoRow icon={<KeyRound size={15} />} label="Auth" value={mcpAuthLabel(selected.mcp_auth_type)} />
              {selected.agent_system_prompt ? <InfoRow icon={<FileText size={15} />} label="System prompt" value="Configured" /> : null}
              {selected.description ? <div className="structured-empty">{selected.description}</div> : null}
              {selected.agent_description ? <div className="structured-empty">{selected.agent_description}</div> : null}
            </FormSection>

            <FormSection title="Required secret">
              {selected.mcp_auth_type === "static_bearer" ? (
                <label>
                  <span>Bearer token</span>
                  <input value={token} onChange={(event) => setToken(event.target.value)} type="password" required />
                </label>
              ) : (
                <div className="form-grid two">
                  <label>
                    <span>Name</span>
                    <input value={secretName} onChange={(event) => setSecretName(event.target.value)} placeholder="API_KEY" required />
                  </label>
                  <label>
                    <span>Value</span>
                    <input value={secretValue} onChange={(event) => setSecretValue(event.target.value)} type="password" required />
                  </label>
                </div>
              )}
            </FormSection>
          </>
        ) : selectedTutorial ? (
          <div className="integration-tutorial-detail">
            <div className="integration-detail-head">
              <span className="integration-tile-logo">{selectedTutorial.icon}</span>
              <span>
                <strong>{selectedTutorial.title}</strong>
                <small>{selectedTutorial.description}</small>
              </span>
            </div>
            {selectedTutorial.steps.map((step, index) => (
              <section className="integration-tutorial-step" key={step.title}>
                <h3>{index + 1}. {step.title}</h3>
                <p>{step.body}</p>
                {step.code ? (
                  <pre className="snippet-block">
                    <code>{step.code}</code>
                  </pre>
                ) : null}
              </section>
            ))}
          </div>
        ) : null}

        {formError ? <div className="notice error">{formError}</div> : null}

        {selected || selectedTutorial ? (
          <div className="dialog-actions">
            <button
              className="secondary-button"
              type="button"
              onClick={() => {
                setSelectedId("");
                setSelectedTutorialId("");
              }}
              disabled={saving}
            >
              <ChevronLeft size={16} aria-hidden="true" />
              Back
            </button>
            {selected ? (
              <button className="primary-button" type="submit" disabled={saving}>
                {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
                Add to project
              </button>
            ) : null}
          </div>
        ) : null}
      </form>
    </Modal>
  );
}

function integrationTutorials(): Array<{
  id: string;
  title: string;
  description: string;
  icon: React.ReactNode;
  steps: Array<{ title: string; body: string; code?: string }>;
}> {
  const endpoint = `${backendUrl}/api/run`;
  return [
    {
      id: "email-trigger",
      title: "Email Trigger",
      description: "Receive Resend webhooks and forward inbound emails into an API trigger.",
      icon: <Mail size={24} aria-hidden="true" />,
      steps: [
        {
          title: "Create an API trigger key",
          body: "Create an API key for this project and connect an API trigger card to the agent that should process inbound email.",
        },
        {
          title: "Call the API trigger from your backend",
          body: "Your custom backend should receive the email event, transform it into a prompt, and call the API trigger.",
          code: `await fetch("${endpoint}", {
  method: "POST",
  headers: {
    authorization: "Bearer <PROJECT_API_KEY>",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    message: [
      "Inbound email received",
      "From: " + email.from,
      "Subject: " + email.subject,
      email.text,
    ].join("\\n"),
  }),
});`,
        },
        {
          title: "Connect Resend",
          body: "Point a Resend inbound webhook at your backend route, then use the webhook payload to call the API trigger.",
          code: `app.post("/webhooks/resend", async (req, res) => {
  const event = req.body;
  if (event.type !== "email.received") return res.sendStatus(204);

  const email = await resend.emails.get(event.data.email_id);
  await runAgentFromEmail(email);
  res.sendStatus(200);
});`,
        },
      ],
    },
    {
      id: "slack-trigger",
      title: "Slack Trigger",
      description: "Receive Slack bot events and forward selected messages into an API trigger.",
      icon: <MessageSquare size={24} aria-hidden="true" />,
      steps: [
        {
          title: "Create an API trigger key",
          body: "Create an API key for this project and connect an API trigger card to the agent that should process Slack messages.",
        },
        {
          title: "Call the API trigger from your backend",
          body: "Your backend should decide which Slack messages should run the agent, then call the API trigger.",
          code: `await fetch("${endpoint}", {
  method: "POST",
  headers: {
    authorization: "Bearer <PROJECT_API_KEY>",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    message: [
      "Slack message received",
      "Channel: " + event.channel,
      "User: " + event.user,
      event.text,
    ].join("\\n"),
  }),
});`,
        },
        {
          title: "Connect the Slack bot",
          body: "Subscribe your Slack app to message events, verify Slack signatures on your backend, then forward accepted events to the API trigger.",
          code: `app.post("/webhooks/slack", verifySlackSignature, async (req, res) => {
  const event = req.body.event;
  if (event?.type !== "message" || event.bot_id) return res.sendStatus(204);

  await runAgentFromSlack(event);
  res.sendStatus(200);
});`,
        },
      ],
    },
  ];
}

function EnvironmentDialog({
  environment,
  saving,
  onClose,
  onSubmit,
}: {
  environment?: AnthropicEnvironment;
  saving: boolean;
  onClose: () => void;
  onSubmit: (payload: JsonObject) => Promise<void>;
}) {
  const editing = Boolean(environment);
  const initialKind = environment?.config.type === "self_hosted" ? "self_hosted" : "cloud";
  const [name, setName] = React.useState(environment?.name ?? "");
  const [description, setDescription] = React.useState(environment?.description ?? "");
  const [kind, setKind] = React.useState<EnvironmentKind>(initialKind);
  const [scope, setScope] = React.useState<"organization" | "account">(environment?.scope ?? "account");
  const [config, setConfig] = React.useState(formatJson(environment?.config ?? defaultEnvironmentConfig("cloud")));
  const [metadata, setMetadata] = React.useState(formatJson(environment?.metadata ?? {}));
  const [formError, setFormError] = React.useState<string | null>(null);

  function changeKind(nextKind: EnvironmentKind) {
    setKind(nextKind);
    setConfig(formatJson(defaultEnvironmentConfig(nextKind)));
  }

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setFormError(null);

    try {
      const payload: JsonObject = {
        name: name.trim(),
        config: parseJsonObject(config, "Config"),
      };
      if (description.trim()) {
        payload.description = description.trim();
      } else if (editing) {
        payload.description = null;
      }
      const parsedMetadata = parseJsonObject(metadata, "Metadata");
      if (editing || Object.keys(parsedMetadata).length > 0) payload.metadata = parsedMetadata;
      if (kind === "self_hosted") payload.scope = scope;
      await onSubmit(payload);
    } catch (submitError) {
      setFormError(errorMessage(submitError));
    }
  }

  return (
    <Modal title={editing ? "Edit environment" : "Create environment"} onClose={onClose}>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="Basics">
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} required />
          </label>
          <label>
            <span>Description</span>
            <input value={description} onChange={(event) => setDescription(event.target.value)} />
          </label>
          <label>
            <span>Type</span>
            <select value={kind} onChange={(event) => changeKind(event.target.value as EnvironmentKind)}>
              <option value="cloud">Cloud</option>
              <option value="self_hosted">Self-hosted</option>
            </select>
          </label>
          {kind === "self_hosted" ? (
            <label>
              <span>Scope</span>
              <select value={scope} onChange={(event) => setScope(event.target.value as "organization" | "account")}>
                <option value="account">Account</option>
                <option value="organization">Organization</option>
              </select>
            </label>
          ) : null}
        </FormSection>
        <FormSection title="Configuration">
          <JsonEditor label="Config" value={config} onChange={setConfig} rows={8} />
          <JsonEditor label="Metadata" value={metadata} onChange={setMetadata} rows={4} />
        </FormSection>
        {formError ? <div className="notice error">{formError}</div> : null}
        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={onClose}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : editing ? <Save size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
            {editing ? "Save" : "Create"}
          </button>
        </div>
      </form>
    </Modal>
  );
}

function CreateDeploymentDialog({
  agents,
  environments,
  vaults,
  saving,
  onClose,
  onCreate,
}: {
  agents: AgentRecord[];
  environments: AnthropicEnvironment[];
  vaults: VaultRecord[];
  saving: boolean;
  onClose: () => void;
  onCreate: (payload: JsonObject) => Promise<void>;
}) {
  return (
    <DeploymentFormDialog
      title="Create deployment"
      submitLabel="Create"
      agents={agents}
      environments={environments}
      vaults={vaults}
      saving={saving}
      onClose={onClose}
      onSubmit={onCreate}
    />
  );
}

function DeploymentDetailsDialog({
  deployment,
  agents,
  environments,
  vaults,
  saving,
  onClose,
  onUpdate,
  onDelete,
}: {
  deployment: AnthropicDeployment;
  agents: AgentRecord[];
  environments: AnthropicEnvironment[];
  vaults: VaultRecord[];
  saving: boolean;
  onClose: () => void;
  onUpdate: (payload: JsonObject) => Promise<void>;
  onDelete: () => Promise<void>;
}) {
  return (
    <DeploymentFormDialog
      title="Deployment details"
      submitLabel="Save"
      deployment={deployment}
      agents={agents}
      environments={environments}
      vaults={vaults}
      saving={saving}
      onClose={onClose}
      onSubmit={onUpdate}
      onDelete={onDelete}
    />
  );
}

function DeploymentFormDialog({
  title,
  submitLabel,
  deployment,
  agents,
  environments,
  vaults,
  saving,
  onClose,
  onSubmit,
  onDelete,
}: {
  title: string;
  submitLabel: string;
  deployment?: AnthropicDeployment;
  agents: AgentRecord[];
  environments: AnthropicEnvironment[];
  vaults: VaultRecord[];
  saving: boolean;
  onClose: () => void;
  onSubmit: (payload: JsonObject) => Promise<void>;
  onDelete?: () => Promise<void>;
}) {
  const initialSchedule = deploymentScheduleDraft(deployment?.schedule);
  const [name, setName] = React.useState(deployment?.name ?? "");
  const [description, setDescription] = React.useState(deployment?.description ?? "");
  const [agentId, setAgentId] = React.useState(deployment ? deploymentAgentId(deployment) : (agents[0]?.id ?? ""));
  const [environmentId, setEnvironmentId] = React.useState(deployment?.environment_id ?? environments[0]?.id ?? "");
  const [initialMessage, setInitialMessage] = React.useState(deploymentInitialMessage(deployment?.initial_events));
  const [scheduleEnabled, setScheduleEnabled] = React.useState(Boolean(deployment?.schedule));
  const [scheduleMode, setScheduleMode] = React.useState<ScheduleMode>(initialSchedule.mode);
  const [scheduleInterval, setScheduleInterval] = React.useState(initialSchedule.interval);
  const [scheduleMinute, setScheduleMinute] = React.useState(initialSchedule.minute);
  const [scheduleHour, setScheduleHour] = React.useState(initialSchedule.hour);
  const [scheduleDayOfWeek, setScheduleDayOfWeek] = React.useState(initialSchedule.dayOfWeek);
  const [scheduleExpression, setScheduleExpression] = React.useState(initialSchedule.expression);
  const [scheduleTimezone, setScheduleTimezone] = React.useState(initialSchedule.timezone);
  const [resources, setResources] = React.useState(formatJson(deployment?.resources ?? []));
  const [metadata, setMetadata] = React.useState(formatJson(deployment?.metadata ?? {}));
  const [formError, setFormError] = React.useState<string | null>(null);
  const [confirmDelete, setConfirmDelete] = React.useState(false);
  const scheduleDraft: ScheduleDraft = {
    mode: scheduleMode,
    interval: scheduleInterval,
    minute: scheduleMinute,
    hour: scheduleHour,
    dayOfWeek: scheduleDayOfWeek,
    expression: scheduleExpression,
    timezone: scheduleTimezone,
  };

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setFormError(null);

    try {
      if (!name.trim()) throw new Error("Deployment name is required.");
      if (!agentId) throw new Error("Agent is required.");
      if (!environmentId) throw new Error("Environment is required.");
      if (!initialMessage.trim()) throw new Error("Initial message is required.");

      const payload: JsonObject = {
        name: name.trim(),
        description: nullableText(description),
        agent: agentId,
        environment_id: environmentId,
        initial_events: deploymentInitialEvents(initialMessage),
        resources: parseJsonArray(resources, "Resources"),
        metadata: parseJsonObject(metadata, "Metadata"),
        schedule: scheduleEnabled
          ? {
              type: "cron",
              expression: cronExpressionForSchedule(scheduleDraft),
              timezone: scheduleTimezone.trim(),
            }
          : null,
      };
      await onSubmit(payload);
    } catch (submitError) {
      setFormError(errorMessage(submitError));
    }
  }

  return (
    <Modal title={title} onClose={onClose} wide>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="Basics">
          <div className="form-grid two">
            <label>
              <span>Name</span>
              <input value={name} onChange={(event) => setName(event.target.value)} required />
            </label>
            <label>
              <span>Status</span>
              <input value={deployment?.status ?? "new"} disabled />
            </label>
          </div>
          <label>
            <span>Description</span>
            <input value={description} onChange={(event) => setDescription(event.target.value)} />
          </label>
        </FormSection>

        <FormSection title="Runtime">
          <div className="form-grid two">
            <label>
              <span>Agent</span>
              <select value={agentId} onChange={(event) => setAgentId(event.target.value)} required>
                {agents.map((record) => (
                  <option value={record.id} key={record.id}>
                    {record.agent.name} · v{record.agent.version}
                  </option>
                ))}
              </select>
            </label>
            <label>
              <span>Environment</span>
              <select value={environmentId} onChange={(event) => setEnvironmentId(event.target.value)} required>
                {environments.map((environment) => (
                  <option value={environment.id} key={environment.id}>
                    {environment.name}
                  </option>
                ))}
              </select>
            </label>
          </div>
          <div className="structured-empty">Deployments use the managed global vault plus the selected agent's project vault.</div>
        </FormSection>

        <FormSection title="Initial event">
          <label>
            <span>User message</span>
            <textarea value={initialMessage} onChange={(event) => setInitialMessage(event.target.value)} rows={5} required />
          </label>
        </FormSection>

        <FormSection title="Schedule">
          <label className="check-row">
            <input type="checkbox" checked={scheduleEnabled} onChange={(event) => setScheduleEnabled(event.target.checked)} />
            <span>Run on a schedule</span>
          </label>
          {scheduleEnabled ? (
            <div className="schedule-builder">
              <label>
                <span>Repeat</span>
                <select value={scheduleMode} onChange={(event) => setScheduleMode(event.target.value as ScheduleMode)}>
                  <option value="hours">Every x hours</option>
                  <option value="days">Every x days</option>
                  <option value="weeks">Every x weeks</option>
                  <option value="cron">Custom cron</option>
                </select>
              </label>
              {scheduleMode === "cron" ? (
                <label>
                  <span>Cron expression</span>
                  <input value={scheduleExpression} onChange={(event) => setScheduleExpression(event.target.value)} placeholder="0 9 * * 1-5" required />
                </label>
              ) : (
                <>
                  <label>
                    <span>Every</span>
                    <input type="number" min={1} max={scheduleIntervalMax(scheduleMode)} value={scheduleInterval} onChange={(event) => setScheduleInterval(numberFromInput(event.target.value, 1))} required />
                  </label>
                  <label>
                    <span>Unit</span>
                    <input value={scheduleMode === "hours" ? "hours" : scheduleMode === "days" ? "days" : "weeks"} disabled />
                  </label>
                  {scheduleMode !== "hours" ? (
                    <label>
                      <span>At</span>
                      <input type="time" value={timeInputValue(scheduleHour, scheduleMinute)} onChange={(event) => {
                        const time = parseTimeInput(event.target.value);
                        setScheduleHour(time.hour);
                        setScheduleMinute(time.minute);
                      }} />
                    </label>
                  ) : (
                    <label>
                      <span>Minute</span>
                      <input type="number" min={0} max={59} value={scheduleMinute} onChange={(event) => setScheduleMinute(numberFromInput(event.target.value, 0))} required />
                    </label>
                  )}
                  {scheduleMode === "weeks" ? (
                    <label>
                      <span>Day</span>
                      <select value={scheduleDayOfWeek} onChange={(event) => setScheduleDayOfWeek(numberFromInput(event.target.value, 1))}>
                        {weekdays().map((day) => (
                          <option value={day.value} key={day.value}>
                            {day.label}
                          </option>
                        ))}
                      </select>
                    </label>
                  ) : null}
                </>
              )}
              <label>
                <span>Timezone</span>
                <input value={scheduleTimezone} onChange={(event) => setScheduleTimezone(event.target.value)} placeholder="UTC" required />
              </label>
              <div className="schedule-summary">
                <span>Cron</span>
                <strong>{cronExpressionPreview(scheduleDraft)}</strong>
              </div>
            </div>
          ) : null}
        </FormSection>

        <FormSection title="Advanced">
          <JsonEditor label="Resources" value={resources} onChange={setResources} rows={6} />
          <JsonEditor label="Metadata" value={metadata} onChange={setMetadata} rows={4} />
        </FormSection>

        {deployment ? (
          <aside className="deployment-summary">
            <InfoRow icon={<Rocket size={15} />} label="ID" value={deployment.id} />
            <InfoRow icon={<Calendar size={15} />} label="Created" value={formatDateTime(deployment.created_at)} />
            <InfoRow icon={<Calendar size={15} />} label="Updated" value={formatDateTime(deployment.updated_at)} />
          </aside>
        ) : null}

        {formError ? <div className="notice error">{formError}</div> : null}

        <div className="dialog-actions">
          {onDelete ? (
            <button className="danger-button" type="button" onClick={() => setConfirmDelete(true)} disabled={saving}>
              {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Archive size={16} aria-hidden="true" />}
              Delete
            </button>
          ) : null}
          <button className="secondary-button" type="button" onClick={onClose}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Save size={16} aria-hidden="true" />}
            {submitLabel}
          </button>
        </div>
      </form>
      {confirmDelete && onDelete ? (
        <ConfirmDialog
          title="Delete deployment"
          message={`Archive ${deployment?.name ?? "this deployment"}?`}
          confirmLabel="Delete"
          danger
          onCancel={() => setConfirmDelete(false)}
          onConfirm={() => {
            setConfirmDelete(false);
            void onDelete().catch((deleteError) => setFormError(errorMessage(deleteError)));
          }}
        />
      ) : null}
    </Modal>
  );
}

function CreateAgentDialog({
  auth,
  projectId,
  projects,
  agents,
  registeredMcpServers,
  projectCanEdit,
  workspaceRole,
  onClose,
  onCreated,
  side,
}: {
  auth: AuthSession;
  projectId: string;
  projects: ProjectRecord[];
  agents: AgentRecord[];
  registeredMcpServers: RegisteredMcpServer[];
  projectCanEdit?: boolean;
  workspaceRole: WorkspaceRole;
  onClose: () => void;
  onCreated: (agent: Agent) => void;
  side?: boolean;
}) {
  const [name, setName] = React.useState("");
  const [model, setModel] = React.useState(defaultAgentModel);
  const [description, setDescription] = React.useState("");
  const [system, setSystem] = React.useState("");
  const [globalAgent, setGlobalAgent] = React.useState(false);
  const [projectIds, setProjectIds] = React.useState<string[]>(projectId ? [projectId] : []);
  const [projectsOpen, setProjectsOpen] = React.useState(false);
  const [advancedOpen, setAdvancedOpen] = React.useState(false);
  const [skills, setSkills] = React.useState<SkillDraft[]>([]);
  const [mcpServers, setMcpServers] = React.useState<McpServerDraft[]>([]);
  const [subAgents, setSubAgents] = React.useState<SubAgentDraft[]>([]);
  const [parameterConfig, setParameterConfig] = React.useState<AgentParameterConfig>(createDefaultAgentParameterConfig());
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState<string | null>(null);
  const [confirmClose, setConfirmClose] = React.useState(false);

  const dirty =
    name.trim() !== "" ||
    model.trim() !== defaultAgentModel ||
    description.trim() !== "" ||
    system.trim() !== "" ||
    globalAgent ||
    JSON.stringify(projectIds) !== JSON.stringify(projectId ? [projectId] : []) ||
    skills.length > 0 ||
    mcpServers.length > 0 ||
    subAgents.length > 0 ||
    agentParameterConfigDirty(parameterConfig);

  function requestClose() {
    if (dirty && !saving) {
      setConfirmClose(true);
      return;
    }
    onClose();
  }

  React.useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      if (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey && event.key.toLowerCase() === "a") {
        event.preventDefault();
        requestClose();
      }
    }

    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [dirty, saving]);

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    setSaving(true);
    setError(null);

    try {
      const payload: JsonObject = { name: name.trim(), model: model.trim(), global: globalAgent };
      if (!globalAgent) {
        if (projectIds.length === 0) throw new Error("Select at least one project or make this agent global.");
        payload.project_ids = projectIds;
      }
      if (description.trim()) payload.description = description.trim();
      if (system.trim()) payload.system = system;
      const parameterMetadata = serializeAgentParameterMetadata(parameterConfig);
      if (parameterMetadata) payload.metadata = parameterMetadata;

      if (advancedOpen) {
        payload.skills = serializeSkillDrafts(skills);
        const selectedMcpServers = serializeMcpServerDrafts(mcpServers, registeredMcpServers);
        payload.mcp_servers = selectedMcpServers;
        payload.tools = serializeMcpToolsets(selectedMcpServers);
        const multiagent = serializeSubAgents(subAgents);
        if (multiagent) payload.multiagent = multiagent;
      }

      const response = await apiFetch<{ agent: Agent }>("/agents", auth, { method: "POST", body: JSON.stringify(payload) });
      onCreated(response.agent);
    } catch (createError) {
      setError(errorMessage(createError));
    } finally {
      setSaving(false);
    }
  }

  function toggleProject(projectId: string, enabled: boolean) {
    setProjectIds((current) => (enabled ? uniqueStrings([...current, projectId]) : current.filter((id) => id !== projectId)));
  }

  return (
    <Modal title="Create agent" onClose={requestClose} side={side}>
      <form className="form-grid" onSubmit={submit}>
        <FormSection title="Agent">
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} required />
          </label>
          <label>
            <span>Description</span>
            <input value={description} onChange={(event) => setDescription(event.target.value)} />
          </label>
          <label>
            <span>System prompt</span>
            <textarea value={system} onChange={(event) => setSystem(event.target.value)} rows={5} />
          </label>
        </FormSection>

        <CollapsibleSection title="Projects" open={projectsOpen} onToggle={() => setProjectsOpen((value) => !value)}>
          <label className="toggle-row">
            <input type="checkbox" checked={globalAgent} onChange={(event) => setGlobalAgent(event.target.checked)} disabled={workspaceRole !== "admin"} />
            <span>Available globally</span>
          </label>
          {!globalAgent ? (
            <div className="project-checkbox-list">
              {projects.length === 0 ? <div className="structured-empty">No projects available</div> : null}
              {projects.map((project) => (
                <label className="checkbox-row" key={project.id}>
                  <input type="checkbox" checked={projectIds.includes(project.id)} onChange={(event) => toggleProject(project.id, event.target.checked)} />
                  <span>{project.name}</span>
                </label>
              ))}
            </div>
          ) : null}
        </CollapsibleSection>

        <CollapsibleSection title="Configuration" open={advancedOpen} onToggle={() => setAdvancedOpen((value) => !value)}>
          <label>
            <span>Model</span>
            <AgentModelSelect value={model} onChange={setModel} required />
          </label>
          <div className="advanced-fields">
            <AgentParameterEditor config={parameterConfig} onChange={setParameterConfig} />
            <SkillEditor skills={skills} onChange={setSkills} />
            <SubAgentEditor subAgents={subAgents} agents={agents} onChange={setSubAgents} />
            <McpServerEditor servers={mcpServers} registeredServers={registeredMcpServers} onChange={setMcpServers} />
          </div>
        </CollapsibleSection>

        {error ? <div className="notice error">{error}</div> : null}

        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={requestClose}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className="primary-button" type="submit" disabled={saving}>
            {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Plus size={16} aria-hidden="true" />}
            Create
          </button>
        </div>
      </form>
      {confirmClose ? (
        <ConfirmDialog title="Discard changes" message="Close this window and discard the unsaved agent draft?" confirmLabel="Discard" onCancel={() => setConfirmClose(false)} onConfirm={onClose} />
      ) : null}
    </Modal>
  );
}

function AgentDetailsDialog({
  record,
  auth,
  agents,
  members,
  registeredMcpServers,
  projectCanEdit,
  projects,
  selectedProjectId,
  workspaceRole,
  onClose,
  onChanged,
  side,
}: {
  record: AgentRecord;
  auth: AuthSession;
  agents: AgentRecord[];
  members: Member[];
  registeredMcpServers: RegisteredMcpServer[];
  projectCanEdit?: boolean;
  projects: ProjectRecord[];
  selectedProjectId: string | null;
  workspaceRole: WorkspaceRole;
  onClose: () => void;
  onChanged: () => void;
  side?: boolean;
}) {
  const agent = record.agent;
  const initialProjectIds = React.useMemo(() => agentProjectIdsFromMetadata(agent.metadata), [agent.metadata]);
  const fallbackProjectIds = initialProjectIds.length > 0 ? initialProjectIds : selectedProjectId ? [selectedProjectId] : [];
  const initialGlobal = initialProjectIds.length === 0;
  const canEdit = workspaceRole === "admin" || (!initialGlobal && projectCanEdit === true);
  const initialMcpServers = React.useMemo(() => mcpServerDraftsFromAgent(agent.mcp_servers, registeredMcpServers), [agent.mcp_servers, registeredMcpServers]);
  const initialSkills = React.useMemo(() => skillDraftsFromAgent(agent.skills), [agent.skills]);
  const initialSubAgents = React.useMemo(() => subAgentDraftsFromAgent(agent.multiagent), [agent.multiagent]);
  const initialParameterConfig = React.useMemo(() => agentParameterConfigFromMetadata(agent.metadata), [agent.metadata]);
  const subAgentOptions = React.useMemo(() => agents.filter((candidate) => candidate.id !== agent.id), [agents, agent.id]);
  const [name, setName] = React.useState(agent.name);
  const [model, setModel] = React.useState(modelValue(agent.model));
  const [description, setDescription] = React.useState(agent.description ?? "");
  const [system, setSystem] = React.useState(agent.system ?? "");
  const [globalAgent, setGlobalAgent] = React.useState(initialGlobal);
  const [projectIds, setProjectIds] = React.useState<string[]>(fallbackProjectIds);
  const [projectsOpen, setProjectsOpen] = React.useState(false);
  const [skills, setSkills] = React.useState<SkillDraft[]>(initialSkills);
  const [mcpServers, setMcpServers] = React.useState<McpServerDraft[]>(initialMcpServers);
  const [subAgents, setSubAgents] = React.useState<SubAgentDraft[]>(initialSubAgents);
  const [parameterConfig, setParameterConfig] = React.useState<AgentParameterConfig>(initialParameterConfig);
  const [configurationOpen, setConfigurationOpen] = React.useState(false);
  const [detailsOpen, setDetailsOpen] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [removing, setRemoving] = React.useState(false);
  const [error, setError] = React.useState<string | null>(null);
  const [confirmClose, setConfirmClose] = React.useState(false);
  const [confirmRemove, setConfirmRemove] = React.useState(false);

  const dirty =
    canEdit &&
    (name !== agent.name ||
      model !== modelValue(agent.model) ||
      description !== (agent.description ?? "") ||
      system !== (agent.system ?? "") ||
      globalAgent !== initialGlobal ||
      JSON.stringify(projectIds) !== JSON.stringify(fallbackProjectIds) ||
      JSON.stringify(comparableSkillDrafts(skills)) !== JSON.stringify(comparableSkillDrafts(initialSkills)) ||
      JSON.stringify(comparableSubAgentDrafts(subAgents)) !== JSON.stringify(comparableSubAgentDrafts(initialSubAgents)) ||
      JSON.stringify(comparableMcpServerDrafts(mcpServers)) !== JSON.stringify(comparableMcpServerDrafts(initialMcpServers)) ||
      JSON.stringify(comparableAgentParameterConfig(parameterConfig)) !== JSON.stringify(comparableAgentParameterConfig(initialParameterConfig)));

  function requestClose() {
    if (dirty && !saving && !removing) {
      setConfirmClose(true);
      return;
    }
    onClose();
  }

  async function save(event: React.FormEvent) {
    event.preventDefault();
    if (!canEdit) return;

    setSaving(true);
    setError(null);
    try {
      const multiagent = serializeSubAgents(subAgents);
      const selectedMcpServers = serializeMcpServerDrafts(mcpServers, registeredMcpServers);
      const parameterMetadata = serializeAgentParameterMetadata(parameterConfig);
      const scopeChanged = globalAgent !== initialGlobal || JSON.stringify(projectIds) !== JSON.stringify(fallbackProjectIds);
      if (!globalAgent && projectIds.length === 0) {
        throw new Error("Select at least one project or make this agent global.");
      }
      await apiFetch<{ agent: Agent }>(`/agents/${encodeURIComponent(agent.id)}`, auth, {
        method: "PATCH",
        body: JSON.stringify({
          version: agent.version,
          name: name.trim(),
          model: model.trim(),
          description: nullableText(description),
          system: nullableText(system),
          ...(workspaceRole === "admin" && (scopeChanged || globalAgent) ? { global: globalAgent } : {}),
          ...(!globalAgent && scopeChanged ? { project_ids: projectIds } : {}),
          skills: serializeSkillDrafts(skills),
          multiagent,
          mcp_servers: selectedMcpServers,
          tools: serializeMcpToolsets(selectedMcpServers),
          metadata: {
            agent_parameter_config: parameterMetadata?.agent_parameter_config ?? null,
          },
        }),
      });
      onChanged();
    } catch (saveError) {
      setError(errorMessage(saveError));
    } finally {
      setSaving(false);
    }
  }

  function toggleProject(projectId: string, enabled: boolean) {
    setProjectIds((current) => (enabled ? uniqueStrings([...current, projectId]) : current.filter((id) => id !== projectId)));
  }

  async function remove() {
    if (!canEdit) return;

    setRemoving(true);
    setError(null);
    try {
      await apiFetch<{ agent: Agent }>(`/agents/${encodeURIComponent(agent.id)}/archive`, auth, { method: "POST", body: "{}" });
      onChanged();
    } catch (removeError) {
      setError(errorMessage(removeError));
    } finally {
      setRemoving(false);
    }
  }

  return (
    <Modal title="Agent details" onClose={requestClose} wide={!side} side={side}>
      <form className="form-grid" onSubmit={save}>
        <FormSection title="Agent">
          <label>
            <span>Name</span>
            <input value={name} onChange={(event) => setName(event.target.value)} disabled={!canEdit} required />
          </label>
          <label>
            <span>Description</span>
            <input value={description} onChange={(event) => setDescription(event.target.value)} disabled={!canEdit} />
          </label>
          <label>
            <span>System prompt</span>
            <textarea value={system} onChange={(event) => setSystem(event.target.value)} disabled={!canEdit} rows={5} />
          </label>
        </FormSection>

        <CollapsibleSection title="Projects" open={projectsOpen} onToggle={() => setProjectsOpen((value) => !value)}>
          <label className="toggle-row">
            <input type="checkbox" checked={globalAgent} onChange={(event) => setGlobalAgent(event.target.checked)} disabled={!canEdit || workspaceRole !== "admin"} />
            <span>Available globally</span>
          </label>
          {!globalAgent ? (
            <div className="project-checkbox-list">
              {projects.length === 0 ? <div className="structured-empty">No projects available</div> : null}
              {projects.map((project) => (
                <label className="checkbox-row" key={project.id}>
                  <input type="checkbox" checked={projectIds.includes(project.id)} onChange={(event) => toggleProject(project.id, event.target.checked)} disabled={!canEdit} />
                  <span>{project.name}</span>
                </label>
              ))}
            </div>
          ) : null}
        </CollapsibleSection>

        <CollapsibleSection title="Configuration" open={configurationOpen} onToggle={() => setConfigurationOpen((value) => !value)}>
          <label>
            <span>Model</span>
            <AgentModelSelect value={model} onChange={setModel} disabled={!canEdit} required />
          </label>
          <div className="advanced-fields">
            <AgentParameterEditor config={parameterConfig} onChange={setParameterConfig} disabled={!canEdit} />
            <SkillEditor skills={skills} onChange={setSkills} disabled={!canEdit} />
            <SubAgentEditor subAgents={subAgents} agents={subAgentOptions} onChange={setSubAgents} disabled={!canEdit} />
            <McpServerEditor servers={mcpServers} registeredServers={registeredMcpServers} onChange={setMcpServers} disabled={!canEdit} />
          </div>
        </CollapsibleSection>

        <CollapsibleSection title="Details" open={detailsOpen} onToggle={() => setDetailsOpen((value) => !value)}>
          <div className="details-info-grid">
            <InfoRow icon={<Bot size={15} />} label="Agent ID" value={agent.id} />
            <InfoRow icon={<Bot size={15} />} label="Version" value={`v${agent.version}`} />
            <InfoRow icon={<Calendar size={15} />} label="Created" value={formatDateTime(agent.created_at)} />
            <InfoRow icon={<Calendar size={15} />} label="Updated" value={formatDateTime(agent.updated_at)} />
            <InfoRow icon={<User size={15} />} label="Creator" value={record.creator_uuid} />
            <InfoRow icon={canEdit ? <Pencil size={15} /> : <User size={15} />} label="Access" value={canEdit ? "Editable" : "Read only"} />
            <a className="doc-link" href="https://docs.anthropic.com/" target="_blank" rel="noreferrer">
              <ExternalLink size={15} aria-hidden="true" />
              Documentation
            </a>
          </div>
        </CollapsibleSection>

        {error ? <div className="notice error">{error}</div> : null}

        <div className="dialog-actions">
          {canEdit ? (
            <button className="danger-button" type="button" onClick={() => setConfirmRemove(true)} disabled={removing}>
              {removing ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Archive size={16} aria-hidden="true" />}
              Remove
            </button>
          ) : null}
          <button className="secondary-button" type="button" onClick={requestClose}>
            <X size={16} aria-hidden="true" />
            Close
          </button>
          {canEdit ? (
            <button className="primary-button" type="submit" disabled={saving}>
              {saving ? <Loader2 className="spin" size={16} aria-hidden="true" /> : <Save size={16} aria-hidden="true" />}
              Save
            </button>
          ) : null}
        </div>
      </form>
      {confirmClose ? (
        <ConfirmDialog title="Discard changes" message="Close this window and discard unsaved updates?" confirmLabel="Discard" onCancel={() => setConfirmClose(false)} onConfirm={onClose} />
      ) : null}
      {confirmRemove ? (
        <ConfirmDialog
          title="Remove agent"
          message={`Archive ${agent.name} and remove it from this registry?`}
          confirmLabel="Remove"
          danger
          onCancel={() => setConfirmRemove(false)}
          onConfirm={() => {
            setConfirmRemove(false);
            void remove();
          }}
        />
      ) : null}
    </Modal>
  );
}

function CollaboratorEditor({
  collaborators,
  members,
  creatorUuid,
  selectedMemberUuid,
  manualCollaborator,
  onSelectedMemberChange,
  onManualCollaboratorChange,
  onAddSelectedMember,
  onAddManualCollaborator,
  onRemoveCollaborator,
}: {
  collaborators: string[];
  members: Member[];
  creatorUuid: string;
  selectedMemberUuid: string;
  manualCollaborator: string;
  onSelectedMemberChange: (uuid: string) => void;
  onManualCollaboratorChange: (uuid: string) => void;
  onAddSelectedMember: () => void;
  onAddManualCollaborator: () => void;
  onRemoveCollaborator: (uuid: string) => void;
}) {
  const collaboratorSet = new Set(collaborators);
  const availableMembers = members.filter((member) => member.uuid !== creatorUuid && !collaboratorSet.has(member.uuid));

  return (
    <div className="collaborator-editor">
      <div className="collaborator-add-row">
        <label>
          <span>Member</span>
          <select value={selectedMemberUuid} onChange={(event) => onSelectedMemberChange(event.target.value)}>
            <option value="">Select member</option>
            {availableMembers.map((member) => (
              <option value={member.uuid} key={member.uuid}>
                {member.email}
              </option>
            ))}
          </select>
        </label>
        <button className="secondary-button compact-button" type="button" onClick={onAddSelectedMember} disabled={!selectedMemberUuid}>
          <Plus size={15} aria-hidden="true" />
          Add
        </button>
      </div>
      <div className="collaborator-add-row">
        <label>
          <span>UUID</span>
          <input value={manualCollaborator} onChange={(event) => onManualCollaboratorChange(event.target.value)} placeholder="00000000-0000-0000-0000-000000000000" />
        </label>
        <button className="secondary-button compact-button" type="button" onClick={onAddManualCollaborator} disabled={!manualCollaborator.trim()}>
          <Plus size={15} aria-hidden="true" />
          Add
        </button>
      </div>
      {collaborators.length === 0 ? <div className="structured-empty">No collaborators configured</div> : null}
      {collaborators.map((uuid) => (
        <div className="collaborator-row" key={uuid}>
          <span>
            <strong>{memberEmailFor(uuid, members) ?? shortId(uuid)}</strong>
            <small>{uuid}</small>
          </span>
          <button className="icon-button row-remove-button" type="button" onClick={() => onRemoveCollaborator(uuid)} title="Remove collaborator">
            <X size={16} aria-hidden="true" />
          </button>
        </div>
      ))}
    </div>
  );
}

function CollaboratorList({ collaborators, members }: { collaborators: string[]; members: Member[] }) {
  return (
    <section className="collaborator-list" aria-label="Collaborators">
      <div className="collaborator-list-head">
        <Users size={15} aria-hidden="true" />
        <span>Collaborators</span>
      </div>
      {collaborators.length === 0 ? (
        <strong>No collaborators</strong>
      ) : (
        collaborators.map((uuid) => (
          <div className="collaborator-list-item" key={uuid}>
            <strong>{memberEmailFor(uuid, members) ?? shortId(uuid)}</strong>
            <small>{uuid}</small>
          </div>
        ))
      )}
    </section>
  );
}

function FormSection({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <section className="form-section">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

function CollapsibleSection({
  title,
  open,
  onToggle,
  children,
}: {
  title: string;
  open: boolean;
  onToggle: () => void;
  children: React.ReactNode;
}) {
  return (
    <section className="form-section collapsible-section">
      <button className="collapsible-section-toggle" type="button" onClick={onToggle} aria-expanded={open}>
        <span>{title}</span>
        {open ? <ChevronDown size={17} aria-hidden="true" /> : <ChevronRight size={17} aria-hidden="true" />}
      </button>
      {open ? <div className="collapsible-section-body">{children}</div> : null}
    </section>
  );
}

function AgentModelSelect({
  value,
  onChange,
  disabled,
  required,
}: {
  value: string;
  onChange: (value: string) => void;
  disabled?: boolean;
  required?: boolean;
}) {
  const hasKnownValue = agentModelOptions.some((option) => option.value === value);

  return (
    <select value={value} onChange={(event) => onChange(event.target.value)} disabled={disabled} required={required}>
      {!hasKnownValue && value ? <option value={value}>{value}</option> : null}
      {agentModelOptions.map((option) => (
        <option value={option.value} key={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}

function JsonEditor({ label, value, onChange, rows, disabled }: { label: string; value: string; onChange: (value: string) => void; rows: number; disabled?: boolean }) {
  return (
    <label>
      <span>{label}</span>
      <textarea className="code-input" value={value} onChange={(event) => onChange(event.target.value)} rows={rows} spellCheck={false} disabled={disabled} />
    </label>
  );
}

function AgentParameterEditor({ config, onChange, disabled }: { config: AgentParameterConfig; onChange: (config: AgentParameterConfig) => void; disabled?: boolean }) {
  function updateParameter(id: string, patch: Partial<AgentParameterDraft>) {
    onChange({ ...config, parameters: config.parameters.map((parameter) => (parameter.id === id ? { ...parameter, ...patch } : parameter)) });
  }

  function removeParameter(id: string) {
    onChange({ ...config, parameters: config.parameters.filter((parameter) => parameter.id !== id) });
  }

  return (
    <div className="structured-editor parameter-schema-editor">
      <div className="structured-editor-head">
        <span>Required values</span>
        <button className="secondary-button compact-button" type="button" onClick={() => onChange({ ...config, parameters: [...config.parameters, createAgentParameterDraft()] })} disabled={disabled || !config.enabled}>
          <Plus size={15} aria-hidden="true" />
          Add value
        </button>
      </div>
      <label className="check-row">
        <input type="checkbox" checked={config.enabled} onChange={(event) => onChange({ ...config, enabled: event.target.checked })} disabled={disabled} />
        <span>Enable custom values</span>
      </label>
      <label className="check-row">
        <input type="checkbox" checked={config.allowAdditional} onChange={(event) => onChange({ ...config, allowAdditional: event.target.checked })} disabled={disabled || !config.enabled} />
        <span>Allow additional custom values</span>
      </label>
      {!config.enabled ? <div className="structured-empty">Values are disabled</div> : null}
      {config.enabled && config.parameters.length === 0 ? <div className="structured-empty">No required values configured</div> : null}
      {config.enabled
        ? config.parameters.map((parameter) => (
            <div className="structured-row parameter-row" key={parameter.id}>
              <label>
                <span>Key</span>
                <input value={parameter.key} onChange={(event) => updateParameter(parameter.id, { key: event.target.value })} disabled={disabled} placeholder="theme" />
              </label>
              <label>
                <span>Label</span>
                <input value={parameter.label} onChange={(event) => updateParameter(parameter.id, { label: event.target.value })} disabled={disabled} placeholder="Theme" />
              </label>
              <label>
                <span>Type</span>
                <select value={parameter.type} onChange={(event) => updateParameter(parameter.id, { type: event.target.value as AgentParameterType })} disabled={disabled}>
                  <option value="text">Text</option>
                  <option value="number">Number</option>
                  <option value="boolean">Boolean</option>
                  <option value="select">Select</option>
                </select>
              </label>
              <label>
                <span>Default</span>
                <input value={parameter.defaultValue} onChange={(event) => updateParameter(parameter.id, { defaultValue: event.target.value })} disabled={disabled} />
              </label>
              {parameter.type === "select" ? (
                <label>
                  <span>Options</span>
                  <input value={parameter.options} onChange={(event) => updateParameter(parameter.id, { options: event.target.value })} disabled={disabled} placeholder="light, dark" />
                </label>
              ) : null}
              <label className="parameter-description-field">
                <span>Description</span>
                <input value={parameter.description} onChange={(event) => updateParameter(parameter.id, { description: event.target.value })} disabled={disabled} />
              </label>
              <button className="icon-button row-remove-button" type="button" onClick={() => removeParameter(parameter.id)} disabled={disabled} title="Remove value">
                <X size={16} aria-hidden="true" />
              </button>
            </div>
          ))
        : null}
    </div>
  );
}

function NodeParameterEditor({ config, values, onChange, disabled }: { config: AgentParameterConfig; values: Record<string, string>; onChange: (values: Record<string, string>) => void; disabled?: boolean }) {
  const [open, setOpen] = React.useState(false);
  const knownKeys = new Set(config.parameters.map((parameter) => parameter.key));
  const additionalEntries = Object.entries(values).filter(([key]) => !knownKeys.has(key));
  const hasExpandableValues = config.allowAdditional;

  function setValue(key: string, value: string) {
    onChange({ ...values, [key]: value });
  }

  function removeValue(key: string) {
    const next = { ...values };
    delete next[key];
    onChange(next);
  }

  function renameValue(previousKey: string, nextKey: string) {
    if (previousKey === nextKey || knownKeys.has(nextKey)) return;
    const next = { ...values };
    const value = next[previousKey] ?? "";
    delete next[previousKey];
    next[nextKey] = value;
    onChange(next);
  }

  function addAdditionalValue() {
    let index = 1;
    let key = "custom_value";
    while (values[key] !== undefined || knownKeys.has(key)) {
      index += 1;
      key = `custom_value_${index}`;
    }
    onChange({ ...values, [key]: "" });
  }

  return (
    <div className="node-parameter-editor">
      {config.parameters.length > 0 ? (
        <div className="node-required-parameter-fields">
          {config.parameters.map((parameter) => (
            <NodeParameterInput key={parameter.key} parameter={parameter} value={values[parameter.key] ?? parameter.defaultValue} onChange={(value) => setValue(parameter.key, value)} disabled={disabled} />
          ))}
        </div>
      ) : null}
      {hasExpandableValues ? (
        <button className="node-parameter-toggle" type="button" onClick={() => setOpen((value) => !value)} aria-expanded={open}>
          <span>Additional values</span>
          {open ? <ChevronDown size={14} aria-hidden="true" /> : <ChevronRight size={14} aria-hidden="true" />}
        </button>
      ) : null}
      {open && hasExpandableValues ? (
        <div className="node-parameter-fields">
          {additionalEntries.map(([key, value]) => (
            <div className="node-parameter-additional-row" key={key}>
              <input value={key} onChange={(event) => renameValue(key, event.target.value)} disabled={disabled || !config.allowAdditional} />
              <input value={value} onChange={(event) => setValue(key, event.target.value)} disabled={disabled || !config.allowAdditional} />
              <button className="icon-button compact-icon" type="button" onClick={() => removeValue(key)} disabled={disabled || !config.allowAdditional} title="Remove value">
                <X size={12} aria-hidden="true" />
              </button>
            </div>
          ))}
          {config.allowAdditional ? (
            <button className="secondary-button compact-button" type="button" onClick={addAdditionalValue} disabled={disabled}>
              <Plus size={14} aria-hidden="true" />
              Add value
            </button>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

function NodeParameterInput({ parameter, value, onChange, disabled }: { parameter: AgentParameterDraft; value: string; onChange: (value: string) => void; disabled?: boolean }) {
  const hint = `Enter ${parameter.label || parameter.key}...`;
  if (parameter.type === "boolean") {
    return (
      <select value={value} onChange={(event) => onChange(event.target.value)} disabled={disabled}>
        <option value="">{hint}</option>
        <option value="true">True</option>
        <option value="false">False</option>
      </select>
    );
  }
  if (parameter.type === "select") {
    const options = selectOptionsFromString(parameter.options);
    return (
      <select value={value} onChange={(event) => onChange(event.target.value)} disabled={disabled}>
        <option value="">{hint}</option>
        {options.map((option) => (
          <option value={option} key={option}>
            {option}
          </option>
        ))}
      </select>
    );
  }
  return <input type={parameter.type === "number" ? "number" : "text"} value={value} onChange={(event) => onChange(event.target.value)} disabled={disabled} placeholder={hint} />;
}

function SkillEditor({ skills, onChange, disabled }: { skills: SkillDraft[]; onChange: (skills: SkillDraft[]) => void; disabled?: boolean }) {
  function update(id: string, patch: Partial<SkillDraft>) {
    onChange(skills.map((skill) => (skill.id === id ? { ...skill, ...patch } : skill)));
  }

  function remove(id: string) {
    onChange(skills.filter((skill) => skill.id !== id));
  }

  return (
    <div className="structured-editor">
      <div className="structured-editor-head">
        <span>Skills</span>
        <button className="secondary-button compact-button" type="button" onClick={() => onChange([...skills, createSkillDraft()])} disabled={disabled}>
          <Plus size={15} aria-hidden="true" />
          Add skill
        </button>
      </div>

      {skills.length === 0 ? <div className="structured-empty">No skills configured</div> : null}

      {skills.map((skill) => (
        <div className="structured-row skill-row" key={skill.id}>
          <label>
            <span>Type</span>
            <select value={skill.type} onChange={(event) => update(skill.id, { type: event.target.value as SkillDraft["type"] })} disabled={disabled}>
              <option value="anthropic">Anthropic</option>
              <option value="custom">Custom</option>
            </select>
          </label>
          <label>
            <span>Skill ID</span>
            <input value={skill.skillId} onChange={(event) => update(skill.id, { skillId: event.target.value })} disabled={disabled} placeholder={skill.type === "anthropic" ? "xlsx" : "skill_abc123"} />
          </label>
          <label>
            <span>Version</span>
            <input value={skill.version} onChange={(event) => update(skill.id, { version: event.target.value })} disabled={disabled} placeholder="latest" />
          </label>
          <button className="icon-button row-remove-button" type="button" onClick={() => remove(skill.id)} disabled={disabled} title="Remove skill">
            <X size={16} aria-hidden="true" />
          </button>
        </div>
      ))}
    </div>
  );
}

function SubAgentEditor({
  subAgents,
  agents,
  onChange,
  disabled,
}: {
  subAgents: SubAgentDraft[];
  agents: AgentRecord[];
  onChange: (subAgents: SubAgentDraft[]) => void;
  disabled?: boolean;
}) {
  function update(id: string, patch: Partial<SubAgentDraft>) {
    onChange(subAgents.map((subAgent) => (subAgent.id === id ? { ...subAgent, ...patch } : subAgent)));
  }

  function remove(id: string) {
    onChange(subAgents.filter((subAgent) => subAgent.id !== id));
  }

  const selectedIds = new Set(subAgents.map((subAgent) => subAgent.agentId).filter(Boolean));

  return (
    <div className="structured-editor">
      <div className="structured-editor-head">
        <span>Sub agents</span>
        <button className="secondary-button compact-button" type="button" onClick={() => onChange([...subAgents, createSubAgentDraft()])} disabled={disabled || agents.length === 0}>
          <Plus size={15} aria-hidden="true" />
          Add sub agent
        </button>
      </div>

      {subAgents.length === 0 ? <div className="structured-empty">No sub agents configured</div> : null}

      {subAgents.map((subAgent) => (
        <div className="structured-row sub-agent-row" key={subAgent.id}>
          <label>
            <span>Agent</span>
            <select value={subAgent.agentId} onChange={(event) => update(subAgent.id, { agentId: event.target.value })} disabled={disabled}>
              <option value="">Select agent</option>
              {agents.map((record) => {
                const selectedElsewhere = selectedIds.has(record.id) && record.id !== subAgent.agentId;
                return (
                  <option value={record.id} key={record.id} disabled={selectedElsewhere}>
                    {record.agent.name} · {record.id}
                  </option>
                );
              })}
            </select>
          </label>
          <button className="icon-button row-remove-button" type="button" onClick={() => remove(subAgent.id)} disabled={disabled} title="Remove sub agent">
            <X size={16} aria-hidden="true" />
          </button>
        </div>
      ))}
    </div>
  );
}

function McpServerEditor({
  servers,
  registeredServers,
  onChange,
  disabled,
}: {
  servers: McpServerDraft[];
  registeredServers: RegisteredMcpServer[];
  onChange: (servers: McpServerDraft[]) => void;
  disabled?: boolean;
}) {
  function update(id: string, registryId: string) {
    const registered = registeredServers.find((server) => server.id === registryId);
    if (!registered) return;
    onChange(servers.map((server) => (server.id === id ? mcpServerDraftFromRegistered(registered, server.id) : server)));
  }

  function remove(id: string) {
    onChange(servers.filter((server) => server.id !== id));
  }

  function add() {
    const firstAvailable = registeredServers.find((server) => !servers.some((selected) => selected.registryId === server.id));
    if (!firstAvailable) return;
    onChange([...servers, mcpServerDraftFromRegistered(firstAvailable)]);
  }

  const selectedRegistryIds = new Set(servers.map((server) => server.registryId).filter(Boolean));

  return (
    <div className="structured-editor">
      <div className="structured-editor-head">
        <span>MCP servers</span>
        <button className="secondary-button compact-button" type="button" onClick={add} disabled={disabled || registeredServers.length === 0 || selectedRegistryIds.size >= registeredServers.length}>
          <Plus size={15} aria-hidden="true" />
          Add MCP
        </button>
      </div>

      {registeredServers.length === 0 ? <div className="structured-empty">No MCP servers registered</div> : servers.length === 0 ? <div className="structured-empty">No MCP servers configured</div> : null}

      {servers.map((server) => (
        <div className="structured-row mcp-row" key={server.id}>
          <label>
            <span>MCP server</span>
            <select value={server.registryId} onChange={(event) => update(server.id, event.target.value)} disabled={disabled}>
              {!server.registryId ? <option value="">{server.name || "Select MCP server"}</option> : null}
              {registeredServers.map((registered) => {
                const selectedElsewhere = selectedRegistryIds.has(registered.id) && registered.id !== server.registryId;
                return (
                  <option value={registered.id} key={registered.id} disabled={selectedElsewhere}>
                    {registered.name}
                  </option>
                );
              })}
            </select>
          </label>
          <button className="icon-button row-remove-button" type="button" onClick={() => remove(server.id)} disabled={disabled} title="Remove MCP server">
            <X size={16} aria-hidden="true" />
          </button>
        </div>
      ))}
    </div>
  );
}

function Modal({
  title,
  children,
  onClose,
  wide,
  side,
  plainHeader,
  className: extraClassName,
}: {
  title: string;
  children: React.ReactNode;
  onClose: () => void;
  wide?: boolean;
  side?: boolean;
  plainHeader?: boolean;
  className?: string;
}) {
  const [entered, setEntered] = React.useState(false);

  React.useEffect(() => {
    const frame = window.requestAnimationFrame(() => setEntered(true));
    return () => window.cancelAnimationFrame(frame);
  }, []);

  const className = `${side ? `modal side ${entered ? "entered" : ""}` : wide ? "modal wide" : "modal"}${extraClassName ? ` ${extraClassName}` : ""}`;
  const backdropClassName = side ? `modal-backdrop side-backdrop ${entered ? "entered" : ""}` : "modal-backdrop";
  return (
    <div className={backdropClassName} role="presentation" onMouseDown={onClose}>
      <section className={className} role="dialog" aria-modal="true" aria-label={title} onMouseDown={(event) => event.stopPropagation()}>
        <header className={plainHeader ? "modal-header plain" : "modal-header"}>
          <h1>{title}</h1>
          {!plainHeader ? (
            <button className="icon-button" type="button" onClick={onClose} title="Close">
              <X size={18} aria-hidden="true" />
            </button>
          ) : (
            <button className="icon-button modal-close-button" type="button" onClick={onClose} title="Close">
              <X size={18} aria-hidden="true" />
            </button>
          )}
        </header>
        {children}
      </section>
    </div>
  );
}

function ConfirmDialog({
  title,
  message,
  confirmLabel,
  danger,
  onCancel,
  onConfirm,
}: {
  title: string;
  message: string;
  confirmLabel: string;
  danger?: boolean;
  onCancel: () => void;
  onConfirm: () => void;
}) {
  return (
    <div className="confirm-backdrop" role="presentation" onMouseDown={onCancel}>
      <section className="confirm-modal" role="dialog" aria-modal="true" aria-label={title} onMouseDown={(event) => event.stopPropagation()}>
        <h2>{title}</h2>
        <p>{message}</p>
        <div className="dialog-actions">
          <button className="secondary-button" type="button" onClick={onCancel}>
            <X size={16} aria-hidden="true" />
            Cancel
          </button>
          <button className={danger ? "danger-button" : "primary-button"} type="button" onClick={onConfirm}>
            <Check size={16} aria-hidden="true" />
            {confirmLabel}
          </button>
        </div>
      </section>
    </div>
  );
}

function InfoRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
  return (
    <div className="info-row">
      <div>{icon}</div>
      <span>{label}</span>
      <strong>{value}</strong>
    </div>
  );
}

async function apiFetch<T>(path: string, auth: AuthSession, init: RequestInit = {}): Promise<T> {
  return publicApiFetch<T>(path, {
    ...init,
    headers: {
      ...requestHeaders(init.headers, init.body),
      authorization: `Bearer ${auth.token}`,
    },
  });
}

async function publicApiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
  const response = await fetch(`${backendUrl}${path}`, {
    ...init,
    headers: requestHeaders(init.headers, init.body),
  });

  const body = (await response.json().catch(() => ({}))) as unknown;
  if (!response.ok) {
    const message = responseErrorMessage(body) ?? `Request failed with ${response.status}`;
    throw new ApiError(message, response.status);
  }
  return body as T;
}

function responseErrorMessage(body: unknown): string | null {
  if (!isRecord(body)) return null;
  if (typeof body.error === "string") return body.error;
  const upstream = body.upstream;
  if (isRecord(upstream) && typeof upstream.message === "string") return upstream.message;
  if (isRecord(upstream) && isRecord(upstream.error) && typeof upstream.error.message === "string") return upstream.error.message;
  return null;
}

function requestHeaders(headers: HeadersInit | undefined, body?: BodyInit | null): Record<string, string> {
  const supplied = headers instanceof Headers ? Object.fromEntries(headers.entries()) : Array.isArray(headers) ? Object.fromEntries(headers) : (headers ?? {});
  if (body instanceof FormData) return supplied;
  return {
    "content-type": "application/json",
    ...supplied,
  };
}

class ApiError extends Error {
  constructor(message: string, readonly status: number) {
    super(message);
  }
}

function isUnauthorized(error: unknown): boolean {
  return error instanceof ApiError && error.status === 401;
}

function isConflict(error: unknown): boolean {
  return error instanceof ApiError && error.status === 409;
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : "Something went wrong";
}

function readIconFile(file: File): Promise<string> {
  const allowedTypes = new Set(["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"]);
  if (!allowedTypes.has(file.type)) throw new Error("Icon must be a PNG, JPEG, WebP, GIF, or SVG image.");
  if (file.size > 256 * 1024) throw new Error("Icon must be smaller than 256 KB.");

  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      if (typeof reader.result === "string") {
        resolve(reader.result);
      } else {
        reject(new Error("Could not read icon image."));
      }
    };
    reader.onerror = () => reject(new Error("Could not read icon image."));
    reader.readAsDataURL(file);
  });
}

function readStoredAuth(): AuthSession | null {
  const raw = localStorage.getItem(authStorageKey);
  if (!raw) return null;

  try {
    const value = JSON.parse(raw) as Partial<AuthSession>;
    if (typeof value.token === "string" && typeof value.uuid === "string" && typeof value.email === "string") {
      return { token: value.token, uuid: value.uuid, email: value.email, role: workspaceRoleFromValue(value.role) ?? undefined };
    }
  } catch {
    clearStoredAuth();
  }

  return null;
}

function workspaceRoleFromValue(value: unknown): WorkspaceRole | null {
  return value === "admin" || value === "member" ? value : null;
}

function workspaceRoleLabel(role: WorkspaceRole): string {
  return role === "admin" ? "Admin" : "Member";
}

function clearStoredAuth() {
  localStorage.removeItem(authStorageKey);
}

function readProjectIdFromPath(): string | null {
  const match = window.location.pathname.match(/^\/project\/([^/]+)\/?$/);
  return match ? decodeURIComponent(match[1]) : null;
}

function replaceProjectPath(projectId: string) {
  const nextPath = `/project/${encodeURIComponent(projectId)}`;
  if (window.location.pathname === nextPath) return;
  window.history.replaceState(null, "", `${nextPath}${window.location.search}${window.location.hash}`);
}

function canEditProject(project: ProjectRecord): boolean {
  return project.current_user_role !== "viewer";
}

function readStoredSelectedProjectId(): string {
  return localStorage.getItem(selectedProjectStorageKey) ?? "";
}

function storeSelectedProjectId(projectId: string) {
  if (projectId) {
    localStorage.setItem(selectedProjectStorageKey, projectId);
  } else {
    localStorage.removeItem(selectedProjectStorageKey);
  }
}

function uniqueStrings(values: string[]): string[] {
  return [...new Set(values)];
}

function readPaletteAgentSections(): { global: boolean; project: boolean } {
  const raw = localStorage.getItem(paletteAgentSectionsStorageKey);
  if (!raw) return { global: true, project: true };
  try {
    const value = JSON.parse(raw) as unknown;
    if (isRecord(value)) {
      return {
        global: typeof value.global === "boolean" ? value.global : true,
        project: typeof value.project === "boolean" ? value.project : true,
      };
    }
  } catch {
    localStorage.removeItem(paletteAgentSectionsStorageKey);
  }
  return { global: true, project: true };
}

function writePaletteAgentSections(value: { global: boolean; project: boolean }) {
  localStorage.setItem(paletteAgentSectionsStorageKey, JSON.stringify(value));
}

function readPaletteMcpSections(): { global: boolean; project: boolean } {
  const raw = localStorage.getItem(paletteMcpSectionsStorageKey);
  if (!raw) return { global: true, project: true };
  try {
    const value = JSON.parse(raw) as unknown;
    if (isRecord(value)) {
      return {
        global: typeof value.global === "boolean" ? value.global : true,
        project: typeof value.project === "boolean" ? value.project : true,
      };
    }
  } catch {
    localStorage.removeItem(paletteMcpSectionsStorageKey);
  }
  return { global: true, project: true };
}

function writePaletteMcpSections(value: { global: boolean; project: boolean }) {
  localStorage.setItem(paletteMcpSectionsStorageKey, JSON.stringify(value));
}

function readPaletteSkillSections(): { builtIn: boolean; global: boolean; project: boolean } {
  const raw = localStorage.getItem(paletteSkillSectionsStorageKey);
  if (!raw) return { builtIn: true, global: true, project: true };
  try {
    const value = JSON.parse(raw) as unknown;
    if (isRecord(value)) {
      return {
        builtIn: typeof value.builtIn === "boolean" ? value.builtIn : true,
        global: typeof value.global === "boolean" ? value.global : typeof value.custom === "boolean" ? value.custom : true,
        project: typeof value.project === "boolean" ? value.project : true,
      };
    }
  } catch {
    localStorage.removeItem(paletteSkillSectionsStorageKey);
  }
  return { builtIn: true, global: true, project: true };
}

function writePaletteSkillSections(value: { builtIn: boolean; global: boolean; project: boolean }) {
  localStorage.setItem(paletteSkillSectionsStorageKey, JSON.stringify(value));
}

function agentProjectIdsFromMetadata(metadata: Record<string, string>): string[] {
  return uniqueStrings([...parseDelimitedMetadata(metadata.project_ids), ...parseDelimitedMetadata(metadata.project_id)]);
}

function parseDelimitedMetadata(value: string | undefined): string[] {
  if (!value) return [];
  return value
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean);
}

function agentIsGlobal(agent: Agent): boolean {
  return agentProjectIdsFromMetadata(agent.metadata).length === 0;
}

function agentFromIntegration(agent: Agent): boolean {
  return Boolean(agent.metadata.integration_template_id);
}

function agentInProject(agent: Agent, projectId: string): boolean {
  return agentProjectIdsFromMetadata(agent.metadata).includes(projectId);
}

function skillIsBuiltIn(skill: SkillRecord): boolean {
  return skill.source === "anthropic";
}

function skillProjectIds(skill: SkillRecord): string[] {
  return skill.project_ids ?? [];
}

function skillIsGlobal(skill: SkillRecord): boolean {
  return skillProjectIds(skill).length === 0;
}

function skillInProject(skill: SkillRecord, projectId: string): boolean {
  return skillProjectIds(skill).includes(projectId);
}

function mcpServerIsGlobal(server: RegisteredMcpServer): boolean {
  return (server.project_ids ?? []).length === 0;
}

function parseJsonObject(value: string, label: string): JsonObject {
  const parsed = JSON.parse(value || "{}") as unknown;
  if (!isRecord(parsed)) throw new Error(`${label} must be a JSON object.`);
  return parsed;
}

function parseJsonArray(value: string, label: string): unknown[] {
  const parsed = JSON.parse(value || "[]") as unknown;
  if (!Array.isArray(parsed)) throw new Error(`${label} must be a JSON array.`);
  return parsed;
}

function mcpServerDraftFromRegistered(server: RegisteredMcpServer, id: string = crypto.randomUUID()): McpServerDraft {
  return { id, registryId: server.id, name: server.name, url: server.url };
}

function createMcpServerDraft(registeredServers: RegisteredMcpServer[] = []): McpServerDraft {
  const first = registeredServers[0];
  return first ? mcpServerDraftFromRegistered(first) : { id: crypto.randomUUID(), registryId: "", name: "", url: "" };
}

function createSkillDraft(): SkillDraft {
  return { id: crypto.randomUUID(), type: "anthropic", skillId: "", version: "" };
}

function createSubAgentDraft(): SubAgentDraft {
  return { id: crypto.randomUUID(), agentId: "" };
}

function createDefaultAgentParameterConfig(): AgentParameterConfig {
  return { enabled: false, allowAdditional: false, parameters: [] };
}

function createAgentParameterDraft(): AgentParameterDraft {
  return { id: crypto.randomUUID(), key: "", label: "", type: "text", defaultValue: "", description: "", options: "" };
}

function createDefaultApiTriggerDraft(apiKeys: ApiKeyRecord[] = []): ApiTriggerDraft {
  return { api_key_id: apiKeys[0]?.id ?? "" };
}

function createDefaultEmailTriggerDraft(emailReceivers: EmailReceiverRecord[] = []): EmailTriggerDraft {
  return { receiver_id: emailReceivers[0]?.id ?? "" };
}

function createDefaultProjectGraph(): ProjectGraph {
  return {
    nodes: [{ id: "play", type: "play", x: 520, y: 280 }],
    edges: [],
  };
}

function cloneProject(project: ProjectRecord): ProjectRecord {
  return JSON.parse(JSON.stringify(project)) as ProjectRecord;
}

function projectEditableShape(project: ProjectRecord): Pick<ProjectRecord, "name" | "description" | "graph"> {
  return { name: project.name, description: project.description, graph: project.graph };
}

function projectNodeTypeLabel(type: ProjectNodeType): string {
  if (type === "play") return "Play";
  if (type === "schedule") return "Schedule";
  if (type === "slack") return "Slack";
  if (type === "api") return "API";
  if (type === "email") return "Email";
  if (type === "mcp") return "MCP";
  if (type === "skill") return "Skill";
  return "Agent";
}

function projectGraphBounds(graph: ProjectGraph): { x: number; y: number; width: number; height: number } {
  if (graph.nodes.length === 0) return { x: 0, y: 0, width: 720, height: 420 };
  const padding = 80;
  const nodeWidth = 240;
  const nodeHeight = 150;
  const minX = Math.min(...graph.nodes.map((node) => node.x)) - padding;
  const minY = Math.min(...graph.nodes.map((node) => node.y)) - padding;
  const maxX = Math.max(...graph.nodes.map((node) => node.x + nodeWidth)) + padding;
  const maxY = Math.max(...graph.nodes.map((node) => node.y + nodeHeight)) + padding;
  return {
    x: minX,
    y: minY,
    width: Math.max(720, maxX - minX),
    height: Math.max(420, maxY - minY),
  };
}

function edgePath(source: ProjectNode, target: ProjectNode): string {
  const sourcePoint = projectNodeCenter(source);
  const targetPoint = projectNodeCenter(target);
  const sourceX = sourcePoint.x;
  const sourceY = sourcePoint.y;
  const targetX = targetPoint.x;
  const targetY = targetPoint.y;
  const midX = (sourceX + targetX) / 2;
  return `M ${sourceX} ${sourceY} C ${midX} ${sourceY}, ${midX} ${targetY}, ${targetX} ${targetY}`;
}

function connectionPreviewPath(source: ProjectNode, target: { x: number; y: number }): string {
  const sourcePoint = projectNodeCenter(source);
  const sourceX = sourcePoint.x;
  const sourceY = sourcePoint.y;
  const midX = (sourceX + target.x) / 2;
  return `M ${sourceX} ${sourceY} C ${midX} ${sourceY}, ${midX} ${target.y}, ${target.x} ${target.y}`;
}

function projectNodeCenter(node: ProjectNode): { x: number; y: number } {
  if (node.type === "mcp") return { x: node.x + 36, y: node.y + 36 };
  if (node.type === "skill") return { x: node.x + 100, y: node.y + 46 };
  if (node.type === "play") return { x: node.x + 130, y: node.y + 72 };
  if (node.type === "agent") return { x: node.x + 90, y: node.y + 44 };
  if (node.type === "slack") return { x: node.x + 110, y: node.y + 74 };
  if (node.type === "api") return { x: node.x + 110, y: node.y + 74 };
  if (node.type === "email") return { x: node.x + 110, y: node.y + 74 };
  return { x: node.x + 120, y: node.y + 72 };
}

function projectEdgeTypeFor(source: ProjectNode, target: ProjectNode): ProjectEdgeType | null {
  if (source.type === "schedule" && target.type === "agent") return "schedules";
  if (source.type === "play" && target.type === "agent") return "runs";
  if (source.type === "agent" && target.type === "agent") return "sub_agent";
  if (source.type === "mcp" && target.type === "agent") return "uses_mcp";
  if (source.type === "skill" && target.type === "agent") return "uses_skill";
  if (source.type === "slack" && target.type === "agent") return "slack_triggers";
  if (source.type === "api" && target.type === "agent") return "api_triggers";
  if (source.type === "email" && target.type === "agent") return "email_triggers";
  return null;
}

function isGlobalAgentNode(node: ProjectNode, agents: AgentRecord[]): boolean {
  if (node.type !== "agent" || !node.agent_id) return false;
  const record = agents.find((candidate) => candidate.id === node.agent_id);
  return record ? agentIsGlobal(record.agent) : false;
}

function syncProjectGraphAgentDependencies(graph: ProjectGraph, agents: AgentRecord[], registeredServers: RegisteredMcpServer[]): ProjectGraph {
  let next = cloneProjectGraph(graph);
  const maxIterations = Math.max(1, agents.length + 1);

  for (let index = 0; index < maxIterations; index += 1) {
    const synced = syncProjectGraphAgentDependenciesOnce(next, agents, registeredServers);
    if (JSON.stringify(synced) === JSON.stringify(next)) return next;
    next = synced;
  }

  return next;
}

function syncProjectGraphAgentDependenciesOnce(graph: ProjectGraph, agents: AgentRecord[], registeredServers: RegisteredMcpServer[]): ProjectGraph {
  const agentById = new Map(agents.map((record) => [record.id, record]));
  let nodes = graph.nodes.map((node) => ({ ...node }));
  let edges = graph.edges.map((edge) => ({ ...edge }));

  for (const source of nodes.filter((node) => node.type === "agent" && node.agent_id && agentById.has(node.agent_id))) {
    const record = agentById.get(source.agent_id ?? "");
    if (!record) continue;
    const globalAgent = agentIsGlobal(record.agent);
    const integrationAgent = agentFromIntegration(record.agent);

    const desiredSubAgentIds = globalAgent ? [] : subAgentIds(record.agent.multiagent);
    const desiredSubAgentSet = new Set(desiredSubAgentIds);
    const desiredMcpServerIds = globalAgent || integrationAgent ? [] : mcpServerIdsFromAgent(record.agent, registeredServers);
    const desiredMcpServerSet = new Set(desiredMcpServerIds);

    edges = edges.filter((edge) => {
      if (edge.source === source.id && edge.type === "sub_agent") {
        const target = nodes.find((node) => node.id === edge.target);
        return target?.type === "agent" && target.agent_id ? desiredSubAgentSet.has(target.agent_id) : false;
      }
      if (edge.target === source.id && edge.type === "uses_mcp") {
        const mcp = nodes.find((node) => node.id === edge.source);
        if (mcp?.type !== "mcp" || !mcp.mcp_server_id) return false;
        if (mcp.synced_from_agent_id === record.id && mcp.synced_role === "mcp") {
          return desiredMcpServerSet.has(mcp.mcp_server_id);
        }
        return true;
      }
      return true;
    });

    nodes = nodes.filter((node) => {
      if (node.synced_from_agent_id !== record.id) return true;
      if (node.synced_role === "sub_agent" && node.agent_id && desiredSubAgentSet.has(node.agent_id)) return true;
      if (node.synced_role === "mcp" && node.mcp_server_id && desiredMcpServerSet.has(node.mcp_server_id)) return true;
      return edges.some((edge) => edge.source === node.id || edge.target === node.id);
    });

    desiredSubAgentIds.forEach((agentId, index) => {
      if (agentId === source.agent_id) return;
      let target = nodes.find((node) => node.type === "agent" && node.agent_id === agentId);
      if (!target) {
        target = {
          id: crypto.randomUUID(),
          type: "agent",
          agent_id: agentId,
          x: source.x + 360,
          y: source.y + index * 170,
          synced_from_agent_id: record.id,
          synced_ref_id: agentId,
          synced_role: "sub_agent",
        };
        nodes = [...nodes, target];
      }
      if (!edges.some((edge) => edge.source === source.id && edge.target === target.id && edge.type === "sub_agent")) {
        edges = [...edges, { id: crypto.randomUUID(), source: source.id, target: target.id, type: "sub_agent" }];
      }
    });

    desiredMcpServerIds.forEach((mcpServerId, index) => {
      const existingMcpNodes = nodes.filter((node) => node.type === "mcp" && node.mcp_server_id === mcpServerId);
      const hasExistingEdge = existingMcpNodes.some((mcp) => edges.some((edge) => edge.source === mcp.id && edge.target === source.id && edge.type === "uses_mcp"));
      if (hasExistingEdge) return;

      let mcp = existingMcpNodes.find((node) => node.synced_from_agent_id === record.id);
      if (!mcp && existingMcpNodes.length > 0) return;
      if (!mcp) {
        mcp = {
          id: crypto.randomUUID(),
          type: "mcp",
          mcp_server_id: mcpServerId,
          x: source.x - 320,
          y: source.y + index * 170,
          synced_from_agent_id: record.id,
          synced_ref_id: mcpServerId,
          synced_role: "mcp",
        };
        nodes = [...nodes, mcp];
      }
      if (!edges.some((edge) => edge.source === mcp.id && edge.target === source.id && edge.type === "uses_mcp")) {
        edges = [...edges, { id: crypto.randomUUID(), source: mcp.id, target: source.id, type: "uses_mcp" }];
      }
    });
  }

  const nodeIds = new Set(nodes.map((node) => node.id));
  return {
    nodes,
    edges: edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)),
  };
}

function cloneProjectGraph(graph: ProjectGraph): ProjectGraph {
  return {
    nodes: graph.nodes.map((node) => ({
      ...node,
      schedule: node.schedule ? { ...node.schedule } : undefined,
      slack_trigger: node.slack_trigger ? { ...node.slack_trigger } : undefined,
      api_trigger: node.api_trigger ? { ...node.api_trigger } : undefined,
      email_trigger: node.email_trigger ? { ...node.email_trigger } : undefined,
      parameter_values: node.parameter_values ? { ...node.parameter_values } : undefined,
    })),
    edges: graph.edges.map((edge) => ({ ...edge })),
  };
}

function mcpServerIdsFromAgent(agent: Agent, registeredServers: RegisteredMcpServer[]): string[] {
  return mcpServerDraftsFromAgent(agent.mcp_servers, registeredServers)
    .map((server) => server.registryId)
    .filter((id): id is string => Boolean(id));
}

function mcpServerDraftsFromAgent(value: unknown, registeredServers: RegisteredMcpServer[]): McpServerDraft[] {
  if (!Array.isArray(value)) return [];
  return value.flatMap((server) => {
    if (!isRecord(server) || server.type !== "url" || typeof server.name !== "string" || typeof server.url !== "string") {
      return [];
    }
    const registered =
      registeredServers.find((candidate) => candidate.name === server.name && candidate.url === server.url) ??
      registeredServers.find((candidate) => candidate.url === server.url) ??
      registeredServers.find((candidate) => candidate.name === server.name);
    return [
      {
        id: crypto.randomUUID(),
        registryId: registered?.id ?? "",
        name: registered?.name ?? server.name,
        url: registered?.url ?? server.url,
      },
    ];
  });
}

function skillDraftsFromAgent(value: unknown): SkillDraft[] {
  if (!Array.isArray(value)) return [];
  return value.flatMap((skill) => {
    if (!isRecord(skill) || (skill.type !== "anthropic" && skill.type !== "custom") || typeof skill.skill_id !== "string") {
      return [];
    }
    return [
      {
        id: crypto.randomUUID(),
        type: skill.type,
        skillId: skill.skill_id,
        version: typeof skill.version === "string" ? skill.version : "",
      },
    ];
  });
}

function subAgentDraftsFromAgent(value: unknown): SubAgentDraft[] {
  if (!isRecord(value) || value.type !== "coordinator" || !Array.isArray(value.agents)) return [];
  return value.agents.flatMap((subAgent) => {
    const agentId = multiagentRosterAgentId(subAgent);
    return agentId ? [{ id: crypto.randomUUID(), agentId }] : [];
  });
}

function agentParameterConfigFromMetadata(metadata: Record<string, string> | undefined): AgentParameterConfig {
  const raw = metadata?.agent_parameter_config;
  if (!raw) return createDefaultAgentParameterConfig();
  try {
    const parsed = JSON.parse(raw) as unknown;
    if (!isRecord(parsed)) return createDefaultAgentParameterConfig();
    const parameters = Array.isArray(parsed.parameters)
      ? parsed.parameters.flatMap((parameter): AgentParameterDraft[] => {
          if (!isRecord(parameter)) return [];
          const key = typeof parameter.key === "string" ? parameter.key : "";
          const type = typeof parameter.type === "string" && ["text", "number", "boolean", "select"].includes(parameter.type) ? (parameter.type as AgentParameterType) : "text";
          return [
            {
              id: key || crypto.randomUUID(),
              key,
              label: typeof parameter.label === "string" ? parameter.label : key,
              type,
              defaultValue: typeof parameter.default === "string" ? parameter.default : "",
              description: typeof parameter.description === "string" ? parameter.description : "",
              options: Array.isArray(parameter.options) ? parameter.options.filter((option): option is string => typeof option === "string").join(", ") : "",
            },
          ];
        })
      : [];
    return {
      enabled: parsed.enabled === true,
      allowAdditional: parsed.allow_additional === true,
      parameters,
    };
  } catch {
    return createDefaultAgentParameterConfig();
  }
}

function collaboratorUuidsFromAgent(agent: Agent): string[] {
  return parseCollaborators(agent.metadata.collaborators);
}

function agentParameterConfigDirty(config: AgentParameterConfig): boolean {
  return config.enabled || config.allowAdditional || config.parameters.length > 0;
}

function comparableAgentParameterConfig(config: AgentParameterConfig): unknown {
  return {
    enabled: config.enabled,
    allowAdditional: config.allowAdditional,
    parameters: config.parameters.map((parameter) => ({
      key: parameter.key.trim(),
      label: parameter.label.trim(),
      type: parameter.type,
      defaultValue: parameter.defaultValue.trim(),
      description: parameter.description.trim(),
      options: selectOptionsFromString(parameter.options),
    })),
  };
}

function serializeAgentParameterMetadata(config: AgentParameterConfig): JsonObject | null {
  if (!agentParameterConfigDirty(config)) return null;
  const keys = new Set<string>();
  const parameters = config.parameters.map((parameter, index) => {
    const key = parameter.key.trim();
    if (!key) throw new Error(`Custom value ${index + 1} needs a key.`);
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Custom value key "${key}" must use letters, numbers, and underscores.`);
    if (keys.has(key)) throw new Error(`Custom value keys must be unique: ${key}.`);
    keys.add(key);
    const options = selectOptionsFromString(parameter.options);
    if (parameter.type === "select" && options.length === 0) throw new Error(`Custom value ${key} needs select options.`);
    return {
      key,
      label: parameter.label.trim() || key,
      type: parameter.type,
      required: true,
      default: parameter.defaultValue.trim(),
      description: parameter.description.trim(),
      ...(parameter.type === "select" ? { options } : {}),
    };
  });
  const serialized = JSON.stringify({
    enabled: config.enabled,
    allow_additional: config.allowAdditional,
    parameters,
  });
  if (serialized.length > 512) throw new Error("Custom values config is too large for agent metadata.");
  return { agent_parameter_config: serialized };
}

function selectOptionsFromString(value: string): string[] {
  return value
    .split(",")
    .map((option) => option.trim())
    .filter((option, index, options) => option.length > 0 && options.indexOf(option) === index);
}

function serializeMcpServerDrafts(servers: McpServerDraft[], registeredServers: RegisteredMcpServer[]): JsonObject[] {
  const names = new Set<string>();
  return servers.map((server, index) => {
    if (!server.registryId) throw new Error(`MCP server ${index + 1} must be selected from registered MCP servers.`);
    const registered = registeredServers.find((candidate) => candidate.id === server.registryId);
    if (!registered) throw new Error(`MCP server ${index + 1} is no longer registered.`);
    const name = registered.name.trim();
    const url = registered.url.trim();
    if (!name) throw new Error(`MCP server ${index + 1} needs a name.`);
    if (names.has(name)) throw new Error(`MCP server names must be unique: ${name}.`);
    names.add(name);
    validateUrl(url, `MCP server ${name}`);
    return { type: "url", name, url };
  });
}

function serializeSkillDrafts(skills: SkillDraft[]): JsonObject[] {
  return skills.map((skill, index) => {
    const skillId = skill.skillId.trim();
    const version = skill.version.trim();
    if (!skillId) throw new Error(`Skill ${index + 1} needs a skill ID.`);
    return version ? { type: skill.type, skill_id: skillId, version } : { type: skill.type, skill_id: skillId };
  });
}

function serializeSubAgents(subAgents: SubAgentDraft[]): JsonObject | null {
  if (subAgents.length === 0) return null;
  const ids = new Set<string>();
  const agents = subAgents.map((subAgent, index) => {
    const agentId = subAgent.agentId.trim();
    if (!agentId) throw new Error(`Sub agent ${index + 1} needs an agent.`);
    if (ids.has(agentId)) throw new Error("Sub agents must be unique.");
    ids.add(agentId);
    return agentId;
  });
  return { type: "coordinator", agents };
}

function serializeCollaborators(collaborators: string[]): string | null {
  const unique = [...new Set(collaborators.map((uuid) => uuid.trim()).filter(Boolean))];
  if (unique.length === 0) return null;
  if (!unique.every(isUuidLike)) throw new Error("Collaborators must be UUIDs.");
  const serialized = unique.join(",");
  if (serialized.length > 512) throw new Error("Too many collaborators for agent metadata.");
  return serialized;
}

function serializeMcpToolsets(servers: JsonObject[]): JsonObject[] {
  const names = servers.map((server) => (typeof server.name === "string" ? server.name.trim() : "")).filter(Boolean);
  return names.map((name) => ({
    type: "mcp_toolset",
    mcp_server_name: name,
    default_config: {
      enabled: true,
      permission_policy: { type: "always_allow" },
    },
  }));
}

function comparableSkillDrafts(skills: SkillDraft[]): Array<Omit<SkillDraft, "id">> {
  return skills.map(({ type, skillId, version }) => ({ type, skillId, version }));
}

function comparableSubAgentDrafts(subAgents: SubAgentDraft[]): Array<Omit<SubAgentDraft, "id">> {
  return subAgents.map(({ agentId }) => ({ agentId }));
}

function comparableMcpServerDrafts(servers: McpServerDraft[]): Array<Omit<McpServerDraft, "id">> {
  return servers.map(({ registryId, name, url }) => ({ registryId, name, url }));
}

function subAgentIds(value: unknown): string[] {
  if (!isRecord(value) || value.type !== "coordinator" || !Array.isArray(value.agents)) return [];
  return value.agents.flatMap((agent) => {
    const agentId = multiagentRosterAgentId(agent);
    return agentId ? [agentId] : [];
  });
}

function multiagentRosterAgentId(value: unknown): string | null {
  if (typeof value === "string") {
    const id = value.trim();
    return id.length > 0 ? id : null;
  }
  if (!isRecord(value)) return null;
  if (value.type !== "agent") return null;
  if (typeof value.id !== "string") return null;
  const id = value.id.trim();
  return id.length > 0 ? id : null;
}

function validateUrl(value: string, label: string) {
  try {
    const url = new URL(value);
    if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("Unsupported protocol");
  } catch {
    throw new Error(`${label} URL must be a valid HTTP URL.`);
  }
}

function parseCollaborators(value: string | undefined): string[] {
  if (!value) return [];
  return value
    .split(",")
    .map((uuid) => uuid.trim())
    .filter((uuid) => uuid.length > 0);
}

function isUuidLike(value: string): boolean {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
}

function memberEmailFor(uuid: string, members: Member[]): string | undefined {
  return members.find((member) => member.uuid === uuid)?.email;
}

function isAgentCreatorByEmail(record: AgentRecord, members: Member[], currentUserEmail: string): boolean {
  const creatorEmail = memberEmailFor(record.creator_uuid, members);
  return creatorEmail ? emailsEqual(creatorEmail, currentUserEmail) : true;
}

function emailsEqual(left: string | undefined | null, right: string | undefined | null): boolean {
  return typeof left === "string" && typeof right === "string" && left.trim().toLowerCase() === right.trim().toLowerCase();
}

function isRecord(value: unknown): value is JsonObject {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function nullableText(value: string): string | null {
  const trimmed = value.trim();
  return trimmed.length > 0 ? value : null;
}

function formatJson(value: unknown): string {
  return JSON.stringify(value ?? {}, null, 2);
}

function modelLabel(model: unknown): string {
  if (typeof model === "string") return model;
  if (isRecord(model) && typeof model.id === "string") return model.id;
  return "model_config";
}

function modelValue(model: unknown): string {
  return typeof model === "string" ? model : modelLabel(model);
}

function mcpAuthLabel(authType: McpAuthKind): string {
  if (authType === "no_auth") return "No auth";
  if (authType === "static_bearer") return "Static bearer";
  return "Environment value";
}

function mcpScopeLabel(server: RegisteredMcpServer): string {
  if (mcpServerIsGlobal(server)) return "Global";
  const projectCount = (server.project_ids ?? []).length;
  return projectCount === 1 ? "1 project" : `${projectCount} projects`;
}

function formatApprovalWait(approvalWait: ChatApprovalWait): string {
  const approvals = approvalWait.approvals.filter((approval): approval is NonNullable<ChatApprovalWait["approvals"][number]> => Boolean(approval));
  if (approvals.length === 0) {
    return `${approvalWait.message} Approve or deny the pending request there, then continue this chat.`;
  }

  const details = approvals
    .map((approval) => {
      if (approval.type === "agent.mcp_tool_use") {
        return `${approval.mcp_server_name ?? "MCP"}:${approval.name ?? approval.id}`;
      }
      return approval.name ? `${approval.type}:${approval.name}` : `${approval.type}:${approval.id}`;
    })
    .join(", ");
  return `${approvalWait.message} Pending: ${details}. Approve or deny it there, then continue this chat.`;
}

function firstToolApproval(approvalWait: ChatApprovalWait): NonNullable<ChatApprovalWait["approvals"][number]> | null {
  return (
    approvalWait.approvals.find(
      (approval): approval is NonNullable<ChatApprovalWait["approvals"][number]> => {
        if (!approval) return false;
        return approval.type === "agent.tool_use" || approval.type === "agent.mcp_tool_use";
      },
    ) ?? null
  );
}

function credentialAuthLabel(auth: VaultCredential["auth"]): string {
  if (auth.type === "static_bearer" && typeof auth.mcp_server_url === "string") {
    return `Static bearer · ${auth.mcp_server_url}`;
  }
  if (auth.type === "mcp_oauth" && typeof auth.mcp_server_url === "string") {
    return `MCP OAuth · ${auth.mcp_server_url}`;
  }
  if (auth.type === "environment_variable" && typeof auth.secret_name === "string") {
    return `Environment variable · ${auth.secret_name}`;
  }
  return auth.type;
}

function deploymentAgentId(deployment: AnthropicDeployment): string {
  return typeof deployment.agent?.id === "string" ? deployment.agent.id : "";
}

function deploymentAgentName(deployment: AnthropicDeployment, agents: AgentRecord[]): string {
  const agentId = deploymentAgentId(deployment);
  return agents.find((record) => record.id === agentId)?.agent.name ?? deployment.agent.name ?? shortId(agentId);
}

function environmentNameFor(environmentId: string, environments: AnthropicEnvironment[]): string {
  return environments.find((environment) => environment.id === environmentId)?.name ?? shortId(environmentId);
}

function deploymentInitialMessage(events: unknown[] | undefined): string {
  if (!Array.isArray(events)) return "";
  const userMessage = events.find((event) => isRecord(event) && event.type === "user.message" && Array.isArray(event.content));
  if (!isRecord(userMessage) || !Array.isArray(userMessage.content)) return "";
  return userMessage.content
    .map((block) => (isRecord(block) && block.type === "text" && typeof block.text === "string" ? block.text : ""))
    .join("")
    .trim();
}

function deploymentInitialEvents(message: string): JsonObject[] {
  return [
    {
      type: "user.message",
      content: [{ type: "text", text: message }],
    },
  ];
}

function deploymentScheduleDraft(schedule: unknown): ScheduleDraft {
  if (isRecord(schedule) && schedule.type === "cron") {
    const expression = typeof schedule.expression === "string" ? schedule.expression : "0 9 * * *";
    const parsed = scheduleDraftFromCronExpression(expression);
    return {
      ...parsed,
      expression,
      timezone: typeof schedule.timezone === "string" ? schedule.timezone : "UTC",
    };
  }
  return { mode: "days", interval: 1, minute: 0, hour: 9, dayOfWeek: 1, expression: "0 9 * * *", timezone: "UTC" };
}

function createDefaultScheduleDraft(): ScheduleDraft {
  return deploymentScheduleDraft(null);
}

function createDefaultSlackTriggerDraft(): SlackTriggerDraft {
  return { type: "none" };
}

function createSlackTriggerDraft(type: SlackTriggerType, current: SlackTriggerDraft = createDefaultSlackTriggerDraft(), nextValue?: string): SlackTriggerDraft {
  if (type === "none" || type === "all") return { type };
  if (type === "channel") return { type, channel_id: nextValue ?? current.channel_id ?? "" };
  if (type === "user") return { type, user_id: nextValue ?? current.user_id ?? "" };
  return { type, keyword: nextValue ?? current.keyword ?? "" };
}

function scheduleDraftFromCronExpression(expression: string): Omit<ScheduleDraft, "timezone"> {
  const fallback = { mode: "cron" as const, interval: 1, minute: 0, hour: 9, dayOfWeek: 1, expression };
  const parts = expression.trim().split(/\s+/);
  if (parts.length !== 5) return fallback;

  const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
  const parsedMinute = parseCronNumber(minute, 0, 59);
  if (parsedMinute === null || month !== "*") return fallback;

  const hourStep = hour.match(/^\*\/(\d+)$/);
  if (hourStep && dayOfMonth === "*" && dayOfWeek === "*") {
    return { mode: "hours", interval: clamp(Number(hourStep[1]), 1, 23), minute: parsedMinute, hour: 9, dayOfWeek: 1, expression };
  }

  const parsedHour = parseCronNumber(hour, 0, 23);
  if (parsedHour === null) return fallback;

  const dayStep = dayOfMonth.match(/^\*\/(\d+)$/);
  if (dayStep && dayOfWeek === "*") {
    return { mode: "days", interval: clamp(Number(dayStep[1]), 1, 31), minute: parsedMinute, hour: parsedHour, dayOfWeek: 1, expression };
  }

  const parsedDayOfWeek = parseCronNumber(dayOfWeek, 0, 7);
  if (dayOfMonth === "*" && parsedDayOfWeek !== null) {
    return { mode: "weeks", interval: 1, minute: parsedMinute, hour: parsedHour, dayOfWeek: parsedDayOfWeek === 0 ? 7 : parsedDayOfWeek, expression };
  }

  return fallback;
}

function cronExpressionForSchedule(draft: ScheduleDraft): string {
  if (!draft.timezone.trim()) throw new Error("Schedule timezone is required.");

  if (draft.mode === "cron") {
    const expression = draft.expression.trim();
    if (expression.split(/\s+/).length !== 5) throw new Error("Cron expression must have 5 fields.");
    return expression;
  }

  const interval = clamp(Math.trunc(draft.interval), 1, scheduleIntervalMax(draft.mode));
  const minute = clamp(Math.trunc(draft.minute), 0, 59);
  if (draft.mode === "hours") {
    return `${minute} */${interval} * * *`;
  }

  const hour = clamp(Math.trunc(draft.hour), 0, 23);
  if (draft.mode === "days") {
    return `${minute} ${hour} */${interval} * *`;
  }

  const dayOfWeek = clamp(Math.trunc(draft.dayOfWeek), 1, 7);
  if (interval === 1) {
    return `${minute} ${hour} * * ${dayOfWeek}`;
  }
  return `${minute} ${hour} */${interval * 7} * *`;
}

function cronExpressionPreview(draft: ScheduleDraft): string {
  try {
    return cronExpressionForSchedule(draft);
  } catch {
    return "Invalid schedule";
  }
}

function scheduleIntervalMax(mode: ScheduleMode): number {
  if (mode === "hours") return 23;
  if (mode === "weeks") return 4;
  return 31;
}

function parseCronNumber(value: string, min: number, max: number): number | null {
  if (!/^\d+$/.test(value)) return null;
  const parsed = Number(value);
  if (!Number.isFinite(parsed) || parsed < min || parsed > max) return null;
  return parsed;
}

function numberFromInput(value: string, fallback: number): number {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallback;
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value));
}

function timeInputValue(hour: number, minute: number): string {
  return `${String(clamp(Math.trunc(hour), 0, 23)).padStart(2, "0")}:${String(clamp(Math.trunc(minute), 0, 59)).padStart(2, "0")}`;
}

function parseTimeInput(value: string): { hour: number; minute: number } {
  const [hour = "0", minute = "0"] = value.split(":");
  return {
    hour: clamp(numberFromInput(hour, 0), 0, 23),
    minute: clamp(numberFromInput(minute, 0), 0, 59),
  };
}

function weekdays(): Array<{ value: number; label: string }> {
  return [
    { value: 1, label: "Monday" },
    { value: 2, label: "Tuesday" },
    { value: 3, label: "Wednesday" },
    { value: 4, label: "Thursday" },
    { value: 5, label: "Friday" },
    { value: 6, label: "Saturday" },
    { value: 7, label: "Sunday" },
  ];
}

function sortAgents(records: AgentRecord[]): AgentRecord[] {
  return [...records].sort((left, right) => {
    const leftMaster = isMasterAgent(left);
    const rightMaster = isMasterAgent(right);
    if (leftMaster !== rightMaster) return leftMaster ? -1 : 1;
    return 0;
  });
}

function isMasterAgent(record: AgentRecord): boolean {
  return record.agent.name.trim().toLowerCase() === "master";
}

function shortId(id: string): string {
  if (id.length <= 12) return id;
  return `${id.slice(0, 8)}...${id.slice(-4)}`;
}

function formatDate(value: string): string {
  return new Intl.DateTimeFormat(undefined, {
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  }).format(new Date(value));
}

function formatDateTime(value: string): string {
  return new Intl.DateTimeFormat(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  }).format(new Date(value));
}

function trimTrailingSlash(value: string): string {
  return value.endsWith("/") ? value.slice(0, -1) : value;
}

function toggleArrayValue(values: string[], value: string): string[] {
  return values.includes(value) ? values.filter((item) => item !== value) : [...values, value];
}

function chatKey(agentId: string, environmentId: string, vaultIds: string[]): string {
  return `${agentId}:${environmentId}:${[...vaultIds].sort().join(",")}`;
}

function defaultEnvironmentConfig(kind: EnvironmentKind): JsonObject {
  if (kind === "self_hosted") {
    return { type: "self_hosted" };
  }

  return {
    type: "cloud",
    networking: { type: "unrestricted" },
    packages: { type: "packages", apt: [], cargo: [], gem: [], go: [], npm: [], pip: [] },
  };
}

function environmentPackageSummary(environment: AnthropicEnvironment): string {
  const packages = isRecord(environment.config.packages) ? environment.config.packages : null;
  if (!packages) return "";

  const parts = ["apt", "cargo", "gem", "go", "npm", "pip"].flatMap((manager) => {
    const values = packages[manager];
    return Array.isArray(values) && values.length > 0 ? [`${manager} ${values.length}`] : [];
  });
  return parts.join(" · ");
}

function themeVariables(): React.CSSProperties & Record<`--${string}`, string | number> {
  return {
    "--page-bg": componentRecipe.page.background,
    "--canvas-bg": appShellRecipe.mainBackground,
    "--surface-bg": componentRecipe.card.background,
    "--surface-hover": designTokens.rawColors.background.surfaceHover,
    "--surface-selected": designTokens.rawColors.background.surfaceSelected,
    "--primary-text": componentRecipe.page.color,
    "--secondary-text": designTokens.colors.secondaryText,
    "--placeholder-text": designTokens.colors.placeholderText,
    "--inverse-text": designTokens.rawColors.foreground.inverse,
    "--brand": designTokens.colors.brand,
    "--brand-hover": designTokens.rawColors.brand.hover,
    "--brand-surface": appShellRecipe.activeNavBackground,
    "--brand-border": designTokens.rawColors.brand.border,
    "--border": designTokens.colors.border,
    "--border-subtle": designTokens.colors.headerBorder,
    "--border-hover": designTokens.rawColors.border.hover,
    "--focus-ring": designTokens.colors.focusRing,
    "--primary-button-bg": buttonRecipe.primary.background,
    "--primary-button-hover": String(buttonRecipe.primary.hoverBackground),
    "--primary-button-active": String(buttonRecipe.primary.activeBackground),
    "--primary-button-text": buttonRecipe.primary.color,
    "--secondary-button-bg": buttonRecipe.secondary.background,
    "--secondary-button-text": buttonRecipe.secondary.color,
    "--secondary-button-border": designTokens.rawColors.button.secondaryBorder,
    "--secondary-button-hover-border": String(buttonRecipe.secondary.hoverBorderColor),
    "--secondary-button-active-bg": String(buttonRecipe.secondary.activeBackground),
    "--disabled-bg": String(buttonRecipe.primary.disabledBackground),
    "--disabled-text": String(buttonRecipe.primary.disabledColor),
    "--danger-bg": buttonRecipe.dangerPrimary.background,
    "--danger-hover": String(buttonRecipe.dangerPrimary.hoverBackground),
    "--danger-active": String(buttonRecipe.dangerPrimary.activeBackground),
    "--danger-text": buttonRecipe.dangerPrimary.color,
    "--danger-soft": badgeRecipe.danger.background,
    "--danger-fg": designTokens.rawColors.semantic.danger.fg,
    "--success-soft": badgeRecipe.success.background,
    "--success-fg": badgeRecipe.success.color,
    "--info-soft": badgeRecipe.info.background,
    "--info-fg": badgeRecipe.info.color,
    "--input-bg": inputRecipe.default.background,
    "--input-border": designTokens.rawColors.border.default,
    "--input-active-bg": designTokens.rawColors.background.searchActive,
    "--card-radius": `${cardRecipe.default.borderRadius}px`,
    "--modal-radius": `${overlayRecipe.modal.borderRadius}px`,
    "--button-radius": `${buttonRecipe.primary.borderRadius}px`,
    "--input-radius": `${inputRecipe.default.borderRadius}px`,
    "--badge-radius": `${badgeRecipe.info.borderRadius}px`,
    "--button-height": `${buttonRecipe.primary.minHeight}px`,
    "--input-height": `${inputRecipe.default.minHeight}px`,
    "--sidebar-width": `${appShellRecipe.sidebarWidth}px`,
    "--modal-shadow": overlayRecipe.modal.boxShadow,
    "--popover-shadow": overlayRecipe.toast.boxShadow,
    "--table-header-bg": tableRecipe.header.background,
    "--table-row-border": tableRecipe.row.borderBottom,
    "--table-row-hover": tableRecipe.row.hoverBackground,
    "--motion-hover": designTokens.motion.hover,
    "--motion-base": designTokens.motion.base,
    "--font-product": `${designTokens.typography.fontFamily}, Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`,
    "--font-mono": `${designTokens.typography.raw.mono.family}, "SFMono-Regular", Consolas, "Liberation Mono", monospace`,
  };
}

const rootElement = document.getElementById("root")!;
rootElement.dataset.build = "2026-07-15-routing-cache-fix";

createRoot(rootElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);
