/* global React, Button, Icon, LoadingSpinner, Pill, StatusDot, EmptyState, Drawer, PlanLimitMeter, ConfirmDialog, Select, useApiResource, apiFetch, fetchOAuthProviders, startOAuthConnect, startOAuthReauthorize, formatDateTime, formatRelative, useToast, StatusBadge, useAuth, requirePaidAction, canUseTenantAction, tenantActionDisabledReason, getPlanLimitState */
const { useEffect: useEffectMcp, useMemo: useMemoMcp, useRef: useRefMcp, useState: useStateMcp } = React;
const TOOL_MONITOR_FLIP_MOVER_DURATION_MS = 950;
const TOOL_MONITOR_FLIP_SIBLING_DURATION_MS = 760;
const TOOL_MONITOR_FLIP_HOLD_DURATION_MS = 1120;

const CLI_STATUS_LABELS = {
  pending: 'Queued',
  provisioning: 'Provisioning',
  discovering: 'Discovering',
  auth_required: 'Auth required',
  needs_review: 'Ready',
  ready: 'Ready',
  failed: 'Failed',
  archived: 'Archived',
};
const SUCCESS_CONNECTION_STATUSES = new Set(['success', 'ok', 'completed']);
const DISCOVERY_TERMINAL_STATUSES = new Set(['completed', 'failed', 'canceled']);

function cliStatusLabel(status) {
  return CLI_STATUS_LABELS[status] || status || 'Unknown';
}

function cliStatusTone(status) {
  if (status === 'ready' || status === 'needs_review') return 'ok';
  if (status === 'failed' || status === 'archived') return 'bad';
  if (status === 'pending' || status === 'provisioning' || status === 'discovering' || status === 'auth_required') return 'warn';
  return 'neutral';
}

function formatCliRiskLabel(risk) {
  if (risk === 'read') return 'Read-only';
  if (risk === 'mutating') return 'Side-effecting';
  return 'Risk unknown';
}

function cliRiskBadgeClass(risk) {
  if (risk === 'read') return 'badge-success';
  if (risk === 'mutating') return 'badge-fail';
  return 'badge-neutral';
}

function renderCliHelpDetails(help, fallbackExcerpt = '', { suppressSummary = false } = {}) {
  if (!help || typeof help !== 'object') {
    const legacy = String(fallbackExcerpt || '').trim();
    return legacy ? <pre className="mcp-command-review-help mcp-command-review-help-legacy">{legacy.slice(0, 420)}</pre> : null;
  }
  const args = Array.isArray(help.arguments) ? help.arguments : [];
  const options = Array.isArray(help.options) ? help.options : [];
  const showSummary = !suppressSummary && Boolean(help.summary);
  if (!help.usage && !showSummary && args.length === 0 && options.length === 0) {
    const legacy = String(fallbackExcerpt || '').trim();
    return legacy ? <pre className="mcp-command-review-help mcp-command-review-help-legacy">{legacy.slice(0, 420)}</pre> : null;
  }
  return (
    <div className="mcp-command-review-help">
      {help.usage && <div className="mcp-command-review-usage">{help.usage}</div>}
      {showSummary && <div className="mcp-command-review-summary">{help.summary}</div>}
      {args.length > 0 && (
        <div className="mcp-command-review-section">
          <div className="mcp-command-review-heading">Arguments</div>
          {args.map((arg) => (
            <div key={arg.name} className="mcp-command-review-help-row">
              <span>{arg.name}</span>
              {arg.description && <span>{arg.description}</span>}
            </div>
          ))}
        </div>
      )}
      {options.length > 0 && (
        <div className="mcp-command-review-section">
          <div className="mcp-command-review-heading">Options</div>
          {options.map((option) => {
            const flag = option.valueName ? `${option.flag} <${option.valueName}>` : option.flag;
            return (
              <div key={option.name || option.flag} className="mcp-command-review-help-row">
                <span>{flag}</span>
                {option.description && <span>{option.description}</span>}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

function isConnectionSuccessStatus(status) {
  return SUCCESS_CONNECTION_STATUSES.has(status);
}

function hostedRuntimeDiagnostic(detail) {
  const text = String(detail || '').trim();
  const match = text.match(/Sandbox stderr(?: \(tail\))?:\s*([\s\S]+)/i);
  const diagnostic = String(match?.[1] || '').replace(/\s+/g, ' ').trim();
  if (!diagnostic) return '';
  return diagnostic.length > 240 ? `${diagnostic.slice(0, 240)}...` : diagnostic;
}

function targetUserErrorMessage(errorCode, targetKind = 'mcp', context = 'runtime', detail = '') {
  const code = String(errorCode || '').toLowerCase();
  const diagnostic = hostedRuntimeDiagnostic(detail);
  if (context === 'provisioning') {
    if (/package_lifecycle_scripts_blocked|lifecycle scripts?|preinstall|postinstall/.test(code)) return 'This package needs install lifecycle scripts, which are disabled in hosted setup. Choose a package that installs without lifecycle scripts or provide a prebuilt CLI package.';
    if (/package_not_found|npm e404|404 not found/.test(code)) return 'The npm package or pinned version could not be found. Check the package name and exact version.';
    if (/package_integrity_mismatch|eintegrity|integrity checksum/.test(code)) return 'The npm package failed integrity verification. Retry after confirming the package version in the registry.';
    if (/package_network_timeout|etimedout|econnreset|enotfound|network timeout/.test(code)) return 'The package install could not reach the npm registry before timing out. Retry provisioning.';
    if (/package_install_failed|npm err|install failed/.test(code)) return 'npm install failed for this package. Check that the package can install with lifecycle scripts disabled.';
  }
  if (targetKind === 'cli') {
    if (/auth|401|403|unauthorized|forbidden/.test(code)) return 'Authentication failed. Check the configured secret and environment variable.';
    if (/timeout|timed_out/.test(code)) return 'The CLI did not respond before the timeout.';
    if (/artifact_not_ready/.test(code)) return 'The CLI package is still installing. Command discovery will run after setup finishes.';
    if (/package_bin_not_found|node_modules\/\.bin/.test(code)) return 'The package does not expose the configured command bin. Check the bin name published by the npm package.';
    if (/not_found|enoent|command/.test(code)) return 'The configured CLI command could not be found in the hosted runtime.';
    return 'We could not finish setting up this CLI. Check the package, version, command name, and whether it can run with --help.';
  }
  if (/package_bin_not_found|node_modules\/\.bin/.test(code)) return 'The package does not expose the configured command bin. Check the bin name published by the npm package.';
  if (/package_crashed_at_startup/.test(code)) {
    return diagnostic
      ? `The hosted package exited before tools could be discovered. Runtime stderr: ${diagnostic}`
      : 'The hosted package exited before tools could be discovered. The package installed, but its MCP process closed during startup; retry provisioning or check its startup requirements.';
  }
  if (/auth|401|403|unauthorized|forbidden/.test(code)) return "Authentication failed. Check this server's auth profile.";
  if (/timeout|timed_out/.test(code)) return 'The MCP server did not respond before the timeout.';
  if (/network|connection|closed|econn|unreachable/.test(code)) return 'We could not reach this MCP server. Check the URL and server availability.';
  return "We could not complete the connection test. Check this target's configuration and try again.";
}

const CLI_PENDING_STATUSES = new Set(['pending', 'provisioning', 'discovering']);
const EXACT_SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/;
// Pinned to four phases that match the backend provisioning state machine.
// The timeline CSS in styles.css (`.cli-setup-timeline-track` insets,
// `.cli-setup-timeline-nodes` grid columns) hardcodes 4 — keep these in sync
// if the phase set ever changes.
const CLI_SETUP_STEPS = [
  { status: 'pending', label: 'Queued' },
  { status: 'provisioning', label: 'Installing package' },
  { status: 'discovering', label: 'Discovering commands' },
  { status: 'ready', label: 'Commands ready' },
];

function isExactSemver(value) {
  return EXACT_SEMVER_RE.test(value);
}


// Translate the provisioning run row into one of the four UI step statuses.
// The server row's `provisioning_status` already exposes the phase when
// available; this is the fallback when only the run row is fetched.
function mapProvisioningRunToStatus(runStatus, phase) {
  if (runStatus === 'failed' || runStatus === 'canceled') return 'failed';
  if (phase === 'discovering') return 'discovering';
  if (runStatus === 'completed') return 'discovering';
  if (phase === 'provisioning' || phase === 'installing') return 'provisioning';
  if (runStatus === 'running') return 'provisioning';
  return 'pending';
}
function cliSetupCopy(status, setupKind = 'cli', errorCode = null) {
  const isCliSetup = setupKind === 'cli';
  if (status === 'auth_required') {
    return {
      title: isCliSetup ? 'Authentication required' : 'Connect OAuth to finish setup',
      body: isCliSetup
        ? 'Setup paused because this CLI needs authentication before commands can be discovered.'
        : 'Setup paused until OAuth is connected. Open the target to finish authorization, then provisioning can continue.',
      tone: 'warn',
    };
  }
  if (status === 'failed') {
    return {
      title: 'Setup failed',
      body: errorCode
        ? targetUserErrorMessage(errorCode, isCliSetup ? 'cli' : 'mcp', 'provisioning')
        : isCliSetup
          ? 'We could not finish setting up this CLI. Check the package, version, command name, and whether it can run with --help.'
          : 'We could not finish setting up this MCP server. Check the package, version, command name, and whether it exposes MCP tools.',
      tone: 'bad',
    };
  }
  if (status === 'ready' || status === 'needs_review') {
    return {
      title: isCliSetup ? 'CLI ready' : 'MCP server ready',
      body: isCliSetup
        ? 'Setup finished and commands are ready to use.'
        : 'Setup finished and tools are ready to use.',
      tone: 'ok',
    };
  }
  if (status === 'discovering') {
    return {
      title: isCliSetup ? 'Discovering commands' : 'Discovering tools',
      body: isCliSetup
        ? 'The package installed. We are reading the CLI help output and building the command catalog.'
        : 'The package installed. We are reading the MCP tool list and building the tool catalog.',
      tone: 'warn',
    };
  }
  if (status === 'provisioning') {
    return {
      title: 'Installing package',
      body: 'We are installing the pinned npm package in a hosted runtime.',
      tone: 'warn',
    };
  }
  return {
    title: 'Setup queued',
    body: isCliSetup
      ? 'Provisioning has started. You can keep this open while we prepare the CLI.'
      : 'Provisioning has started. You can keep this open while we prepare the MCP server.',
    tone: 'warn',
  };
}

const HOSTED_MCP_SOURCE_REMOTE = 'remote_url';
const HOSTED_MCP_SOURCE_NPM = 'npm_package';

const HOSTED_OAUTH_ENV_BY_PROVIDER = {
  allowance: 'ALLOWANCE_ACCESS_TOKEN',
  linear: 'LINEAR_ACCESS_TOKEN',
};
const HOSTED_ENV_VAR_RE = /^[A-Z_][A-Z0-9_]*$/;
const RESERVED_HOSTED_ENV_VARS = new Set([
  'AWS_ACCESS_KEY_ID',
  'AWS_SECRET_ACCESS_KEY',
  'AWS_SESSION_TOKEN',
  'PATH',
  'HOME',
  'SHELL',
  'NODE_OPTIONS',
]);
const HOSTED_AUTH_ENV_DEFAULTS = new Set([
  'ACME_API_KEY',
  'ACME_ACCESS_TOKEN',
  ...Object.values(HOSTED_OAUTH_ENV_BY_PROVIDER),
]);

function hostedOauthEnvVar(providerSlug) {
  return HOSTED_OAUTH_ENV_BY_PROVIDER[String(providerSlug || '').toLowerCase()] || 'ACME_ACCESS_TOKEN';
}

function validateHostedEnvVar(value) {
  const envVar = String(value || '').trim();
  if (!envVar) return 'Environment variable is required for hosted MCP authentication';
  if (!HOSTED_ENV_VAR_RE.test(envVar)) return 'Environment variable must use uppercase letters, numbers, and underscores';
  if (RESERVED_HOSTED_ENV_VARS.has(envVar)) return 'That environment variable name is reserved';
  return '';
}

function defaultAuthProfileForServer(server, authProfiles) {
  if (!server) return null;
  if (server.default_auth_profile) return server.default_auth_profile;
  const profiles = Array.isArray(server.auth_profiles) ? server.auth_profiles : authProfiles;
  if (!Array.isArray(profiles)) return null;
  return profiles.find((profile) => profile?.is_default) || null;
}

const OAUTH_POPUP_NAME_PREFIX = 'armature-mcp-oauth-';
const OAUTH_POPUP_FEATURES = 'width=920,height=760';
const OAUTH_POPUP_POLL_MS = 500;
const OAUTH_BROADCAST_CHANNEL = 'armature:mcp-oauth';
const MCP_OAUTH_SAME_TAB_FLOW_STORAGE_KEY = 'armature:mcp-oauth-same-tab-flow';

function generateOauthFlowId() {
  // crypto.randomUUID is supported wherever BroadcastChannel is, but fall
  // back to a Math.random-based id so we never throw if the dashboard ever
  // ends up running in a context that doesn't expose it.
  try {
    if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
      return crypto.randomUUID();
    }
  } catch (_error) {}
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}

function payloadToOutcome(data) {
  if (!data) return null;
  if (data.type === 'armature:mcp-oauth-connected') {
    return { outcome: 'connected', profileId: data.profileId || null };
  }
  if (data.type === 'armature:mcp-oauth-failed') {
    return {
      outcome: 'failed',
      code: data.code || 'oauth_error',
      message: data.message || 'OAuth authorization failed.',
    };
  }
  if (data.type === 'armature:mcp-oauth-pending') {
    return {
      outcome: 'pending',
      reason: data.reason || 'oauth_pending',
      profileId: data.profileId || null,
    };
  }
  return null;
}

// Open the OAuth authorize URL in a named popup, focus it, and resolve once
// the popup either posts a result back (via the bootstrap shim in
// Armature.html that runs when our /?oauth=... callback page loads) or is
// closed by the user. Keeping the popup separate from the original tab is
// what lets the user resume the rest of the connect wizard (probe, tool
// monitor selection) once the provider is done.
//
// Returns one of:
//   { outcome: 'connected', profileId }
//   { outcome: 'failed', code, message }
//   { outcome: 'pending', reason, profileId }
//   { outcome: 'closed' }                    // user closed popup before we got a result
//   { outcome: 'navigated' }                 // popup blocked, current tab navigated instead
//
// `options.onPopupOpened(popup)` lets the caller hold the popup reference
// (e.g. to wire a "Cancel" button in the parent UI to popup.close()).
async function openOAuthPopup(authorizationUrl, options) {
  const onPopupOpened = options && options.onPopupOpened;
  // Each call gets a unique flow id so concurrent OAuth flows (e.g. user
  // starts a connect popup, then triggers a reconnect from the slideover
  // before finishing the first one) don't cross-resolve each other's
  // promises. Two discriminators are needed:
  //   - `event.source === popup` for the postMessage path.
  //   - `data.flowId === flowId` for the BroadcastChannel path, since BC
  //     events have no `source` we can compare against.
  //
  // The flow id ALWAYS comes from the server when available (`startResult.
  // clientFlowId`, plumbed through `options.flowId`). The server stitches
  // it back into the popup's `/?oauth=...` redirect URL (via the OAuth
  // state row id) so the shim can read it from `URLSearchParams` even when
  // the OAuth provider ships `Cross-Origin-Opener-Policy: same-origin` —
  // COOP triggers a browsing-context-group switch that wipes
  // `window.name`, which would otherwise be the only place the shim could
  // recover the flow id. Notion is the canonical example.
  // We still echo flowId into `window.name` as a best-effort fallback for
  // providers that don't ship COOP and the legacy pre-server-echo path.
  const flowId = (options && options.flowId) || generateOauthFlowId();
  const popupName = `${OAUTH_POPUP_NAME_PREFIX}${flowId}`;
  // Security trade-off: we deliberately omit `noopener` here so we can keep
  // the `Window` reference (needed to detect popup blockers via `null`,
  // call `popup.focus()`, poll `popup.closed`, and route the postMessage
  // result via `event.source`). The cost is that pages loaded inside the
  // popup — including the OAuth provider's own pages — can read
  // `window.opener`. The provider can't read our DOM (cross-origin), and
  // since Chrome 88 / equivalent, cross-origin opener navigation
  // (`window.opener.location = ...`) is also blocked, so the practical
  // attack surface is limited to providers we already trust to host an
  // authorization endpoint. The postMessage handshake is locked down with
  // `event.source === popup` and `event.origin === window.location.origin`
  // checks below; the BroadcastChannel handshake is implicitly same-origin.
  const popup = window.open(authorizationUrl, popupName, OAUTH_POPUP_FEATURES);
  if (!popup) {
    try { window.sessionStorage?.setItem(MCP_OAUTH_SAME_TAB_FLOW_STORAGE_KEY, flowId); } catch (_error) {}
    window.location.href = authorizationUrl;
    return { outcome: 'navigated' };
  }
  try { popup.focus(); } catch (_error) {}
  if (typeof onPopupOpened === 'function') {
    try { onPopupOpened(popup); } catch (_error) {}
  }

  return new Promise((resolve) => {
    let resolved = false;
    let pollerId = 0;
    // BroadcastChannel is the primary completion signal. It works
    // regardless of whether window.opener survives — many providers ship
    // Cross-Origin-Opener-Policy: same-origin on their login pages, which
    // severs the opener relationship permanently even after the popup
    // navigates back to our origin. postMessage-via-opener silently drops
    // in that case, but BroadcastChannel keeps working since both contexts
    // are on our origin.
    let channel = null;
    try {
      if (typeof BroadcastChannel !== 'undefined') channel = new BroadcastChannel(OAUTH_BROADCAST_CHANNEL);
    } catch (_error) {}

    function settle(result) {
      if (resolved) return;
      resolved = true;
      window.removeEventListener('message', onMessage);
      if (channel) {
        try { channel.removeEventListener('message', onChannelMessage); } catch (_error) {}
        try { channel.close(); } catch (_error) {}
        channel = null;
      }
      if (pollerId) {
        try { window.clearInterval(pollerId); } catch (_error) {}
        pollerId = 0;
      }
      try { if (!popup.closed) popup.close(); } catch (_error) {}
      try { window.focus(); } catch (_error) {}
      resolve(result);
    }

    function onMessage(event) {
      if (event.source !== popup) return;
      if (event.origin !== window.location.origin) return;
      const result = payloadToOutcome(event.data);
      if (result) settle(result);
    }

    function onChannelMessage(event) {
      const data = event && event.data;
      if (!data) return;
      // Strict per-flow filter: the BroadcastChannel is global to the
      // origin, so messages from a sibling popup must be ignored. The shim
      // always echoes back the flow id encoded in window.name, so the
      // absence of `flowId` is itself disqualifying.
      if (data.flowId !== flowId) return;
      const result = payloadToOutcome(data);
      if (result) settle(result);
    }

    window.addEventListener('message', onMessage);
    if (channel) {
      try { channel.addEventListener('message', onChannelMessage); } catch (_error) {}
    }
    pollerId = window.setInterval(() => {
      if (!popup.closed) return;
      settle({ outcome: 'closed' });
    }, OAUTH_POPUP_POLL_MS);
  });
}

// ----- Loopback paste-back OAuth flow ----------------------------------
// Some MCP authorization servers (Vercel) restrict redirect URIs to loopback
// (`http://127.0.0.1:*` only). The /start API registers a loopback DCR client
// and signals `redirectUriMode === 'loopback'`. Since the user's browser
// can't reach that loopback after the AS redirect, we open the authorize URL
// in a popup and ask the user to paste the resulting URL back here — we
// can't auto-extract it because the popup is on a cross-origin "site can't
// be reached" page. The pasted URL goes to
// /api/auth-profiles/oauth/paste-callback on the server, which extracts
// code+state and runs the same token-exchange path as the regular GET
// callback.
//
// The modal is two-phase so the user reads the instructions BEFORE the
// popup opens (otherwise they'd be looking at the consent screen by the
// time the directions render and might miss the "copy the address bar"
// step). Phase 1 is "ready" — instructions + Open sign-in button. Phase 2
// is "awaiting_paste" — popup is open, paste textarea is focused, and the
// user can re-open sign-in if they forgot to copy the URL the first time.
//
// This is implemented as a module-scoped imperative controller (registered
// by `<LoopbackOAuthModalRoot />`) so any descendant component can call
// `presentLoopbackOAuthFlow(url)` without threading props through the tree.
let activeLoopbackOAuthController = null;

const LOOPBACK_POPUP_NAME = 'armature-mcp-oauth-loopback';
const LOOPBACK_POPUP_FEATURES = 'width=920,height=760';
const LOOPBACK_POPUP_POLL_MS = 500;
const LOOPBACK_PASTE_PREFIX = 'http://127.0.0.1:';

function setLoopbackOAuthController(controller) {
  activeLoopbackOAuthController = controller;
}

function presentLoopbackOAuthFlow(authorizationUrl) {
  if (!activeLoopbackOAuthController) {
    return Promise.resolve({
      outcome: 'failed',
      code: 'loopback_modal_unmounted',
      message: 'OAuth paste-back UI is not mounted; refresh the page and try again.',
    });
  }
  return activeLoopbackOAuthController.present(authorizationUrl);
}

// Single entry point used by the connect / reconnect / hosted-OAuth flows.
// Branches on `redirectUriMode` from /start so callers don't have to. The
// loopback flow does NOT auto-open a window: the modal opens first so the
// user can read the instructions, and the popup is opened by the user's
// own click inside the modal. That click also serves as the user-gesture
// the popup blocker requires.
async function runMcpOAuthFlow(startResult, options) {
  if (startResult?.redirectUriMode === 'loopback' && startResult.authorizationUrl) {
    return presentLoopbackOAuthFlow(startResult.authorizationUrl);
  }
  // Forward the server-issued `clientFlowId` so the BroadcastChannel
  // listener can match the result that flows back via the popup-shim path
  // even when the OAuth provider's COOP wipes `window.name` mid-flow.
  return openOAuthPopup(startResult.authorizationUrl, {
    ...(options || {}),
    flowId: startResult?.clientFlowId || (options && options.flowId) || null,
  });
}

function LoopbackOAuthModalRoot() {
  const [pending, setPending] = useStateMcp(null);
  const [phase, setPhase] = useStateMcp('ready'); // 'ready' | 'awaiting_paste'
  const [pastedUrl, setPastedUrl] = useStateMcp('');
  const [submitting, setSubmitting] = useStateMcp(false);
  const [errorMessage, setErrorMessage] = useStateMcp('');
  const [popupClosedHint, setPopupClosedHint] = useStateMcp(false);
  const [clipboardError, setClipboardError] = useStateMcp('');
  const textareaRef = useRefMcp(null);
  const popupRef = useRefMcp(null);
  const popupPollRef = useRefMcp(0);

  function clearPopupPoller() {
    if (popupPollRef.current) {
      try { window.clearInterval(popupPollRef.current); } catch (_error) {}
      popupPollRef.current = 0;
    }
  }

  function watchPopup(popup) {
    clearPopupPoller();
    popupRef.current = popup;
    setPopupClosedHint(false);
    if (!popup) return;
    popupPollRef.current = window.setInterval(() => {
      if (!popup.closed) return;
      clearPopupPoller();
      popupRef.current = null;
      setPopupClosedHint(true);
    }, LOOPBACK_POPUP_POLL_MS);
  }

  useEffectMcp(() => {
    setLoopbackOAuthController({
      present: (authorizationUrl) => new Promise((resolve) => {
        // Resolve any in-flight prior present() before overwriting it so the
        // caller's awaited promise doesn't orphan. Concurrent presentations
        // are rare in practice (the connect/reconnect buttons are disabled
        // while an OAuth flow is running) but possible across separate
        // descendants (e.g. TargetSourcesPage + an open McpServerDetail).
        setPending((prev) => {
          if (prev?.resolve) {
            try {
              prev.resolve({ outcome: 'superseded' });
            } catch (_error) {
              // Ignored: previous caller already cleaned up.
            }
          }
          return { authorizationUrl, resolve };
        });
        setPhase('ready');
        setPastedUrl('');
        setErrorMessage('');
        setClipboardError('');
        setSubmitting(false);
        setPopupClosedHint(false);
        clearPopupPoller();
        popupRef.current = null;
      }),
    });
    return () => {
      setLoopbackOAuthController(null);
      clearPopupPoller();
    };
  }, []);

  useEffectMcp(() => {
    if (phase === 'awaiting_paste' && textareaRef.current) {
      try { textareaRef.current.focus(); } catch (_error) {}
    }
  }, [phase]);

  useEscapeToClose({
    enabled: Boolean(pending),
    disabled: submitting,
    onClose: () => finalize({ outcome: 'closed' }),
  });

  if (!pending) return null;

  function finalize(outcome) {
    const resolve = pending.resolve;
    clearPopupPoller();
    try { popupRef.current?.close?.(); } catch (_error) {}
    popupRef.current = null;
    setPending(null);
    setPhase('ready');
    setPastedUrl('');
    setErrorMessage('');
    setClipboardError('');
    setSubmitting(false);
    setPopupClosedHint(false);
    resolve(outcome);
  }

  function openSignInPopup() {
    setClipboardError('');
    setErrorMessage('');
    setPopupClosedHint(false);
    let popup = null;
    try {
      // Use a sized popup (not _blank) so it sits next to the modal and the
      // user can see the modal's "paste below" instructions while reading
      // the address bar in the popup. The popup blocker only allows
      // window.open from a synchronous user gesture, which the click on
      // this button satisfies.
      popup = window.open(pending.authorizationUrl, LOOPBACK_POPUP_NAME, LOOPBACK_POPUP_FEATURES);
    } catch (_error) {
      popup = null;
    }
    if (!popup) {
      // Popup blocker swallowed it. Fall back to a new tab so the user can
      // still complete the flow; they'll need to alt-tab back to paste.
      try { window.open(pending.authorizationUrl, '_blank', 'noopener'); } catch (_error) {}
    } else {
      try { popup.focus(); } catch (_error) {}
      watchPopup(popup);
    }
    setPhase('awaiting_paste');
  }

  async function pasteFromClipboard() {
    setClipboardError('');
    setErrorMessage('');
    if (!navigator?.clipboard?.readText) {
      setClipboardError("Your browser doesn't expose clipboard access. Paste manually with Cmd/Ctrl+V.");
      return;
    }
    try {
      const text = await navigator.clipboard.readText();
      const trimmed = String(text || '').trim();
      if (!trimmed) {
        setClipboardError('Clipboard is empty.');
        return;
      }
      setPastedUrl(trimmed);
      if (!trimmed.startsWith(LOOPBACK_PASTE_PREFIX)) {
        setClipboardError(`That doesn't look like the loopback URL. It should start with ${LOOPBACK_PASTE_PREFIX}`);
      }
    } catch (error) {
      setClipboardError(error?.message || 'Could not read from clipboard. Paste manually with Cmd/Ctrl+V.');
    }
  }

  async function handleSubmit(event) {
    event?.preventDefault?.();
    const trimmed = pastedUrl.trim();
    if (!trimmed) {
      setErrorMessage('Paste the URL from the popup\'s address bar first.');
      return;
    }
    setSubmitting(true);
    setErrorMessage('');
    setClipboardError('');
    try {
      const result = await apiFetch('/api/auth-profiles/oauth/paste-callback', {
        method: 'POST',
        body: JSON.stringify({ pastedUrl: trimmed }),
      });
      finalize({
        outcome: 'connected',
        profileId: result.profileId || null,
        hostedDiscoveryDeferred: result.hostedDiscoveryDeferred || null,
      });
    } catch (error) {
      setErrorMessage(error?.message || 'Could not complete OAuth.');
      setSubmitting(false);
    }
  }

  const isReadyPhase = phase === 'ready';
  const pasteLooksValid = pastedUrl.trim().startsWith(LOOPBACK_PASTE_PREFIX);
  const subtitle = isReadyPhase
    ? "This provider only accepts a localhost callback, so we can't auto-detect when sign-in finishes. Read the steps below before opening the popup."
    : 'Approve the permissions in the popup, copy the address-bar URL, then paste it below.';

  const modal = (
    <div
      className="dialog-backdrop"
      onMouseDown={(event) => {
        if (event.target === event.currentTarget && !submitting) finalize({ outcome: 'closed' });
      }}>
      <form
        className="modal-panel mcp-loopback-oauth-modal"
        role="dialog"
        aria-modal="true"
        aria-labelledby="mcp-loopback-oauth-title"
        onSubmit={handleSubmit}>
        <div className="modal-header">
          <div>
            <div className="modal-title" id="mcp-loopback-oauth-title">
              {isReadyPhase ? 'Manual sign-in required' : 'Paste the redirect URL'}
            </div>
            <div className="modal-subtitle">{subtitle}</div>
          </div>
          <button
            className="icon-btn"
            type="button"
            aria-label="Cancel"
            disabled={submitting}
            onClick={() => finalize({ outcome: 'closed' })}>
            <Icon name="x" size={15} />
          </button>
        </div>
        <div className="modal-body mcp-loopback-oauth-body">
          <ol className="mcp-loopback-oauth-steps">
            <li>
              <div className="mcp-loopback-step-title">Click "Open sign-in"</div>
              <div className="mcp-loopback-step-desc">
                A small window will pop up with the provider's sign-in page.
              </div>
            </li>
            <li>
              <div className="mcp-loopback-step-title">Approve the permissions</div>
              <div className="mcp-loopback-step-desc">
                Sign in and grant access in the popup.
              </div>
            </li>
            <li>
              <div className="mcp-loopback-step-title">Copy the URL from the popup</div>
              <div className="mcp-loopback-step-desc">
                After you approve, the popup will show <strong>"This site can't be reached"</strong>{' '}
                — that's expected. <strong>Copy the full URL from the popup's address bar</strong>{' '}
                (it starts with <code>{LOOPBACK_PASTE_PREFIX}</code>).
              </div>
            </li>
            <li>
              <div className="mcp-loopback-step-title">Paste it below</div>
              <div className="mcp-loopback-step-desc">
                {isReadyPhase
                  ? 'The text box below activates after you open sign-in.'
                  : 'Paste it here to finish connecting.'}
              </div>
              <div className="mcp-loopback-paste-row">
                <textarea
                  ref={textareaRef}
                  className="textarea mcp-loopback-paste-input"
                  rows={3}
                  placeholder={`${LOOPBACK_PASTE_PREFIX}54321/callback?code=...&state=...`}
                  value={pastedUrl}
                  disabled={submitting || isReadyPhase}
                  onChange={(event) => {
                    setPastedUrl(event.target.value);
                    setClipboardError('');
                  }} />
                <button
                  type="button"
                  className="btn btn-secondary mcp-loopback-paste-clipboard"
                  disabled={submitting || isReadyPhase}
                  onClick={pasteFromClipboard}>
                  Paste from clipboard
                </button>
              </div>
              {clipboardError && (
                <div className="mcp-form-field-warning" role="alert">{clipboardError}</div>
              )}
              {errorMessage && (
                <div className="mcp-form-field-warning" role="alert">{errorMessage}</div>
              )}
              {!isReadyPhase && popupClosedHint && !pasteLooksValid && (
                <div className="mcp-loopback-popup-hint">
                  Looks like you closed the popup. If you didn't copy the URL, click{' '}
                  <strong>Reopen sign-in</strong> below to try again.
                </div>
              )}
            </li>
          </ol>
        </div>
        <div className="modal-actions mcp-loopback-actions">
          <button
            type="button"
            className="btn"
            disabled={submitting}
            onClick={() => finalize({ outcome: 'closed' })}>
            Cancel
          </button>
          {isReadyPhase ? (
            <button
              type="button"
              className="btn btn-primary"
              disabled={submitting}
              onClick={openSignInPopup}>
              Open sign-in
            </button>
          ) : (
            <>
              <button
                type="button"
                className="btn btn-secondary"
                disabled={submitting}
                onClick={openSignInPopup}>
                Reopen sign-in
              </button>
              <button
                type="submit"
                className="btn btn-primary"
                disabled={submitting || !pastedUrl.trim()}>
                {submitting ? 'Finishing\u2026' : 'Finish connecting'}
              </button>
            </>
          )}
        </div>
      </form>
    </div>
  );
  if (typeof document !== 'undefined' && ReactDOM?.createPortal) {
    return ReactDOM.createPortal(modal, document.body);
  }
  return modal;
}

function McpServersPage({ navigate, queryString }) {
  return <TargetSourcesPage navigate={navigate} queryString={queryString} targetKind="mcp" />;
}

function CliTargetsPage({ navigate }) {
  return <TargetSourcesPage navigate={navigate} targetKind="cli" />;
}

// Combined "My MCPs / CLIs" destination from the sidebar. Renders both target
// kinds as stacked sections under a single page title. Each section keeps its
// own header actions, empty state, and modal flows via `embedded` mode.
function SourcesPage({ navigate, queryString = '' }) {
  return (
    <div className="page-inner sources-page">
      <div className="ui-page-head">
        <h1 className="ui-page-title">My MCPs / CLIs</h1>
      </div>
      <TargetSourcesPage navigate={navigate} queryString={queryString} targetKind="mcp" embedded />
      <TargetSourcesPage navigate={navigate} targetKind="cli" embedded />
    </div>
  );
}

function getTargetKind(row) {
  return row?.target_kind || row?.targetKind || 'mcp';
}

function isHostedMcpServerRow(row) {
  return getTargetKind(row) === 'mcp' && (row?.transport_type || row?.transportType) === 'stdio_hosted';
}

function TargetSourcesPage({ navigate, targetKind, queryString = '', embedded = false }) {
  const auth = useAuth();
  const toast = useToast();
  const servers = useApiResource('/api/mcp-servers');
  // Benchmark sources are managed exclusively from the /benchmark index
  // page now — this surface is production-only. /api/mcp-servers already
  // excludes is_benchmark rows server-side (listMcpServers `scope =
  // 'production'` default), so no client-side filtering is needed.
  const allMonitors = useApiResource('/api/tool-monitors');
  const serverRows = servers.data?.rows || [];
  const targetRows = useMemoMcp(
    () => serverRows.filter((row) => getTargetKind(row) === targetKind),
    [serverRows, targetKind],
  );
  const routeSearchParams = useMemoMcp(() => new URLSearchParams(queryString || ''), [queryString]);
  const routeServerId = routeSearchParams.get('server');
  const monitorsByServer = useMemoMcp(() => {
    const out = {};
    for (const m of allMonitors.data?.rows || []) {
      if (!m.mcp_server_id) continue;
      if (!out[m.mcp_server_id]) out[m.mcp_server_id] = [];
      out[m.mcp_server_id].push(m);
    }
    return out;
  }, [allMonitors.data]);
  const isInitialLoading = servers.loading && !servers.data;
  const mcpServerCreateLimitState = getPlanLimitState(auth, 'mcpServers', 1);
  const isCliPage = targetKind === 'cli';
  const pageTitle = isCliPage ? 'CLIs' : 'MCP Servers';
  const sectionTitle = isCliPage ? 'CLI targets' : 'MCP servers';
  const createLabel = isCliPage ? 'New CLI' : 'New MCP server';
  const canManageSources = canUseTenantAction(auth, 'editor');
  const sourceActionDisabledReason = tenantActionDisabledReason(auth, 'editor');
  const createDisabled = isInitialLoading || !canManageSources || !mcpServerCreateLimitState.allowed;
  const createDisabledReason = !canManageSources
    ? sourceActionDisabledReason
    : !mcpServerCreateLimitState.allowed
    ? mcpServerCreateLimitState.message
    : undefined;
  const [message, setMessage] = useStateMcp('');
  const [connectModalOpen, setConnectModalOpen] = useStateMcp(false);
  // The modal's shared "Advanced" disclosure (extra headers when not using
  // OAuth, or the curated provider/scopes override when OAuth is the
  // selected auth method) lives inside the shared form state hook.
  const formState = useMcpConnectFormState({ targetKind });
  const [serverSaving, setServerSaving] = useStateMcp(false);
  // Tracks an in-flight OAuth handshake initiated from the Connect modal.
  // While this is set, the modal stays open and renders an "awaiting
  // sign-in" panel in place of the form, with a Cancel button that closes
  // the popup. We deliberately keep the modal open during the handshake
  // (rather than dismissing it the moment the popup opens) so the user
  // always has a clear "what's happening" surface and a way to back out.
  const [oauthInFlight, setOauthInFlight] = useStateMcp(/** @type {{popup: Window | null, providerLabel: string, redirectUriMode: string} | null} */ (null));
  const [pendingCliTargetId, setPendingCliTargetId] = useStateMcp(null);
  const [pendingCliPollCount, setPendingCliPollCount] = useStateMcp(0);
  const [cliSetupState, setCliSetupState] = useStateMcp(null);
  const [probeServerId, setProbeServerId] = useStateMcp(null);
  const [probeResult, setProbeResult] = useStateMcp(null); // { ok, tools, errorCode }
  const [probeLoading, setProbeLoading] = useStateMcp(false);
  const [selectedTools, setSelectedTools] = useStateMcp({}); // { [toolName]: { checked, interval, values, extras, errors, saveError } }
  const [agentState, setAgentState] = useStateMcp(/** @type {{status: string, error?: string}} */ ({ status: 'idle' }));
  const [savingMonitors, setSavingMonitors] = useStateMcp(false);
  const [wizardSaved, setWizardSaved] = useStateMcp(/** @type {{count: number, serverName: string} | null} */ (null));
  const [detailServerId, setDetailServerId] = useStateMcp(null);
  const [detailInitialTab, setDetailInitialTab] = useStateMcp('tools');
  const handledRouteServerRef = useRefMcp('');
  const [pendingDeleteServer, setPendingDeleteServer] = useStateMcp(/** @type {{id: string, name: string} | null} */ (null));
  const [deletingServer, setDeletingServer] = useStateMcp(false);
  const probeServer = serverRows.find((row) => row.id === probeServerId) || null;

  async function confirmDeleteServer() {
    if (!pendingDeleteServer) return;
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    const target = pendingDeleteServer;
    setDeletingServer(true);
    try {
      await apiFetch(`/api/mcp-servers/${target.id}`, { method: 'DELETE' });
      toast.show({ tone: 'ok', title: `Deleted ${target.name}` });
      if (detailServerId === target.id) setDetailServerId(null);
      setPendingDeleteServer(null);
      await servers.reload();
    } catch (err) {
      toast.show({ tone: 'bad', title: 'Delete failed', description: err.message });
    } finally {
      setDeletingServer(false);
    }
  }

  function resetConnectForm() {
    formState.reset();
  }

  function openConnectModal() {
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    if (!mcpServerCreateLimitState.allowed) {
      toast.show({ tone: 'bad', title: 'Plan limit reached', description: mcpServerCreateLimitState.message });
      return;
    }
    // Make sure the form starts from a clean slate every time the modal opens
    // so the OAuth-by-default choice survives auth/feature flag rehydration.
    resetConnectForm();
    setMessage('');
    setCliSetupState(null);
    setConnectModalOpen(true);
  }

  function closeConnectModal() {
    if (serverSaving) return;
    resetConnectForm();
    setConnectModalOpen(false);
    setMessage('');
    setCliSetupState(null);
  }

  // Closes the OAuth popup the user opened from the connect modal. Closing
  // the popup makes `openOAuthPopup`'s poller observe `popup.closed === true`
  // and settle the awaited promise with `{ outcome: 'closed' }`, which
  // unwinds the awaiting state in `addServer` and clears `oauthInFlight`.
  function cancelOauthInFlight() {
    const popup = oauthInFlight?.popup;
    if (popup) {
      try { popup.close(); } catch (_error) {}
    }
    // For the loopback flow the popup is opened/owned by
    // LoopbackOAuthModalRoot, not by us. The Cancel button in that modal
    // is responsible for resolving its promise; this no-op fallback just
    // means the user has to use the loopback modal's Cancel.
  }

  useMcpProvisioningPoll({
    cliSetupState, setCliSetupState,
    pendingCliTargetId, setPendingCliTargetId,
    pendingCliPollCount, setPendingCliPollCount,
    reload: servers.reload,
  });

  function dismissWizard() {
    setProbeServerId(null);
    setProbeResult(null);
    setSelectedTools({});
    setAgentState({ status: 'idle' });
    setWizardSaved(null);
  }

  useEscapeToClose({
    enabled: Boolean(probeServerId),
    disabled: savingMonitors,
    onClose: dismissWizard,
  });

  async function runProbe(serverId) {
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    setProbeServerId(serverId);
    setProbeLoading(true);
    setProbeResult(null);
    setSelectedTools({});
    setAgentState({ status: 'idle' });
    setWizardSaved(null);
    try {
      const probe = await apiFetch(`/api/mcp-servers/${serverId}/probe`, { method: 'POST' });
      setProbeResult(probe);
      try {
        await servers.reload();
      } catch (_error) {}
      if (probe?.ok && Array.isArray(probe.tools)) {
        setSelectedTools(buildInitialToolState(probe.tools));
        runAgentFillFor(serverId, probe.tools);
      }
    } catch (probeError) {
      setProbeResult({ ok: false, tools: [], errorCode: probeError.code || 'request_failed', errorMessage: probeError.message });
    } finally {
      setProbeLoading(false);
    }
  }

  async function runAgentFillFor(serverId, tools) {
    if (!tools || tools.length === 0) return;
    setAgentState({ status: 'loading' });
    try {
      const data = await apiFetch(`/api/mcp-servers/${serverId}/agent-fill`, {
        method: 'POST',
        body: JSON.stringify({ tools }),
      });
      setSelectedTools((cur) => mergeAgentFills(cur, tools, data?.fills || {}));
      setAgentState({ status: 'ready' });
    } catch (err) {
      setAgentState({ status: 'error', error: err.message });
    }
  }

  async function addServer() {
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    setMessage('');
    setServerSaving(true);
    try {
      const validationError = validateMcpConnectFormInputs(formState);
      if (validationError) throw new Error(validationError);
      const limitState = getPlanLimitState(auth, 'mcpServers', 1);
      if (!limitState.allowed) {
        toast.show({ tone: 'bad', title: 'Plan limit reached', description: limitState.message });
        return;
      }
      const built = buildMcpConnectPayload(formState);
      const created = await apiFetch('/api/mcp-servers', {
        method: 'POST',
        body: JSON.stringify(built.payload),
      });
      const newServerId = created?.mcpServer?.id;
      if (newServerId && (built.kind === 'cli' || built.kind === 'hosted')) {
        setPendingCliTargetId(newServerId);
        setPendingCliPollCount(0);
        setCliSetupState({
          serverId: newServerId,
          serverName: formState.serverName.trim(),
          setupKind: built.kind,
          provisioningRunId: created?.provisioningRun?.id || null,
          status: created?.mcpServer?.provisioning_status || 'pending',
          failedAtStatus: null,
          errorCode: null,
          updatedAt: new Date().toISOString(),
        });
        toast.show({
          tone: 'ok',
          title: built.kind === 'cli' ? 'CLI setup started' : 'MCP setup started',
          description: 'Provisioning has started. This dialog will show progress.',
        });
        await servers.reload();
        return;
      }
      if (newServerId && built.kind === 'remote' && built.oauth) {
        const oauthStart = await startOAuthConnect({
          mcpServerId: newServerId,
          providerSlug: built.oauth.providerSlug,
          requestedScopes: built.oauth.requestedScopes,
          authConfig: {},
        });
        await servers.reload();
        // Keep the modal open and switch its body to an "awaiting sign-in"
        // panel until the OAuth handshake settles. For the loopback flow
        // the popup is owned by LoopbackOAuthModalRoot (it shows on top of
        // the connect modal), so we just leave the connect modal in
        // waiting state behind it.
        const inFlight = {
          popup: null,
          providerLabel: built.oauth.providerSlug || 'the provider',
          redirectUriMode: oauthStart?.redirectUriMode === 'loopback' ? 'loopback' : 'web',
        };
        setOauthInFlight(inFlight);
        // The inner finally is what guarantees the awaiting panel is taken
        // down on every exit path, including unexpected throws from
        // runMcpOAuthFlow / its inner awaits. Without it, the outer catch
        // would surface the toast but leave the modal stuck on the
        // "Awaiting OAuth sign-in" panel with no escape other than the
        // header X button (Greptile #281 P2).
        let popupResult;
        try {
          popupResult = await runMcpOAuthFlow(oauthStart, {
            onPopupOpened: (popup) => setOauthInFlight((current) => current ? { ...current, popup } : current),
          });
        } finally {
          setOauthInFlight(null);
        }
        if (popupResult.outcome === 'navigated') return;
        await servers.reload();
        if (popupResult.outcome === 'connected') {
          resetConnectForm();
          setConnectModalOpen(false);
          await runProbe(newServerId);
        } else if (popupResult.outcome === 'failed') {
          toast.show({
            tone: 'bad',
            title: 'OAuth failed',
            description: popupResult.message || 'OAuth authorization did not complete.',
          });
          // Leave the modal open so the user can read the error and retry
          // without re-entering all of the connect form fields.
        } else if (popupResult.outcome === 'pending') {
          resetConnectForm();
          setConnectModalOpen(false);
          toast.show({
            tone: 'warn',
            title: 'OAuth completed; provisioning is still in progress',
            description: 'Tools will appear once provisioning finishes.',
          });
        } else if (popupResult.outcome === 'closed') {
          // User canceled (closed popup or hit Cancel in the loopback modal).
          // Keep the connect modal open so they can retry without losing
          // form state; surface a non-blocking message above the form.
          setMessage('Sign-in was canceled. Submit again to retry.');
        }
        return;
      }
      resetConnectForm();
      setConnectModalOpen(false);
      toast.show({
        tone: 'ok',
        title: targetKind === 'cli' ? 'CLI target created' : 'Server connected',
        description: targetKind === 'cli' || built.kind === 'hosted'
          ? (formState.authType === 'oauth_authorization_code' ? 'Provisioning has started. Connect OAuth when auth is required.' : 'Provisioning has started.')
          : formState.authType === 'none' ? 'No auth profile attached.' : 'Auth secret stored securely.',
      });
      await servers.reload();
      if (newServerId && built.kind === 'remote') {
        // Probe remote sources right after create so the tool-monitor
        // wizard has a populated tool list to drive its checklist.
        await runProbe(newServerId);
      }
    } catch (error) {
      toast.show({ tone: 'bad', title: 'Connect failed', description: error.message });
    } finally {
      setServerSaving(false);
    }
  }

  useEffectMcp(() => {
    if (targetKind !== 'mcp' || !routeServerId) return;
    if (isInitialLoading) return;
    const routeKey = `${routeServerId}:${queryString}`;
    if (handledRouteServerRef.current === routeKey) return;
    handledRouteServerRef.current = routeKey;
    const routeServer = targetRows.find((row) => row.id === routeServerId);
    if (!routeServer) {
      toast.show({
        tone: 'warn',
        title: 'Could not reopen MCP server',
        description: 'OAuth completed, but the target server is no longer available.',
      });
      navigate('/sources/mcp-servers', { replace: true });
      return;
    }
    setDetailInitialTab('tools');
    setDetailServerId(routeServerId);
    navigate('/sources/mcp-servers', { replace: true });
  }, [targetKind, routeServerId, queryString, isInitialLoading, targetRows, navigate, toast]);

  const canConnectServer = !isInitialLoading && !serverSaving && formState.canConnect;

  const createAction = (
    <Button
      variant="primary"
      size="sm"
      disabled={createDisabled}
      onClick={openConnectModal}
      title={createDisabledReason}>
      <Icon name="plus" size={13} />{createLabel}
    </Button>
  );

  // Stand-alone mode keeps the page-level h1 + action block. Embedded mode
  // (inside /sources) drops the redundant page header — the section head
  // below already carries the title + count, and the create button moves
  // into that same row so each section reads as one self-contained block.
  const head = embedded ? null : (
    <div className="ui-page-head">
      <h1 className="ui-page-title">{pageTitle}</h1>
      <div className="ui-page-actions">
        <PlanLimitMeter label="Target limit" state={getPlanLimitState(auth, 'mcpServers', 0)} />
        {createAction}
      </div>
    </div>
  );

  const body = (
    <>
      {/* `LoopbackOAuthModalRoot` lives at the app root in app.jsx so a
          single instance owns paste-back flows for both this page and
          the onboarding connect form. */}
      {head}

      {message && <div className="auth-message alerts-message">{message}</div>}

      <div className="col" style={{ gap: 12 }}>
        <section>
          <div className="ui-section-head">
            <div className="ui-section-title">
              {/* In embedded /sources view, the section IS the page-level
                  block, so use the prominent pageTitle wording ("MCP
                  Servers" / "CLIs"). Standalone pages keep the lower-case
                  sectionTitle ("MCP servers" / "CLI targets"). */}
              {embedded ? pageTitle : sectionTitle}
              {!isInitialLoading && targetRows.length > 0 && (
                <span className="ui-section-count">{targetRows.length}</span>
              )}
            </div>
            {embedded && (
              <div className="ui-section-actions">
                <PlanLimitMeter label="Target limit" state={getPlanLimitState(auth, 'mcpServers', 0)} />
                {createAction}
              </div>
            )}
          </div>
          {isInitialLoading ? (
            <div className="mcp-server-grid">
              {Array.from({ length: 3 }).map((_, idx) => (
                <div key={idx} className="mcp-card mcp-card-loading">
                  <span className="skel skel-mid"></span>
                  <span className="skel skel-long" style={{ marginTop: 10 }}></span>
                  <span className="skel skel-short" style={{ marginTop: 10 }}></span>
                </div>
              ))}
            </div>
          ) : targetRows.length === 0 ? (
            <div className="ui-surface alerts-empty">
              <EmptyState
                icon={isCliPage ? 'terminal' : 'mcp'}
                title={isCliPage ? 'No CLIs added' : 'No MCP servers connected'}
                body={servers.error
                  ? servers.error.message
                  : isCliPage
                    ? 'Add an npm CLI target so workflows can exercise command-line tools.'
                    : 'Connect at least one MCP server so workflows have tools to call.'}
                action={(
                  <Button
                    variant="primary"
                    size="sm"
                    disabled={createDisabled}
                    onClick={openConnectModal}
                    title={createDisabledReason}>
                    <Icon name="plus" size={13} />{createLabel}
                  </Button>
                )} />
            </div>
          ) : (
            <div className="mcp-server-grid">
              {targetRows.map(server => (
                <McpServerCard
                  key={server.id}
                  server={server}
                  monitors={monitorsByServer[server.id] || []}
                  onOpen={() => { setDetailInitialTab('tools'); setDetailServerId(server.id); }}
                  onPeekMonitors={() => { setDetailInitialTab('monitors'); setDetailServerId(server.id); }}
                  onDelete={canManageSources ? () => setPendingDeleteServer({ id: server.id, name: server.name }) : null} />
              ))}
            </div>
          )}
        </section>
      </div>
    </>
  );

  const modals = (
    <>
      {probeServerId && (
        <div
          className="dialog-backdrop"
          onMouseDown={(e) => {
            if (e.target !== e.currentTarget) return;
            if (savingMonitors) return;
            if (wizardSaved) { dismissWizard(); return; }
            dismissWizard();
          }}>
          <div
            className="modal-panel tool-wizard-modal"
            role="dialog"
            aria-modal="true"
            aria-labelledby="tool-wizard-modal-title">
            <div className="modal-header">
              <div>
                <div className="modal-title" id="tool-wizard-modal-title">
                  {wizardSaved ? `Monitors created for ${wizardSaved.serverName}` : 'Set up tool monitors'}
                </div>
                {wizardSaved && (
                  <div className="modal-subtitle">Your monitors are saved and scheduled.</div>
                )}
              </div>
              <button
                className="icon-btn"
                type="button"
                aria-label="Close"
                disabled={savingMonitors}
                onClick={dismissWizard}>
                <Icon name="x" size={15} />
              </button>
            </div>
            <div className="modal-body tool-wizard-modal-body">
              {wizardSaved ? (
                <div className="tool-wizard-done">
                  <div className="tool-wizard-done-mark">
                    <Icon name="check" size={28} />
                  </div>
                  <div className="tool-wizard-done-title">
                    {wizardSaved.count} tool monitor{wizardSaved.count === 1 ? '' : 's'} saved
                  </div>
                  <div className="tool-wizard-done-sub">
                    Each one will run on its schedule and alert you when it fails.
                  </div>
                  <div className="tool-wizard-done-actions">
                    <Button
                      variant="primary"
                      onClick={() => { dismissWizard(); navigate('/tool-monitors'); }}>
                      <Icon name="zap" size={13} />Open tool monitors
                    </Button>
                    <Button onClick={dismissWizard}>
                      Back to MCP servers
                    </Button>
                  </div>
                </div>
              ) : (
                <>
                  {probeLoading && <ToolWizardLoading phase="probe" />}
                  {!probeLoading && probeResult && !probeResult.ok && (
                    <div className="tool-wizard-error">
                      <div className="row" style={{ gap: 8, alignItems: 'flex-start' }}>
                        <Icon name="alert" size={16} />
                        <div className="col" style={{ gap: 4, flex: 1 }}>
                          <div style={{ fontWeight: 600 }}>Could not list tools from this MCP server</div>
                          <div className="text-xs">{targetUserErrorMessage(probeResult.errorCode, targetKind, 'runtime', probeResult.errorMessage)}</div>
                        </div>
                        <button className="btn btn-sm" onClick={() => runProbe(probeServerId)}>
                          <Icon name="refresh" size={12} />Retry
                        </button>
                        <button className="btn btn-sm btn-ghost" onClick={dismissWizard}>
                          Dismiss
                        </button>
                      </div>
                    </div>
                  )}
                  {!probeLoading && probeResult?.ok && (
                    <ToolMonitorWizard
                      tools={probeResult.tools}
                      selectedTools={selectedTools}
                      setSelectedTools={setSelectedTools}
                      saving={savingMonitors}
                      onSave={async () => {
                        if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
                        const validation = validateAllSelectedTools(probeResult.tools, selectedTools);
                        if (validation.hasErrors) {
                          setSelectedTools(validation.next);
                          return;
                        }
                        const picked = probeResult.tools.filter((t) => {
                          const c = validation.next[t.name];
                          return c?.checked && !c?.existing;
                        });
                        const limitState = getPlanLimitState(auth, 'toolMonitors', picked.length);
                        if (!limitState.allowed) {
                          toast.show({ tone: 'bad', title: 'Plan limit reached', description: limitState.message });
                          return;
                        }
                        setSavingMonitors(true);
                        let savedCount = 0;
                        let failedAny = false;
                        const nextState = { ...validation.next };
                        try {
                          for (const tool of picked) {
                            const cfg = nextState[tool.name];
                            const argsObj = buildArgsFromState(tool, cfg);
                            try {
                              await apiFetch(`/api/mcp-servers/${probeServerId}/tool-monitors`, {
                                method: 'POST',
                                body: JSON.stringify({
                                  toolName: tool.name,
                                  intervalSeconds: cfg.interval,
                                  arguments: argsObj,
                                  inputSchema: tool.inputSchema,
                                }),
                              });
                              nextState[tool.name] = { ...cfg, saved: true, saveError: null };
                              savedCount += 1;
                            } catch (perToolError) {
                              failedAny = true;
                              nextState[tool.name] = { ...cfg, saveError: perToolError.message };
                            }
                          }
                          setSelectedTools(nextState);
                          servers.reload();
                          if (!failedAny) {
                            const probedServer = serverRows.find((s) => s.id === probeServerId);
                            setWizardSaved({
                              count: savedCount,
                              serverName: probedServer?.name || 'this server',
                            });
                          } else {
                            setMessage(`Saved ${savedCount} of ${picked.length} monitors. Resolve errors below and retry.`);
                          }
                        } finally {
                          setSavingMonitors(false);
                        }
                      }}
                      onCancel={dismissWizard}
                      agentState={agentState}
                      onAgentRetry={() => runAgentFillFor(probeServerId, probeResult.tools)}
                    />
                  )}
                </>
              )}
            </div>
          </div>
        </div>
      )}

      <McpServerDetail
        serverId={detailServerId}
        navigate={navigate}
        initialTab={detailInitialTab}
        onRefreshed={() => servers.reload()}
        onClose={() => setDetailServerId(null)}
        onDeleted={(name) => {
          setDetailServerId(null);
          toast.show({ tone: 'ok', title: `Deleted ${name}` });
          servers.reload();
        }}
        onEdited={() => {
          toast.show({ tone: 'ok', title: isCliPage ? 'CLI updated' : 'Server updated' });
          servers.reload();
        }}
        onCreateMonitor={() => navigate(`/tool-monitors?new=1&server=${encodeURIComponent(detailServerId)}`)}
        onCreateWorkflow={() => navigate(`/workflows?new=1&server=${encodeURIComponent(detailServerId)}`)}
        onViewMonitors={() => navigate('/tool-monitors')} />

      <ConfirmDialog
        open={Boolean(pendingDeleteServer)}
        tone="danger"
        title={pendingDeleteServer ? `Delete ${isCliPage ? 'CLI' : 'MCP server'} "${pendingDeleteServer.name}"?` : ''}
        description={`This removes the ${isCliPage ? 'CLI target' : 'server'} and any monitors attached to it. This action can't be undone.`}
        confirmLabel={isCliPage ? 'Delete CLI' : 'Delete server'}
        confirmBusyLabel="Deleting"
        busy={deletingServer}
        onCancel={() => { if (!deletingServer) setPendingDeleteServer(null); }}
        onConfirm={confirmDeleteServer} />

      <McpConnectModal
        open={connectModalOpen}
        message={message}
        formState={formState}
        saving={serverSaving}
        oauthInFlight={oauthInFlight}
        onCancelOauth={cancelOauthInFlight}
        onSubmit={addServer}
        onCancel={closeConnectModal}
        cliSetupState={cliSetupState}
        onOpenSetupTarget={() => {
          if (!cliSetupState?.serverId) return;
          const targetTab = cliSetupState.status === 'failed' ? 'history' : 'tools';
          resetConnectForm();
          setConnectModalOpen(false);
          setCliSetupState(null);
          setDetailInitialTab(targetTab);
          setDetailServerId(cliSetupState.serverId);
        }}
      />
    </>
  );

  if (embedded) {
    return (
      <>
        <section className="sources-embedded-section mcp-servers-page">{body}</section>
        {modals}
      </>
    );
  }
  return (
    <>
      <div className="page-inner mcp-servers-page">{body}</div>
      {modals}
    </>
  );
}

// Curated-only OAuth picker. Used for hosted-MCP targets where there is no
// remote URL to probe, so discovery is not applicable and the operator must
// pick a curated provider. Remote-MCP OAuth uses auto-discovery from the
// server URL and exposes a curated override only inside the Advanced
// disclosure (see `OauthOverrideFields`).
function OauthCuratedConfig({ providerSlug, setProviderSlug, scopesText, setScopesText, saving }) {
  const [providers, setProviders] = useStateMcp([]);
  const [loading, setLoading] = useStateMcp(true);
  const [error, setError] = useStateMcp('');

  useEffectMcp(() => {
    let cancelled = false;
    setLoading(true);
    setError('');
    fetchOAuthProviders()
      .then((rows) => {
        if (cancelled) return;
        const nextProviders = Array.isArray(rows) ? rows : [];
        setProviders(nextProviders);
        if (!providerSlug && nextProviders.length > 0) {
          setProviderSlug(nextProviders[0].slug);
          setScopesText((nextProviders[0].default_scopes || []).join(' '));
        }
      })
      .catch((err) => {
        if (!cancelled) setError(err.message || 'Could not load OAuth providers');
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, []);

  function selectProvider(slug) {
    setProviderSlug(slug);
    // Reverting to the empty placeholder option finds no entry in `providers`,
    // so explicitly clear the scopes field instead of leaving stale text from
    // the previously selected provider.
    const provider = providers.find((row) => row.slug === slug);
    setScopesText(provider ? (provider.default_scopes || []).join(' ') : '');
  }

  return (
    <div className="mcp-oauth-config mcp-modal-wide">
      <label className="field">
        <span className="field-label">OAuth provider</span>
        <Select
          value={providerSlug}
          disabled={saving || loading || providers.length === 0}
          onChange={selectProvider}
          options={[
            { value: '', label: loading ? 'Loading providers...' : 'Select provider' },
            ...providers.map((provider) => ({ value: provider.slug, label: provider.display_name })),
          ]} />
      </label>
      <label className="field">
        <span className="field-label">Scopes</span>
        <input
          className="input mono"
          value={scopesText}
          disabled={saving || loading}
          onChange={(event) => setScopesText(event.target.value)} />
      </label>
      {error && <div className="auth-message error">{error}</div>}
    </div>
  );
}

// Curated-provider override fields. Rendered inside the modal's shared
// "Advanced" disclosure when OAuth is the selected auth method for a
// remote MCP server. Lazily loads the provider list when first shown.
function OauthOverrideFields({
  providerSlug,
  setProviderSlug,
  scopesText,
  setScopesText,
  saving,
  enabled,
}) {
  const [providers, setProviders] = useStateMcp([]);
  const [loading, setLoading] = useStateMcp(false);
  const [error, setError] = useStateMcp('');

  useEffectMcp(() => {
    if (!enabled) return undefined;
    if (providers.length > 0) return undefined;
    let cancelled = false;
    setLoading(true);
    setError('');
    fetchOAuthProviders()
      .then((rows) => {
        if (cancelled) return;
        setProviders(Array.isArray(rows) ? rows : []);
      })
      .catch((err) => {
        if (!cancelled) setError(err.message || 'Could not load OAuth providers');
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, [enabled]);

  function selectProvider(slug) {
    setProviderSlug(slug);
    // Reverting to the empty placeholder option finds no entry in `providers`,
    // so explicitly clear the scopes field instead of leaving stale text from
    // the previously selected provider.
    const provider = providers.find((row) => row.slug === slug);
    setScopesText(provider ? (provider.default_scopes || []).join(' ') : '');
  }

  return (
    <>
      <div className="mcp-form-row mcp-form-row-1-1">
        <label className="field">
          <span className="field-label">OAuth provider override</span>
          <Select
            value={providerSlug}
            disabled={saving || loading || providers.length === 0}
            onChange={selectProvider}
            options={[
              { value: '', label: loading ? 'Loading providers...' : 'Use auto-discovery' },
              ...providers.map((provider) => ({ value: provider.slug, label: provider.display_name })),
            ]} />
          <span className="field-help">
            Pick a curated provider only when your MCP server doesn't expose OAuth
            metadata (RFC 9728) or a registration endpoint. Leave on "Use auto-discovery"
            for any standards-compliant MCP server.
          </span>
        </label>
        <label className="field">
          <span className="field-label">Scopes override</span>
          <input
            className="input mono"
            placeholder="space-separated scopes"
            value={scopesText}
            disabled={saving || loading}
            onChange={(event) => setScopesText(event.target.value)} />
          <span className="field-help">
            Required when overriding the provider. Selecting a provider above pre-fills
            its default scopes; tweak them only if you know what you need.
          </span>
          {/* Picking a curated provider whose `default_scopes` is missing or
              empty leaves this field blank and silently disables the Connect
              button. Surface the requirement inline so the user knows why. */}
          {providerSlug && !scopesText.trim() && (
            <span className="field-help mcp-form-field-warning">
              This provider didn't ship default scopes — type the scopes you need
              to request before connecting.
            </span>
          )}
        </label>
      </div>
      {error && <div className="auth-message error">{error}</div>}
    </>
  );
}

// Renders inside the connect modal (replacing the form) while we're
// waiting for the user to finish sign-in in the OAuth popup. The popup is
// owned by `openOAuthPopup` (or by `LoopbackOAuthModalRoot` for the
// loopback flow) and stays open in a separate browser window; this panel
// just gives the user a visible "we know what's happening" surface. The
// Cancel control lives in the modal footer (next to the spinner), so this
// component is purely informational. We deliberately keep the modal open
// during this wait — closing it the moment the popup opens (the old
// behavior) left the user staring at a blank page with no way back.
function McpOauthAwaitingPanel({ inFlight }) {
  const isLoopback = inFlight?.redirectUriMode === 'loopback';
  return (
    <div className="mcp-oauth-awaiting">
      <div className="mcp-oauth-awaiting-spinner" aria-hidden="true">
        <LoadingSpinner size="lg" decorative />
      </div>
      <h3 className="mcp-oauth-awaiting-title">
        {isLoopback ? 'Finish sign-in in the smaller dialog' : 'Waiting for sign-in\u2026'}
      </h3>
      <p className="mcp-oauth-awaiting-desc">
        {isLoopback
          ? "Follow the steps in the dialog on top of this one. It will walk you through opening the provider, approving access, and pasting the loopback URL back."
          : "We opened a popup with the provider's sign-in page. Approve the permissions there and this dialog will continue automatically."}
      </p>
      <p className="mcp-oauth-awaiting-hint">
        Don't close this dialog — your form is preserved here so you can retry without re-entering anything.
      </p>
    </div>
  );
}

// Drives the CLI/hosted setup modal's progress timeline by polling the
// provisioning-run endpoint on a 2s cadence. Each tick re-fetches the
// dedicated run row and reloads the caller's source list so the
// dashboard catches up. Soft-fails are tolerated — the next tick simply
// retries. Used by both the production sources page and the benchmark
// sources area, so the timeline behavior stays in lock-step.
//
// Callers own the relevant useState slots (cliSetupState, pendingCli*)
// so they can also drive the modal's body off the same state. The
// `reload` callback receives no args and is expected to return the
// list response (`{ rows: [...] }`) — it's invoked through a stable
// ref so re-renders inside `reload()` don't cancel the in-flight tick.
function useMcpProvisioningPoll({
  cliSetupState, setCliSetupState,
  pendingCliTargetId, setPendingCliTargetId,
  pendingCliPollCount, setPendingCliPollCount,
  reload,
}) {
  const reloadRef = useRefMcp(reload);
  useEffectMcp(() => {
    reloadRef.current = reload;
  }, [reload]);

  useEffectMcp(() => {
    if (!pendingCliTargetId) return undefined;
    const runId = cliSetupState?.provisioningRunId;
    const currentStatus = cliSetupState?.status;
    if (currentStatus && !CLI_PENDING_STATUSES.has(currentStatus)) {
      setPendingCliTargetId(null);
      setPendingCliPollCount(0);
      return undefined;
    }
    if (pendingCliPollCount >= 90) {
      setPendingCliTargetId(null);
      setPendingCliPollCount(0);
      return undefined;
    }
    let cancelled = false;
    const timer = setTimeout(async () => {
      if (cancelled) return;
      try {
        let nextStatus = null;
        let errorCode = null;
        let updatedAt = null;
        if (runId) {
          const data = await apiFetch(`/api/mcp-servers/${pendingCliTargetId}/provisioning-runs/${runId}`);
          const run = data?.provisioningRun || null;
          if (run) {
            errorCode = run.error_code || null;
            updatedAt = run.completed_at || run.started_at || null;
            const phase = run.result?.phase || run.result?.status || null;
            nextStatus = mapProvisioningRunToStatus(run.status, phase);
          }
        }
        const list = await reloadRef.current();
        const refreshedServer = (list?.rows || []).find((row) => row.id === pendingCliTargetId);
        const rowStatus = refreshedServer?.provisioning_status
          || refreshedServer?.provisioningStatus
          || refreshedServer?.last_connection_status
          || refreshedServer?.lastConnectionStatus
          || null;
        // Prefer the run row's terminal status over a still-pending row
        // status: the list sometimes lags one tick behind the run row, and
        // a stale `discovering` from the list shouldn't downgrade a freshly
        // resolved `ready`/`failed` from the per-run endpoint.
        const nextIsTerminal = nextStatus && !CLI_PENDING_STATUSES.has(nextStatus);
        const rowIsPending = !rowStatus || CLI_PENDING_STATUSES.has(rowStatus);
        if (rowStatus && !(nextIsTerminal && rowIsPending)) {
          nextStatus = rowStatus;
        }
        if (refreshedServer?.provisioning_error_code) {
          errorCode = refreshedServer.provisioning_error_code;
        } else if (refreshedServer?.latest_connection_test?.error_code) {
          errorCode = refreshedServer.latest_connection_test.error_code;
        }
        if (cancelled) return;
        if (nextStatus) {
          setCliSetupState((current) => {
            if (!current || current.serverId !== pendingCliTargetId) return current;
            // Freeze the phase the run failed in so the timeline can keep
            // the red node anchored to that step instead of snapping back
            // to Queued. The list/run rows overwrite the prior phase when
            // they land on `failed`, so this is the only place we can
            // preserve it.
            const failedAtStatus = nextStatus === 'failed'
              ? (current.failedAtStatus
                || (CLI_PENDING_STATUSES.has(current.status) ? current.status : 'pending'))
              : null;
            return {
              ...current,
              status: nextStatus,
              failedAtStatus,
              errorCode: errorCode || current.errorCode || null,
              updatedAt: updatedAt || current.updatedAt,
            };
          });
        }
        setPendingCliPollCount((count) => count + 1);
      } catch (_error) {
        if (cancelled) return;
        // Transient fetch failures shouldn't kill the modal — count the
        // tick and retry next round.
        setPendingCliPollCount((count) => count + 1);
      }
    }, 2000);
    return () => {
      cancelled = true;
      clearTimeout(timer);
    };
  }, [pendingCliTargetId, pendingCliPollCount, cliSetupState?.provisioningRunId, cliSetupState?.status]);
}

// Shared "connect MCP / connect CLI" form state. The same hook backs the
// production Connect modal (TargetSourcesPage) AND the onboarding "first
// MCP" form, so any change here flows through to both surfaces and the
// behavior stays in lock-step.
//
// Callers pass `targetKind`; the hook exposes every controlled field, the
// derived validity flags (`canConnect`, `hostedMcpEnvVarError`,
// `authRequiresSecret`, `isHostedMcpConnect`, `cliAllowedDomainList`),
// and a `reset()` that returns the form to its OAuth-first defaults.
function useMcpConnectFormState({
  targetKind = 'mcp',
} = {}) {
  // OAuth is the recommended default for remote MCP. CLI defaults to
  // bearer — its auth-mode picker (`cliAuthMode`) is what governs the
  // CLI flow.
  const initialOauthDefault = targetKind === 'mcp';

  const [serverName, setServerName] = useStateMcp('');
  const [serverUrl, setServerUrl] = useStateMcp('');
  const [mcpSourceMode, setMcpSourceMode] = useStateMcp(HOSTED_MCP_SOURCE_REMOTE);
  const [cliPackageName, setCliPackageName] = useStateMcp('');
  const [cliPackageVersion, setCliPackageVersion] = useStateMcp('');
  const [cliBinName, setCliBinName] = useStateMcp('');
  const [cliAuthMode, setCliAuthMode] = useStateMcp('none');
  const [cliAuthEnvVar, setCliAuthEnvVar] = useStateMcp('');
  const [hostedMcpAuthEnvVar, setHostedMcpAuthEnvVar] = useStateMcp('');
  const [cliAllowedDomains, setCliAllowedDomains] = useStateMcp('');
  const [authType, setAuthType] = useStateMcp(initialOauthDefault ? 'oauth_authorization_code' : 'bearer_token');
  const [authHeaderName, setAuthHeaderName] = useStateMcp('x-api-key');
  const [apiKey, setApiKey] = useStateMcp('');
  const [oauthProviderSlug, setOauthProviderSlug] = useStateMcp('');
  const [oauthScopesText, setOauthScopesText] = useStateMcp('');
  const [advancedOpen, setAdvancedOpen] = useStateMcp(false);
  const [extraHeadersJson, setExtraHeadersJson] = useStateMcp('');

  const isHostedMcpConnect = targetKind === 'mcp' && mcpSourceMode === HOSTED_MCP_SOURCE_NPM;
  const authRequiresSecret = authType !== 'none' && authType !== 'oauth_authorization_code';
  const hostedMcpEnvVarError = isHostedMcpConnect && authType !== 'none'
    ? validateHostedEnvVar(hostedMcpAuthEnvVar)
    : '';
  const cliAllowedDomainList = cliAllowedDomains
    .split(',')
    .map((value) => value.trim())
    .filter(Boolean);

  // Migrate between auth-specific sample env-var names when the user toggles
  // auth type, but only if the field still contains one of those sample names.
  // An empty value is intentional user input and should stay empty so the
  // sample text can render as the input placeholder.
  useEffectMcp(() => {
    if (!isHostedMcpConnect) return;
    if (authType === 'oauth_authorization_code') {
      if (HOSTED_AUTH_ENV_DEFAULTS.has(hostedMcpAuthEnvVar)) {
        setHostedMcpAuthEnvVar(hostedOauthEnvVar(oauthProviderSlug));
      }
    } else if (authType !== 'none' && HOSTED_AUTH_ENV_DEFAULTS.has(hostedMcpAuthEnvVar)) {
      setHostedMcpAuthEnvVar('ACME_API_KEY');
    }
  }, [isHostedMcpConnect, authType, oauthProviderSlug, hostedMcpAuthEnvVar]);

  function reset() {
    setServerName('');
    setServerUrl('');
    setMcpSourceMode(HOSTED_MCP_SOURCE_REMOTE);
    setCliPackageName('');
    setCliPackageVersion('');
    setCliBinName('');
    setCliAuthMode('none');
    setCliAuthEnvVar('');
    setHostedMcpAuthEnvVar('');
    setCliAllowedDomains('');
    setAuthType(initialOauthDefault ? 'oauth_authorization_code' : 'bearer_token');
    setAuthHeaderName('x-api-key');
    setApiKey('');
    setOauthProviderSlug('');
    setOauthScopesText('');
    setAdvancedOpen(false);
    setExtraHeadersJson('');
  }

  // Mirrors the validity rule that historically lived inline in
  // TargetSourcesPage: every required field for the chosen path must be
  // non-empty, OAuth scopes-when-overriding must be filled, hosted env
  // var must validate, etc. Surfaced from the hook so both the modal and
  // the onboarding form drive their submit button off the same predicate.
  const canConnect = !serverName.trim()
    ? false
    : (targetKind === 'cli' || isHostedMcpConnect)
      ? Boolean(
        cliPackageName.trim()
          && isExactSemver(cliPackageVersion.trim())
          && cliBinName.trim()
          && cliAllowedDomainList.length > 0
          && (targetKind === 'cli'
            ? (cliAuthMode === 'none'
              || (cliAuthMode === 'api_key_env' && cliAuthEnvVar.trim() && apiKey.trim() && !validateHostedEnvVar(cliAuthEnvVar))
              || (cliAuthMode === 'oauth_authorization_code' && cliAuthEnvVar.trim() && oauthProviderSlug && !validateHostedEnvVar(cliAuthEnvVar)))
            : (authType === 'none'
              || (!hostedMcpEnvVarError
                && hostedMcpAuthEnvVar.trim()
                && (authType === 'oauth_authorization_code' ? oauthProviderSlug : apiKey.trim())))),
      )
      : authType === 'oauth_authorization_code'
        ? Boolean(serverUrl.trim() && (!advancedOpen || !oauthProviderSlug || oauthScopesText.trim()))
        : Boolean((!authRequiresSecret || apiKey.trim()) && (authType !== 'api_key_header' || authHeaderName.trim()));

  return {
    targetKind,
    serverName, setServerName,
    serverUrl, setServerUrl,
    mcpSourceMode, setMcpSourceMode,
    cliPackageName, setCliPackageName,
    cliPackageVersion, setCliPackageVersion,
    cliBinName, setCliBinName,
    cliAuthMode, setCliAuthMode,
    cliAuthEnvVar, setCliAuthEnvVar,
    hostedMcpAuthEnvVar, setHostedMcpAuthEnvVar,
    cliAllowedDomains, setCliAllowedDomains,
    authType, setAuthType,
    authHeaderName, setAuthHeaderName,
    apiKey, setApiKey,
    oauthProviderSlug, setOauthProviderSlug,
    oauthScopesText, setOauthScopesText,
    advancedOpen, setAdvancedOpen,
    extraHeadersJson, setExtraHeadersJson,
    isHostedMcpConnect,
    authRequiresSecret,
    hostedMcpEnvVarError,
    cliAllowedDomainList,
    canConnect,
    reset,
  };
}

// Pre-submit validation. Returns `null` when the form is good to submit, or
// a user-facing error string otherwise. Throws are reserved for genuinely
// programmer-facing bugs — callers handle the returned message inline.
function validateMcpConnectFormInputs(formState) {
  const { targetKind, isHostedMcpConnect } = formState;
  if (!formState.serverName.trim()) return 'Display name is required';
  if (targetKind === 'cli' || isHostedMcpConnect) {
    if (!formState.cliPackageName.trim()) return 'NPM package is required';
    const version = formState.cliPackageVersion.trim();
    if (!version || !isExactSemver(version)) return 'Enter an exact npm package version such as 1.2.3';
    if (!formState.cliBinName.trim()) return 'Command/bin name is required';
    if (formState.cliAllowedDomainList.length === 0) return 'At least one allowed domain is required';
    if (targetKind === 'cli'
      && formState.cliAuthMode === 'api_key_env'
      && (!formState.apiKey.trim() || !formState.cliAuthEnvVar.trim())) {
      return 'API key environment variable and secret value must be provided together';
    }
    if (targetKind === 'cli' && formState.cliAuthMode === 'oauth_authorization_code') {
      if (!formState.cliAuthEnvVar.trim()) return 'Environment variable is required for CLI OAuth';
      const envError = validateHostedEnvVar(formState.cliAuthEnvVar);
      if (envError) return envError;
      if (!formState.oauthProviderSlug) return 'OAuth provider is required';
    }
    if (isHostedMcpConnect && formState.authType !== 'none') {
      if (!formState.hostedMcpAuthEnvVar.trim()) return 'Environment variable is required for hosted MCP authentication';
      const envError = validateHostedEnvVar(formState.hostedMcpAuthEnvVar);
      if (envError) return envError;
    }
    if (isHostedMcpConnect && formState.authRequiresSecret && !formState.apiKey.trim()) {
      return 'Secret value is required for hosted MCP authentication';
    }
    if (isHostedMcpConnect && formState.authType === 'oauth_authorization_code' && !formState.oauthProviderSlug) {
      return 'OAuth provider is required';
    }
  } else if (formState.authType === 'oauth_authorization_code') {
    if (!formState.serverUrl.trim()) return 'Endpoint URL is required for OAuth MCP servers';
  }
  return null;
}

// Build the create-server request body for the chosen flow. Returns
// `{ kind, payload, oauth }` where `kind` is 'cli' | 'hosted' | 'remote',
// and `oauth` is non-null when the resulting server should kick off an
// OAuth handshake right after creation. Throws a regular Error when
// `extraHeadersJson` is set but unparseable — both callers surface that
// message verbatim.
function buildMcpConnectPayload(formState) {
  const {
    targetKind,
    isHostedMcpConnect,
    serverName,
    serverUrl,
    cliPackageName,
    cliPackageVersion,
    cliBinName,
    cliAuthMode,
    cliAuthEnvVar,
    hostedMcpAuthEnvVar,
    cliAllowedDomainList,
    authType,
    authHeaderName,
    apiKey,
    oauthProviderSlug,
    extraHeadersJson,
    advancedOpen,
    oauthScopesText,
  } = formState;
  const cliSecretValue = apiKey.trim();
  // Strip a leading `Bearer ` that users sometimes paste from curl examples;
  // the auth profile re-adds it server-side. Other auth types take the raw
  // value verbatim.
  const mcpSecretValue = authType === 'bearer_token'
    ? apiKey.trim().replace(/^Bearer\s+/i, '')
    : apiKey.trim();
  const hostedEnvVar = hostedMcpAuthEnvVar.trim();

  // Extra headers live inside the modal's "Advanced" disclosure and are
  // mutually exclusive with the OAuth provider override. Gating inclusion
  // on `authType !== 'oauth_authorization_code'` makes sure a stale value
  // typed before the user switched to OAuth doesn't silently get sent.
  let extraHeaders = null;
  if (
    targetKind === 'mcp'
    && !isHostedMcpConnect
    && authType !== 'oauth_authorization_code'
    && extraHeadersJson.trim()
  ) {
    let parsed;
    try {
      parsed = JSON.parse(extraHeadersJson);
    } catch (parseError) {
      throw new Error(`Invalid extra headers JSON: ${parseError.message}`);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
      throw new Error('Extra headers must be a JSON object');
    }
    extraHeaders = parsed;
  }

  if (targetKind === 'cli') {
    const cliOAuthScopes = oauthScopesText.split(/\s+/).map((value) => value.trim()).filter(Boolean);
    return {
      kind: 'cli',
      payload: {
        name: serverName,
        targetKind: 'cli',
        connectionConfig: {
          cli: {
            command: cliBinName.trim(),
            package: {
              type: 'npm',
              packageName: cliPackageName.trim(),
              version: cliPackageVersion.trim(),
              bin: cliBinName.trim(),
            },
            version_args: ['--version'],
            discovery: { root_help_args: ['--help'], subcommand_help_arg: '--help' },
          },
        },
        networkPolicy: { allowedDomains: cliAllowedDomainList.slice() },
        authProfile: cliAuthMode === 'api_key_env' ? {
          name: `${cliAuthEnvVar.trim()} secret`,
          authType: 'api_key_env',
          secretBackend: 'aws_secrets_manager',
          secretValue: cliSecretValue,
          authConfig: {
            env_var: cliAuthEnvVar.trim(),
            delivery: { type: 'env', env_var: cliAuthEnvVar.trim() },
          },
        } : null,
        pendingOAuth: cliAuthMode === 'oauth_authorization_code' ? {
          authType: 'oauth_authorization_code',
          providerSlug: oauthProviderSlug,
          authConfig: {
            delivery: { type: 'env', env_var: cliAuthEnvVar.trim() },
            requested_scopes: cliOAuthScopes,
          },
        } : null,
      },
      oauth: null,
    };
  }

  if (isHostedMcpConnect) {
    const hostedAuthRequiresSecret = authType !== 'none' && authType !== 'oauth_authorization_code';
    return {
      kind: 'hosted',
      payload: {
        name: serverName,
        targetKind: 'mcp',
        transportType: 'stdio_hosted',
        connectionConfig: {
          hosted: {
            source_type: 'npm',
            package: {
              name: cliPackageName.trim(),
              version: cliPackageVersion.trim(),
              bin: cliBinName.trim(),
            },
          },
          env: authType === 'none' ? {} : {
            [hostedEnvVar]: authType === 'oauth_authorization_code' ? '${OAUTH_RUNTIME_TOKEN}' : '${MCP_AUTH_SECRET}',
          },
        },
        networkPolicy: { allowedDomains: cliAllowedDomainList.slice() },
        authProfile: hostedAuthRequiresSecret ? {
          name: `${hostedEnvVar} secret`,
          authType: authType === 'api_key_header' ? 'api_key_env' : authType,
          secretBackend: 'aws_secrets_manager',
          secretValue: mcpSecretValue,
          authConfig: { delivery: { type: 'env', env_var: hostedEnvVar } },
        } : null,
        pendingOAuth: authType === 'oauth_authorization_code' ? {
          authType: 'oauth_authorization_code',
          providerSlug: oauthProviderSlug,
          authConfig: { delivery: { type: 'env', env_var: hostedEnvVar } },
        } : null,
      },
      oauth: null,
    };
  }

  // Remote MCP server.
  const explicitProviderSlug = advancedOpen && oauthProviderSlug ? oauthProviderSlug : null;
  const requestedScopes = explicitProviderSlug
    ? oauthScopesText.split(/\s+/).map((value) => value.trim()).filter(Boolean)
    : [];
  return {
    kind: 'remote',
    payload: {
      name: serverName,
      targetKind: 'mcp',
      transportType: 'streamable_http',
      baseUrl: serverUrl || null,
      connectionConfig: extraHeaders ? { headers: extraHeaders } : {},
      // OAuth and "None" don't attach an auth profile at create time; OAuth
      // attaches it server-side as part of the start handshake immediately
      // after this POST resolves.
      authProfile: authType === 'none' || authType === 'oauth_authorization_code' ? null : {
        name: authType === 'bearer_token' ? 'Default bearer token' : `Default ${authHeaderName.trim()} API key`,
        authType,
        secretBackend: 'aws_secrets_manager',
        secretValue: mcpSecretValue,
        authConfig: authType === 'api_key_header' ? { header_name: authHeaderName.trim() } : {},
      },
    },
    oauth: authType === 'oauth_authorization_code' ? {
      providerSlug: explicitProviderSlug,
      requestedScopes,
    } : null,
  };
}

function hostedPackageForTarget(row) {
  return row?.connection_config?.hosted?.package
    || row?.connectionConfig?.hosted?.package
    || row?.connection_config?.cli?.package
    || row?.connectionConfig?.cli?.package
    || null;
}

function allowedDomainsForTarget(row) {
  const allowed = row?.network_policy?.allowed_domains
    || row?.networkPolicy?.allowed_domains
    || row?.network_policy?.allowedDomains
    || row?.networkPolicy?.allowedDomains
    || [];
  return Array.isArray(allowed) ? allowed : [];
}

function formatJsonForTextarea(value) {
  if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length === 0) return '';
  return JSON.stringify(value, null, 2);
}

function parseJsonObjectField(value, label) {
  const trimmed = String(value || '').trim();
  if (!trimmed) return null;
  let parsed;
  try {
    parsed = JSON.parse(trimmed);
  } catch (parseError) {
    throw new Error(`Invalid ${label} JSON: ${parseError.message}`);
  }
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error(`${label} must be a JSON object`);
  }
  return parsed;
}

function authTypeForEdit(profile, { hostedMcp = false } = {}) {
  if (!profile) return 'none';
  if (profile.auth_type === 'oauth_authorization_code') return 'oauth_authorization_code';
  if (profile.auth_type === 'bearer_token') return 'bearer_token';
  if (profile.auth_type === 'api_key_header') return 'api_key_header';
  if (profile.auth_type === 'api_key_env') return hostedMcp ? 'api_key_header' : 'none';
  return 'none';
}

function authEnvVarForProfile(profile) {
  const delivery = profile?.auth_config?.delivery || profile?.authConfig?.delivery;
  return delivery?.env_var || profile?.auth_config?.env_var || profile?.authConfig?.env_var || '';
}

function authProviderSlugForProfile(profile) {
  return profile?.auth_config?.provider_slug || profile?.authConfig?.provider_slug || '';
}

function authScopesTextForProfile(profile) {
  const scopes = profile?.auth_config?.requested_scopes || profile?.authConfig?.requested_scopes || [];
  return Array.isArray(scopes) ? scopes.join(' ') : '';
}

function buildEditAuthPayload({ formState, hostedMcp, existingProfile }) {
  const desiredAuthType = formState.authType;
  if (desiredAuthType === 'none') return null;
  const rawSecretValue = formState.apiKey.trim();
  const secretValue = desiredAuthType === 'bearer_token'
    ? rawSecretValue.replace(/^Bearer\s+/i, '')
    : rawSecretValue;
  const staticAuthTypeChanged = Boolean(existingProfile
    && existingProfile.auth_type !== desiredAuthType
    && existingProfile.auth_type !== 'oauth_authorization_code'
    && desiredAuthType !== 'oauth_authorization_code');
  if (!existingProfile && desiredAuthType !== 'oauth_authorization_code' && !secretValue) {
    throw new Error('Secret value is required when adding static authentication.');
  }
  if (staticAuthTypeChanged && !secretValue) {
    throw new Error('Secret value is required when changing static authentication type.');
  }
  if (hostedMcp) {
    const envVar = formState.hostedMcpAuthEnvVar.trim();
    const envError = validateHostedEnvVar(envVar);
    if (envError) throw new Error(envError);
    if (desiredAuthType === 'oauth_authorization_code') {
      return {
        authType: 'oauth_authorization_code',
        authConfig: { delivery: { type: 'env', env_var: envVar } },
      };
    }
    return {
      name: `${envVar} secret`,
      authType: 'api_key_env',
      authConfig: { delivery: { type: 'env', env_var: envVar } },
      ...(secretValue ? { secretValue } : {}),
      isDefault: true,
    };
  }
  if (desiredAuthType === 'oauth_authorization_code') {
    return {
      authType: 'oauth_authorization_code',
      authConfig: {},
    };
  }
  return {
    name: desiredAuthType === 'bearer_token' ? 'Default bearer token' : `Default ${formState.authHeaderName.trim()} API key`,
    authType: desiredAuthType,
    authConfig: desiredAuthType === 'api_key_header' ? { header_name: formState.authHeaderName.trim() } : {},
    ...(secretValue ? { secretValue } : {}),
    isDefault: true,
  };
}

// Pure form-body for the Connect MCP / Connect CLI flow. Renders the same
// section pattern (display name → URL/package → auth method → advanced)
// that ships in the production modal. The onboarding form embeds it under
// its own chrome (`OnboardingShell` instead of a modal panel) so users see
// the exact same layout from day-one as they will in the dashboard later.
//
// This component does NOT render OAuth-awaiting / CLI-setup states — those
// live above the form because the form itself is hidden during them.
function McpConnectFormBody({
  formState,
  saving,
  nameInputRef = null,
  storedSecretNotice = '',
  replacingStoredSecret = true,
  onStartSecretReplacement = null,
  connectedOAuthNotice = '',
}) {
  const {
    targetKind,
    serverName, setServerName,
    serverUrl, setServerUrl,
    mcpSourceMode, setMcpSourceMode,
    cliPackageName, setCliPackageName,
    cliPackageVersion, setCliPackageVersion,
    cliBinName, setCliBinName,
    cliAuthMode, setCliAuthMode,
    cliAuthEnvVar, setCliAuthEnvVar,
    hostedMcpAuthEnvVar, setHostedMcpAuthEnvVar,
    cliAllowedDomains, setCliAllowedDomains,
    authType, setAuthType,
    authHeaderName, setAuthHeaderName,
    apiKey, setApiKey,
    oauthProviderSlug, setOauthProviderSlug,
    oauthScopesText, setOauthScopesText,
    advancedOpen, setAdvancedOpen,
    extraHeadersJson, setExtraHeadersJson,
    isHostedMcpConnect,
    authRequiresSecret,
    hostedMcpEnvVarError,
  } = formState;
  const hasStoredSecretNotice = Boolean(storedSecretNotice && !replacingStoredSecret && authRequiresSecret);
  const storedSecretNoun = authType === 'bearer_token' ? 'token' : 'key';
  const oauthModeLabel = connectedOAuthNotice ? 'Reconnect OAuth' : 'OAuth';

  function renderSecretValueField({ label, placeholder }) {
    if (hasStoredSecretNotice) {
      return (
        <div className="field">
          <span className="field-label">{label}</span>
          <div className="row" style={{ gap: 8 }}>
            <input
              className="input mono"
              type="password"
              aria-label={label}
              placeholder={storedSecretNotice || placeholder}
              value=""
              disabled
              readOnly />
            <Button
              type="button"
              size="sm"
              onClick={onStartSecretReplacement}
              disabled={saving || !onStartSecretReplacement}>
              Change {storedSecretNoun}
            </Button>
          </div>
          <span className="field-help">A secret is already stored securely. Click Change {storedSecretNoun} to replace it.</span>
        </div>
      );
    }
    return (
      <label className="field">
        <span className="field-label">{label}</span>
        <input
          className="input mono"
          type="password"
          placeholder={placeholder}
          value={apiKey}
          disabled={saving}
          onChange={event => setApiKey(event.target.value)} />
      </label>
    );
  }

  return (
    <div className="mcp-form">

      {/* DISPLAY NAME — section heading + description, then the bare
          input. No field-label, since the heading already names it. */}
      <section className="mcp-form-section">
        <h4 className="mcp-form-section-title">Display name</h4>
        <p className="mcp-form-section-desc">How this target appears in the dashboard.</p>
        <input
          ref={nameInputRef}
          className="input"
          aria-label="Display name"
          placeholder={targetKind === 'cli' ? 'Acme CLI' : 'Armature Demo MCP'}
          value={serverName}
          disabled={saving}
          onChange={event => setServerName(event.target.value)} />
      </section>

      {/* MCP TYPE — Remote vs Local. "Local" relies on the hosted
          sandbox runtime. */}
      {targetKind === 'mcp' && (
        <section className="mcp-form-section">
          <h4 className="mcp-form-section-title">MCP type</h4>
          <div className="mcp-auth-mode-group" data-options="2" role="group" aria-label="MCP type">
            <button
              className={`mcp-auth-mode ${mcpSourceMode === HOSTED_MCP_SOURCE_REMOTE ? 'active' : ''}`}
              type="button"
              aria-pressed={mcpSourceMode === HOSTED_MCP_SOURCE_REMOTE}
              data-tooltip="Connect to a streamable HTTP MCP server you host yourself. The most common choice for production deployments."
              disabled={saving}
              onClick={() => setMcpSourceMode(HOSTED_MCP_SOURCE_REMOTE)}>
              <span>Remote</span>
              <Icon name="info" size={11} className="mcp-auth-mode-help" />
            </button>
            <button
              className={`mcp-auth-mode ${mcpSourceMode === HOSTED_MCP_SOURCE_NPM ? 'active' : ''}`}
              type="button"
              aria-pressed={mcpSourceMode === HOSTED_MCP_SOURCE_NPM}
              data-tooltip="We install an MCP server published to npm and run it for you inside an isolated worker. No remote endpoint required — only npm-published packages are supported today."
              disabled={saving}
              onClick={() => setMcpSourceMode(HOSTED_MCP_SOURCE_NPM)}>
              <span>Local (npm packages only)</span>
              <Icon name="info" size={11} className="mcp-auth-mode-help" />
            </button>
          </div>
        </section>
      )}

      {/* SERVER LOCATION — server URL for remote MCP, or package
          details (NPM package / version / command / allowed domains)
          for CLI and hosted-npm MCP. */}
      {targetKind === 'cli' || isHostedMcpConnect ? (
        <section className="mcp-form-section">
          <h4 className="mcp-form-section-title">Package</h4>
          <p className="mcp-form-section-desc">Use exact semver — ranges and tags like <code>latest</code> or <code>^1.2</code> aren't allowed.</p>
          <div className="mcp-form-row mcp-form-row-2-1">
            <label className="field">
              <span className="field-label">NPM package</span>
              <input
                className="input mono"
                placeholder={isHostedMcpConnect ? '@acme/mcp-server' : '@acme/cli'}
                value={cliPackageName}
                disabled={saving}
                onChange={event => setCliPackageName(event.target.value)} />
            </label>
            <label className="field">
              <span className="field-label">Exact version</span>
              <input
                className="input mono"
                placeholder="0.4.3"
                value={cliPackageVersion}
                disabled={saving}
                onChange={event => setCliPackageVersion(event.target.value)} />
            </label>
          </div>
          <label className="field">
            <span className="field-label">Command/bin name</span>
            <input
              className="input mono"
              placeholder={isHostedMcpConnect ? 'acme-mcp-server' : 'acme'}
              value={cliBinName}
              disabled={saving}
              onChange={event => setCliBinName(event.target.value)} />
            <span className="field-help">The executable inside the package — usually the package name without the scope. Check the <code>bin</code> field of the package's <code>package.json</code>.</span>
          </label>
          <label className="field">
            <span className="field-label">Allowed domains</span>
            <input
              className="input mono"
              placeholder={isHostedMcpConnect ? 'app.acme.com, api.acme.com' : 'api.vercel.com, api.example.com'}
              value={cliAllowedDomains}
              disabled={saving}
              onChange={event => setCliAllowedDomains(event.target.value)} />
            <span className="field-help">Runtime API hosts the package may call from the sandbox. Only allow domains you trust; the CLI can send runtime data to these hosts. Private networks, cloud metadata, and Armature infrastructure remain blocked.</span>
          </label>
        </section>
      ) : (
        <section className="mcp-form-section">
          <h4 className="mcp-form-section-title">Server URL</h4>
          <p className="mcp-form-section-desc">The streamable HTTP URL of your remote MCP server. SSE transport is not supported.</p>
          <input
            className="input"
            aria-label="Server URL"
            placeholder="https://mcp.example.com"
            value={serverUrl}
            disabled={saving}
            onChange={event => setServerUrl(event.target.value)} />
        </section>
      )}

      {/* AUTHENTICATION METHOD */}
      <section className="mcp-form-section">
        <h4 className="mcp-form-section-title">Authentication method</h4>

        {targetKind === 'cli' ? (
          <>
            <div className="mcp-auth-mode-group" data-options="3" role="group" aria-label="CLI authentication mode">
              <button
                className={`mcp-auth-mode ${cliAuthMode === 'none' ? 'active' : ''}`}
                type="button"
                aria-pressed={cliAuthMode === 'none'}
                data-tooltip="The CLI does not need an API key. Use this for tools that authenticate some other way (e.g. OAuth flows the CLI runs itself)."
                disabled={saving}
                onClick={() => {
                  setCliAuthMode('none');
                  setCliAuthEnvVar('');
                  setApiKey('');
                }}>
                <span>No auth</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
              <button
                className={`mcp-auth-mode ${cliAuthMode === 'api_key_env' ? 'active' : ''}`}
                type="button"
                aria-pressed={cliAuthMode === 'api_key_env'}
                data-tooltip="Inject a static API key as an environment variable when the CLI runs. Use this when the CLI reads its credentials from an env var like ACME_API_KEY."
                disabled={saving}
                onClick={() => setCliAuthMode('api_key_env')}>
                <span>API key env</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
              <button
                className={`mcp-auth-mode ${cliAuthMode === 'oauth_authorization_code' ? 'active' : ''}`}
                type="button"
                aria-pressed={cliAuthMode === 'oauth_authorization_code'}
                data-tooltip="Each user signs in with the upstream provider; we inject the resulting access token as an environment variable when the CLI runs."
                disabled={saving}
                onClick={() => {
                  setCliAuthMode('oauth_authorization_code');
                  setApiKey('');
                }}>
                <span>{oauthModeLabel}</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
            </div>
            {cliAuthMode === 'api_key_env' && (
              <div className="mcp-form-row mcp-form-row-1-1">
                <label className="field">
                  <span className="field-label">API key environment variable</span>
                  <input
                    className="input mono"
                    placeholder="ACME_API_KEY"
                    value={cliAuthEnvVar}
                    disabled={saving}
                    onChange={event => setCliAuthEnvVar(event.target.value)} />
                </label>
                {renderSecretValueField({ label: 'Secret value', placeholder: 'API key' })}
              </div>
            )}
            {cliAuthMode === 'oauth_authorization_code' && (
              <>
                <label className="field">
                  <span className="field-label">Env var name</span>
                  <input
                    className={`input mono ${validateHostedEnvVar(cliAuthEnvVar) ? 'input-error' : ''}`}
                    placeholder="ACME_ACCESS_TOKEN"
                    value={cliAuthEnvVar}
                    aria-invalid={validateHostedEnvVar(cliAuthEnvVar) ? 'true' : undefined}
                    disabled={saving}
                    onChange={event => setCliAuthEnvVar(event.target.value)} />
                  {validateHostedEnvVar(cliAuthEnvVar) ? (
                    <span className="field-help error-text">{validateHostedEnvVar(cliAuthEnvVar)}</span>
                  ) : (
                    <span className="field-help">The env var your CLI reads at startup.</span>
                  )}
                </label>
                <OauthCuratedConfig
                  providerSlug={oauthProviderSlug}
                  setProviderSlug={(slug) => {
                    setOauthProviderSlug(slug);
                    if (HOSTED_AUTH_ENV_DEFAULTS.has(cliAuthEnvVar) || !cliAuthEnvVar.trim()) {
                      setCliAuthEnvVar(hostedOauthEnvVar(slug));
                    }
                  }}
                  scopesText={oauthScopesText}
                  setScopesText={setOauthScopesText}
                  saving={saving}
                />
              </>
            )}
          </>
        ) : isHostedMcpConnect ? (
          <>
            <div className="mcp-auth-mode-group" data-options="4" role="group" aria-label="Hosted MCP authentication method">
              <button
                className={`mcp-auth-mode ${authType === 'oauth_authorization_code' ? 'active' : ''}`}
                type="button"
                aria-pressed={authType === 'oauth_authorization_code'}
                data-tooltip="Each user signs in with the upstream provider; we hand the package a fresh access token at run time. Recommended whenever the provider supports it."
                disabled={saving}
                onClick={() => setAuthType('oauth_authorization_code')}>
                <span>{oauthModeLabel}</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
              <button
                className={`mcp-auth-mode ${authType === 'bearer_token' ? 'active' : ''}`}
                type="button"
                aria-pressed={authType === 'bearer_token'}
                data-tooltip="Inject a static bearer token as an environment variable. Same token for every caller — fine for service-to-service auth."
                disabled={saving}
                onClick={() => setAuthType('bearer_token')}>
                <span>Bearer</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
              <button
                className={`mcp-auth-mode ${authType === 'api_key_header' ? 'active' : ''}`}
                type="button"
                aria-pressed={authType === 'api_key_header'}
                data-tooltip="Inject a static API key as an environment variable. Same key for every caller. Use this when the package reads ACME_API_KEY (or similar) at startup."
                disabled={saving}
                onClick={() => setAuthType('api_key_header')}>
                <span>API key</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
              <button
                className={`mcp-auth-mode ${authType === 'none' ? 'active' : ''}`}
                type="button"
                aria-pressed={authType === 'none'}
                data-tooltip="The package does not need credentials. Only safe for public or unauthenticated MCP servers."
                disabled={saving}
                onClick={() => setAuthType('none')}>
                <span>None</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
            </div>
            {authType !== 'none' && (
              <div className="mcp-form-row mcp-form-row-1-1">
                <label className="field">
                  <span className="field-label">Env var name</span>
                  <input
                    className={`input mono ${hostedMcpEnvVarError ? 'input-error' : ''}`}
                    placeholder={authType === 'oauth_authorization_code' ? 'ACME_ACCESS_TOKEN' : 'ACME_API_KEY'}
                    value={hostedMcpAuthEnvVar}
                    aria-invalid={hostedMcpEnvVarError ? 'true' : undefined}
                    disabled={saving}
                    onChange={event => setHostedMcpAuthEnvVar(event.target.value)} />
                  {hostedMcpEnvVarError ? (
                    <span className="field-help error-text">{hostedMcpEnvVarError}</span>
                  ) : (
                    <span className="field-help">The env var your package reads at startup.</span>
                  )}
                </label>
                {authRequiresSecret && (
                  renderSecretValueField({
                    label: 'Value',
                    placeholder: authType === 'bearer_token' ? 'Bearer token' : 'API key',
                  })
                )}
              </div>
            )}
            {authType === 'oauth_authorization_code' && (
              <OauthCuratedConfig
                providerSlug={oauthProviderSlug}
                setProviderSlug={(slug) => {
                  setOauthProviderSlug(slug);
                  setHostedMcpAuthEnvVar(hostedOauthEnvVar(slug));
                }}
                scopesText={oauthScopesText}
                setScopesText={setOauthScopesText}
                saving={saving}
              />
            )}
          </>
        ) : (
          <>
            <div className="mcp-auth-mode-group" data-options="4" role="group" aria-label="Authentication method">
              <button
                className={`mcp-auth-mode ${authType === 'oauth_authorization_code' ? 'active' : ''}`}
                type="button"
                aria-pressed={authType === 'oauth_authorization_code'}
                data-tooltip="Each user signs in to the upstream provider with the standard authorization code flow; we never see a shared secret. Recommended."
                disabled={saving}
                onClick={() => setAuthType('oauth_authorization_code')}>
                <span>{oauthModeLabel}</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
              <button
                className={`mcp-auth-mode ${authType === 'bearer_token' ? 'active' : ''}`}
                type="button"
                aria-pressed={authType === 'bearer_token'}
                data-tooltip="A static bearer token sent in the Authorization header. The same token is used for every caller."
                disabled={saving}
                onClick={() => setAuthType('bearer_token')}>
                <span>Bearer</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
              <button
                className={`mcp-auth-mode ${authType === 'api_key_header' ? 'active' : ''}`}
                type="button"
                aria-pressed={authType === 'api_key_header'}
                data-tooltip="A static API key sent in a custom header (for example X-API-Key). The same key is used for every caller."
                disabled={saving}
                onClick={() => setAuthType('api_key_header')}>
                <span>API key</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
              <button
                className={`mcp-auth-mode ${authType === 'none' ? 'active' : ''}`}
                type="button"
                aria-pressed={authType === 'none'}
                data-tooltip="No credentials are sent. Only safe for public or unauthenticated MCP servers."
                disabled={saving}
                onClick={() => setAuthType('none')}>
                <span>None</span>
                <Icon name="info" size={11} className="mcp-auth-mode-help" />
              </button>
            </div>
            {authType === 'api_key_header' && (
              <div className="mcp-form-row mcp-form-row-1-1">
                <label className="field">
                  <span className="field-label">Header name</span>
                  <input
                    className="input mono"
                    placeholder="x-api-key"
                    value={authHeaderName}
                    disabled={saving}
                    onChange={event => setAuthHeaderName(event.target.value)} />
                </label>
                {renderSecretValueField({ label: 'Value', placeholder: 'API key' })}
              </div>
            )}
            {authType === 'bearer_token' && (
              renderSecretValueField({ label: 'Bearer token', placeholder: 'Bearer token' })
            )}
          </>
        )}
      </section>

      {/* ADVANCED — only for remote MCP. Holds the OAuth curated-provider
          override when OAuth is the chosen auth method, or extra request
          headers otherwise. Most users won't need to open this. */}
      {targetKind === 'mcp' && !isHostedMcpConnect && (
        <details
          className="mcp-form-advanced"
          open={advancedOpen}
          onToggle={(event) => setAdvancedOpen(event.currentTarget.open)}>
          <summary className="mcp-form-advanced-summary">
            <span>Advanced (rarely needed)</span>
            <span className="mcp-form-advanced-hint">
              {authType === 'oauth_authorization_code' ? 'OAuth provider override' : 'Extra headers'}
            </span>
          </summary>
          <div className="mcp-form-advanced-body">
            {authType === 'oauth_authorization_code' ? (
              <OauthOverrideFields
                providerSlug={oauthProviderSlug}
                setProviderSlug={setOauthProviderSlug}
                scopesText={oauthScopesText}
                setScopesText={setOauthScopesText}
                saving={saving}
                enabled={advancedOpen}
              />
            ) : (
              <label className="field">
                <span className="field-label">Extra headers (JSON)</span>
                <textarea
                  className="input mono"
                  rows={3}
                  placeholder='{"X-Base-URL":"https://..."}'
                  value={extraHeadersJson}
                  disabled={saving}
                  onChange={event => setExtraHeadersJson(event.target.value)} />
                <span className="field-help">
                  Custom headers added to every request to your MCP server.
                  Use JSON like <code>{'{"X-Tenant":"acme"}'}</code>. Leave empty
                  unless your server requires extras beyond the auth method above.
                </span>
              </label>
            )}
          </div>
        </details>
      )}
    </div>
  );
}

function McpConnectModal({
  open,
  message,
  formState,
  saving,
  oauthInFlight,
  onCancelOauth,
  onSubmit,
  onCancel,
  cliSetupState,
  onOpenSetupTarget,
  // The benchmark-source variant of this modal hides the "Open target"
  // CTA (benchmark sources are managed from /benchmark, not from a
  // detail page) and renames the submit button. Both default to the
  // production behavior.
  submitLabel = '',
  submitLoadingLabel = '',
  showOpenTargetCta = true,
}) {
  const targetKind = formState.targetKind;
  const canConnectServer = formState.canConnect;
  const hasSetupProgress = Boolean(cliSetupState);
  const setupKind = cliSetupState?.setupKind || (targetKind === 'cli' ? 'cli' : 'hosted');
  const dialogRef = useRefMcp(null);
  const nameInputRef = useRefMcp(null);
  const previousActiveRef = useRefMcp(null);
  const savingRef = useRefMcp(saving);
  const onCancelRef = useRefMcp(onCancel);

  useEffectMcp(() => {
    savingRef.current = saving;
    onCancelRef.current = onCancel;
  }, [saving, onCancel]);

  useEffectMcp(() => {
    if (!open) return undefined;
    previousActiveRef.current = document.activeElement;

    function onKeyDown(event) {
      if (event.key === 'Escape' && !savingRef.current) {
        onCancelRef.current();
        return;
      }
      if (event.key !== 'Tab' || !dialogRef.current) return;

      const focusable = Array.from(dialogRef.current.querySelectorAll('button:not(:disabled), input:not(:disabled), textarea:not(:disabled)'));
      if (!focusable.length) return;
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      }
    }

    document.addEventListener('keydown', onKeyDown);
    requestAnimationFrame(() => nameInputRef.current?.focus());
    return () => {
      document.removeEventListener('keydown', onKeyDown);
      if (previousActiveRef.current instanceof HTMLElement && previousActiveRef.current.isConnected) {
        previousActiveRef.current.focus();
      }
    };
  }, [open]);

  if (!open) return null;

  function submitForm(event) {
    event.preventDefault();
    if (canConnectServer) onSubmit();
  }

  return (
    <div
      className="dialog-backdrop"
      onMouseDown={(event) => {
        if (event.target === event.currentTarget && !saving) onCancel();
      }}>
      <form
        ref={dialogRef}
        className="modal-panel mcp-connect-modal"
        role="dialog"
        aria-modal="true"
        aria-labelledby="mcp-connect-modal-title"
        onSubmit={submitForm}>
        <div className="modal-header">
          <div>
            <div className="modal-title" id="mcp-connect-modal-title">
              {hasSetupProgress
                ? setupKind === 'cli' ? 'CLI setup' : 'MCP setup'
                : targetKind === 'cli' ? 'Add CLI target' : 'Connect MCP server'}
            </div>
            {(targetKind === 'cli' || hasSetupProgress) && (
              <div className="modal-subtitle">
                {cliSetupState
                  ? cliSetupState.serverName || (setupKind === 'cli' ? 'Hosted CLI target' : 'Hosted MCP server')
                  : 'Install an npm package and discover its commands.'}
              </div>
            )}
          </div>
          <button className="icon-btn" type="button" aria-label="Close" disabled={saving} onClick={onCancel}>
            <Icon name="x" size={15} />
          </button>
        </div>
        <div className="modal-body">
          {message && <div className="auth-message">{message}</div>}
          {oauthInFlight ? (
            <McpOauthAwaitingPanel inFlight={oauthInFlight} />
          ) : hasSetupProgress ? (
            <CliSetupProgress state={cliSetupState} setupKind={setupKind} />
          ) : (
            <McpConnectFormBody
              formState={formState}
              saving={saving}
              nameInputRef={nameInputRef} />
          )}
        </div>
        <div className="modal-actions">
          {oauthInFlight ? (
            // For loopback flows the popup is owned by
            // LoopbackOAuthModalRoot (rendered on top of this modal), so a
            // Cancel button here would be a visible no-op — the user has
            // to use the loopback modal's own Cancel. Hide it in that
            // case to avoid dead-button confusion.
            oauthInFlight.redirectUriMode !== 'loopback' && (
              <Button onClick={onCancelOauth}>Cancel sign-in</Button>
            )
          ) : hasSetupProgress ? (
            <>
              <Button disabled={saving} onClick={onCancel}>Close</Button>
              {showOpenTargetCta && (
                <Button variant="primary" onClick={onOpenSetupTarget}>
                  <Icon name="chevronRight" size={12} />Open target
                </Button>
              )}
            </>
          ) : (
            <>
              <Button disabled={saving} onClick={onCancel}>Cancel</Button>
              <Button
                variant="primary"
                type="submit"
                disabled={!canConnectServer}
                loading={saving}
                loadingLabel={submitLoadingLabel || (targetKind === 'cli' ? 'Adding...' : 'Connecting...')}>
                <Icon name={targetKind === 'cli' ? 'terminal' : 'mcp'} size={13} />
                {submitLabel || (targetKind === 'cli' ? 'Add CLI' : 'Connect MCP server')}
              </Button>
            </>
          )}
        </div>
      </form>
    </div>
  );
}

function setupStepLabel(step, setupKind = 'cli', status = '') {
  if (setupKind === 'cli') return step.label;
  if (status === 'auth_required' && step.status === 'ready') return 'OAuth required';
  if (step.status === 'discovering') return 'Discovering tools';
  if (step.status === 'ready') return 'Tools ready';
  return step.label;
}

function CliSetupProgress({ state, setupKind = 'cli' }) {
  const status = state?.status || 'pending';
  const copy = cliSetupCopy(status, setupKind, state?.errorCode);
  const isFailed = status === 'failed';
  const isAuthRequired = status === 'auth_required';
  const isComplete = status === 'ready' || status === 'needs_review';
  const stepIndex = CLI_SETUP_STEPS.findIndex((step) => step.status === status);
  const activeIndex = stepIndex >= 0
    ? stepIndex
    : (isComplete || isAuthRequired) ? CLI_SETUP_STEPS.length - 1 : 0;
  // When status is `failed` the prior phase is gone (server overwrote it).
  // The polling reducer freezes the last pending phase into `failedAtStatus`
  // so we can paint the red node where the failure actually happened
  // (Installing / Discovering) instead of always falling back to Queued.
  const failedAtIndex = isFailed && state?.failedAtStatus
    ? CLI_SETUP_STEPS.findIndex((step) => step.status === state.failedAtStatus)
    : -1;
  const failedIndex = isFailed
    ? (failedAtIndex >= 0 ? failedAtIndex : Math.max(activeIndex, 0))
    : -1;
  const stepCount = CLI_SETUP_STEPS.length;
  const completedFraction = isComplete
    ? 1
    : isFailed
      ? failedIndex / Math.max(stepCount - 1, 1)
      : activeIndex / Math.max(stepCount - 1, 1);

  return (
    <div className="cli-setup">
      <div className="cli-setup-header">
        <div className={`cli-setup-header-mark ${isFailed || isAuthRequired ? 'is-failed' : isComplete ? 'is-complete' : 'is-active'}`}>
          <Icon name={isFailed || isAuthRequired ? 'alert' : isComplete ? 'check' : 'terminal'} size={16} />
        </div>
        <div className="cli-setup-header-text">
          <div className="cli-setup-header-title">{copy.title}</div>
          <div className="cli-setup-header-body">{copy.body}</div>
        </div>
      </div>
      <div
        className={`cli-setup-timeline ${isFailed ? 'is-failed' : ''} ${isComplete ? 'is-complete' : ''}`}
        role="progressbar"
        aria-valuemin={0}
        aria-valuemax={stepCount}
        aria-valuenow={isComplete ? stepCount : activeIndex + 1}
        aria-valuetext={copy.title}>
        <div className="cli-setup-timeline-track" aria-hidden="true">
          <div
            className="cli-setup-timeline-fill"
            style={{ width: `${Math.min(100, Math.max(0, completedFraction * 100))}%` }} />
          {!isFailed && !isComplete && (
            <div
              className="cli-setup-timeline-sweep"
              style={{
                left: `${(activeIndex / Math.max(stepCount - 1, 1)) * 100}%`,
                width: `${(1 / Math.max(stepCount - 1, 1)) * 100}%`,
              }} />
          )}
        </div>
        <ol className="cli-setup-timeline-nodes">
          {CLI_SETUP_STEPS.map((step, index) => {
            // Steps before the failure remain done (the green fill already
            // shows progress up to the failure node, and the per-step state
            // should match it). Only the failed step itself flips red.
            const isDone = isComplete
              ? true
              : isFailed
                ? index < failedIndex
                : index < activeIndex;
            const isActive = !isFailed && !isComplete && !isAuthRequired && index === activeIndex;
            const isStepFailed = (isFailed && index === failedIndex) || (isAuthRequired && index === activeIndex);
            const stateClass = isStepFailed
              ? 'is-failed'
              : isDone
                ? 'is-done'
                : isActive
                  ? 'is-active'
                  : 'is-idle';
            return (
              <li key={step.status} className={`cli-setup-timeline-step ${stateClass}`}>
                <span className="cli-setup-timeline-node" aria-hidden="true">
                  {isStepFailed && <Icon name="alert" size={12} />}
                  {isDone && !isStepFailed && <Icon name="check" size={12} />}
                  {isActive && <span className="cli-setup-timeline-node-spinner" />}
                </span>
                <span className="cli-setup-timeline-label">{setupStepLabel(step, setupKind, status)}</span>
              </li>
            );
          })}
        </ol>
      </div>
      {isFailed && (
        <p className="cli-setup-note">
          {setupKind === 'cli'
            ? 'Setup can fail for CLIs that are not MCP-compatible, do not expose stable help output, or require local files not available in the hosted runtime.'
            : 'Setup can fail for MCP packages that are not MCP-compatible, do not expose tools, or require local files not available in the hosted runtime.'}
        </p>
      )}
      {isFailed && state?.errorCode && (
        <div className="cli-setup-meta">
          <span>Error code</span>
          <span className="mono">{state.errorCode}</span>
        </div>
      )}
      {state?.provisioningRunId && (
        <div className="cli-setup-meta">
          <span>Diagnostic ID</span>
          <span className="mono">{state.provisioningRunId}</span>
        </div>
      )}
    </div>
  );
}

// ---------- MCP server card + detail drawer ----------

function McpServerCard({ server, monitors = [], onOpen, onPeekMonitors, onDelete }) {
  const targetKind = server.target_kind || server.targetKind || 'mcp';
  const isHostedMcpTarget = isHostedMcpServerRow(server);
  const monitorRows = monitors;
  const passing = monitorRows.filter((m) => m.last_status === 'pass').length;
  const failing = monitorRows.filter((m) => m.last_status === 'fail' || m.last_status === 'error').length;
  const status = targetKind === 'cli' || isHostedMcpTarget
    ? (server.provisioning_status || server.last_connection_status || server.latest_connection_test?.status)
    : (server.last_connection_status || server.latest_connection_test?.status);
  const lastTested = server.latest_connection_test?.created_at || server.last_connected_at;
  const dotTone = targetKind === 'cli' || isHostedMcpTarget ? cliStatusTone(status) : status === 'success' || status === 'ok'
    ? 'ok'
    : status === 'failed' || status === 'error'
      ? 'bad'
      : status
        ? 'warn'
        : 'neutral';
  const statusLabel = targetKind === 'cli' || isHostedMcpTarget ? cliStatusLabel(status) : status === 'success' ? 'reachable'
    : status === 'ok' ? 'reachable'
    : status === 'failed' ? 'failed'
    : status === 'error' ? 'error'
    : status || 'unknown';
  const targetNoun = targetKind === 'cli' ? 'CLI' : 'MCP server';
  // The benchmark sources area on /benchmark renders this card without
  // a detail drawer (yet) — passing onOpen={null} drops the role=button
  // affordance so the card doesn't pretend to be clickable. The
  // monitors strip below is also hidden in that mode (benchmark sources
  // are never monitored).
  const interactive = typeof onOpen === 'function';
  const showMonitors = typeof onPeekMonitors === 'function';
  return (
    <div
      className={`mcp-card ${interactive ? 'ui-surface-interactive' : 'ui-surface'}`}
      role={interactive ? 'button' : undefined}
      tabIndex={interactive ? 0 : undefined}
      onClick={interactive ? onOpen : undefined}
      onKeyDown={interactive
        ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(); } }
        : undefined}>
      <div className="mcp-card-head">
        <div className="mcp-card-icon"><Icon name={targetKind === 'cli' ? 'terminal' : 'mcp'} size={16} /></div>
        <div className="mcp-card-identity">
          <div className="mcp-card-name">{server.name}</div>
          <div className="mcp-card-url mono">{targetKind === 'cli' ? server.connection_config?.cli?.command || 'CLI command' : server.base_url || 'No base URL'}</div>
        </div>
        {onDelete && (
          <button
            type="button"
            className="mcp-card-delete"
            aria-label={`Delete ${targetNoun} ${server.name}`}
            title={`Delete ${targetNoun}`}
            onClick={(event) => { event.stopPropagation(); onDelete(); }}
            onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') event.stopPropagation(); }}>
            <Icon name="trash" size={13} />
          </button>
        )}
      </div>
      <div className="mcp-card-meta">
        <Pill>{targetKind === 'cli' ? 'CLI' : 'MCP'}</Pill>
        <Pill tone={dotTone === 'ok' ? 'ok' : dotTone === 'bad' ? 'bad' : dotTone === 'warn' ? 'warn' : 'neutral'} dot>
          {statusLabel}
        </Pill>
        <span className="mcp-card-meta-item">
          <Icon name="clock" size={11} />
          <span>{lastTested ? `Tested ${formatRelative(lastTested)}` : 'Never tested'}</span>
        </span>
        {showMonitors && (
          monitorRows.length > 0 ? (
            <button
              className="mcp-card-meta-link"
              onClick={(event) => { event.stopPropagation(); onPeekMonitors(); }}
              title="Peek at monitors">
              <Icon name="zap" size={11} />
              <span>{monitorRows.length} monitor{monitorRows.length === 1 ? '' : 's'}</span>
              {failing > 0 && <span className="ui-pill ui-pill-bad">{failing} failing</span>}
              {failing === 0 && passing > 0 && <span className="ui-pill ui-pill-ok">{passing} passing</span>}
            </button>
          ) : (
            <span className="mcp-card-meta-item muted">
              <Icon name="zap" size={11} />
              <span>No monitors</span>
            </span>
          )
        )}
      </div>
      {interactive && (
        <div className="mcp-card-cta">
          <span>Open</span>
          <Icon name="chevronRight" size={12} />
        </div>
      )}
    </div>
  );
}

function DetailTab({ id, current, onSelect, children }) {
  const isActive = current === id;
  return (
    <button
      type="button"
      role="tab"
      aria-selected={isActive}
      tabIndex={isActive ? 0 : -1}
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={() => onSelect(id)}
      onKeyDown={(event) => {
        if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault();
          onSelect(id);
        }
      }}>
      {children}
    </button>
  );
}

function McpServerDetail({ serverId, initialTab = 'tools', navigate, onClose, onDeleted, onEdited, onRefreshed, onCreateMonitor, onCreateWorkflow, onViewMonitors }) {
  const auth = useAuth();
  const toast = useToast();
  const detail = useApiResource(serverId ? `/api/mcp-servers/${serverId}` : null, [serverId]);
  const monitors = useApiResource(serverId ? `/api/mcp-servers/${serverId}/tool-monitors` : null, [serverId]);
  const [editing, setEditing] = useStateMcp(false);
  const [editName, setEditName] = useStateMcp('');
  const [editUrl, setEditUrl] = useStateMcp('');
  // Hosted-npm fields (used for hosted MCP and hosted CLI). For preinstalled
  // CLI and remote HTTP MCP these stay empty and aren't rendered.
  const [editPackageName, setEditPackageName] = useStateMcp('');
  const [editPackageVersion, setEditPackageVersion] = useStateMcp('');
  const [editPackageBin, setEditPackageBin] = useStateMcp('');
  const [editAllowedDomains, setEditAllowedDomains] = useStateMcp('');
  // Snapshot of the hosted source at the moment we loaded the target so we
  // only re-send `connectionConfig` / `networkPolicy` (which would re-queue
  // provisioning) when the user actually changed something. Held as state so
  // changes propagate to render — refs would defer one cycle.
  const [editHostedSnapshot, setEditHostedSnapshot] = useStateMcp(null);
  const [editSaving, setEditSaving] = useStateMcp(false);
  const [editError, setEditError] = useStateMcp('');
  const [editReplacingSecret, setEditReplacingSecret] = useStateMcp(false);
  const [probe, setProbe] = useStateMcp(null);
  const [probing, setProbing] = useStateMcp(false);
  const [deleting, setDeleting] = useStateMcp(false);
  const [reconnectingOAuth, setReconnectingOAuth] = useStateMcp(false);
  const [retryingProvisioning, setRetryingProvisioning] = useStateMcp(false);
  const [tab, setTab] = useStateMcp(initialTab);
  const [commandRiskFilter, setCommandRiskFilter] = useStateMcp('all');
  const [commandMonitoredFilter, setCommandMonitoredFilter] = useStateMcp('all');
  const [commandSort, setCommandSort] = useStateMcp('name');

  useEffectMcp(() => {
    if (serverId) setTab(initialTab);
  }, [serverId, initialTab]);

  const onCloseRef = useRefMcp(onClose);
  const escGuardRef = useRefMcp({ editSaving, deleting });
  useEffectMcp(() => {
    onCloseRef.current = onClose;
    escGuardRef.current = { editSaving, deleting };
  });
  useEffectMcp(() => {
    if (!serverId) {
      setEditing(false);
      setProbe(null);
      setEditError('');
      return undefined;
    }
    function onKey(event) {
      const guard = escGuardRef.current;
      if (event.key === 'Escape' && !guard.editSaving && !guard.deleting) onCloseRef.current?.();
    }
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [serverId]);

  // Reset per-server command filters whenever the user opens a different
  // target. Without this a "Monitored" or "Mutating" filter set on one CLI
  // would silently carry over to the next CLI, making it look like the
  // second target has no matching commands.
  useEffectMcp(() => {
    setCommandRiskFilter('all');
    setCommandMonitoredFilter('all');
    setCommandSort('name');
  }, [serverId]);

  const loadedServer = detail.data?.mcpServer;
  const loadedTargetKind = loadedServer?.target_kind || loadedServer?.targetKind || 'mcp';
  const isCliTarget = loadedTargetKind === 'cli';
  const loadedDefaultAuthProfile = defaultAuthProfileForServer(loadedServer, detail.data?.authProfiles);
  const editForm = useMcpConnectFormState({ targetKind: loadedTargetKind });
  const commandReviews = useApiResource(
    serverId && loadedTargetKind === 'cli' ? `/api/mcp-servers/${serverId}/cli-command-reviews` : null,
    [serverId, loadedTargetKind],
  );

  useEffectMcp(() => {
    const row = loadedServer;
    if (!row) return;
    setEditName(row.name || '');
    setEditUrl(row.base_url || '');
    // For hosted-npm targets (MCP `stdio_hosted`, or CLI with an npm
    // package), the package spec lives in connection_config. Preinstalled
    // CLI keeps its `command` directly on `cli`.
    const hostedPackage = hostedPackageForTarget(row);
    const hostedIsNpm = hostedPackage?.type === 'npm' || hostedPackage?.source_type === 'npm';
    const allowedList = allowedDomainsForTarget(row);
    setEditAllowedDomains(allowedList.join(', '));
    if (hostedIsNpm) {
      const pkgName = hostedPackage.package_name || hostedPackage.packageName || hostedPackage.name || '';
      const pkgVersion = hostedPackage.version || '';
      const pkgBin = hostedPackage.bin || hostedPackage.command || '';
      setEditPackageName(pkgName);
      setEditPackageVersion(pkgVersion);
      setEditPackageBin(pkgBin);
      setEditHostedSnapshot({
        package_name: pkgName,
        version: pkgVersion,
        bin: pkgBin,
        allowed_domains: allowedList.slice(),
      });
    } else {
      setEditPackageName('');
      setEditPackageVersion('');
      setEditPackageBin('');
      setEditHostedSnapshot(null);
    }
  }, [loadedServer?.id]);

  useEffectMcp(() => {
    const row = loadedServer;
    if (!row || isCliTarget) return;
    const hostedPackage = hostedPackageForTarget(row);
    const isHostedNpm = isHostedMcpServerRow(row);
    editForm.setServerName(row.name || '');
    editForm.setServerUrl(row.base_url || '');
    editForm.setMcpSourceMode(isHostedNpm ? HOSTED_MCP_SOURCE_NPM : HOSTED_MCP_SOURCE_REMOTE);
    editForm.setCliAllowedDomains(allowedDomainsForTarget(row).join(', '));
    if (hostedPackage) {
      editForm.setCliPackageName(hostedPackage.package_name || hostedPackage.packageName || hostedPackage.name || '');
      editForm.setCliPackageVersion(hostedPackage.version || '');
      editForm.setCliBinName(hostedPackage.bin || hostedPackage.command || '');
    } else {
      editForm.setCliPackageName('');
      editForm.setCliPackageVersion('');
      editForm.setCliBinName('');
    }
    const headers = row.connection_config?.headers || row.connectionConfig?.headers || null;
    editForm.setExtraHeadersJson(formatJsonForTextarea(headers));
    const profile = loadedDefaultAuthProfile;
    const nextAuthType = authTypeForEdit(profile, { hostedMcp: isHostedNpm });
    editForm.setAuthType(nextAuthType);
    editForm.setAuthHeaderName(profile?.auth_config?.header_name || profile?.authConfig?.header_name || 'x-api-key');
    editForm.setApiKey('');
    setEditReplacingSecret(false);
    editForm.setOauthProviderSlug(authProviderSlugForProfile(profile));
    editForm.setOauthScopesText(authScopesTextForProfile(profile));
    editForm.setHostedMcpAuthEnvVar(authEnvVarForProfile(profile) || (nextAuthType === 'oauth_authorization_code' ? 'ACME_ACCESS_TOKEN' : 'ACME_API_KEY'));
    editForm.setAdvancedOpen(Boolean(headers && Object.keys(headers).length > 0) || nextAuthType === 'oauth_authorization_code');
  }, [loadedServer?.id, loadedDefaultAuthProfile?.id]);

  useEffectMcp(() => {
    if (!serverId) return;
    setProbe(null);
    if (['active', 'trialing'].includes(auth?.me?.organization?.subscriptionStatus)) {
      if (loadedServer && loadedTargetKind === 'mcp') {
        if (isHostedMcpServerRow(loadedServer)) {
          setProbing(false);
          return;
        }
        runProbe();
      } else {
        setProbing(false);
      }
    } else {
      setProbing(false);
    }
  }, [serverId, loadedServer?.id, loadedTargetKind, auth?.me?.organization?.subscriptionStatus]);

  useEffectMcp(() => {
    if (!serverId) return undefined;
    const manualDiscoveryQueued = isCliTarget && probe?.status === 'queued';
    const status = loadedServer?.provisioning_status || loadedServer?.provisioningStatus;
    const shouldPoll = (
      manualDiscoveryQueued || ((loadedTargetKind === 'cli'
        || isHostedMcpServerRow(loadedServer)
      ) && CLI_PENDING_STATUSES.has(status))
    );
    if (!shouldPoll) return undefined;
    let canceled = false;
    const timer = setTimeout(async () => {
      if (manualDiscoveryQueued && probe?.pollUrl) {
        try {
          const runResult = await apiFetch(probe.pollUrl);
          const runStatus = runResult?.discoveryRun?.status;
          if (!canceled && DISCOVERY_TERMINAL_STATUSES.has(runStatus)) {
            setProbe(null);
          }
        } catch (_error) {
          // Keep polling the target detail; transient run-status fetch failures
          // should not strand the modal in stale UI.
        }
      }
      if (canceled) return;
      detail.reload();
      if (loadedTargetKind === 'cli') commandReviews.reload();
    }, 3000);
    return () => {
      canceled = true;
      clearTimeout(timer);
    };
  }, [serverId, loadedServer?.id, loadedServer?.provisioning_status, loadedServer?.provisioningStatus, loadedTargetKind, probe?.status, probe?.pollUrl, detail, commandReviews]);

  if (!serverId) return null;

  const server = loadedServer;
  const latestConnectionTest = server?.latest_connection_test || detail.data?.latestConnectionTests?.[0] || null;
  const targetKind = loadedTargetKind;
  const isHostedMcpTarget = isHostedMcpServerRow(server);
  const cliProvisioningStatus = server?.provisioning_status || server?.provisioningStatus || 'ready';
  const canManageSources = canUseTenantAction(auth, 'editor');
  const sourceActionDisabledReason = tenantActionDisabledReason(auth, 'editor');
  const cliDiscoveryDisabled = isCliTarget
    && !['ready', 'needs_review'].includes(cliProvisioningStatus);
  const targetLocator = isCliTarget
    ? (server?.connection_config?.cli?.command || server?.connectionConfig?.cli?.command || 'CLI command')
    : isHostedMcpTarget
      ? (server?.connection_config?.hosted?.package?.package_name || 'Hosted npm package')
      : (server?.base_url || 'No base URL');
  const targetLabel = isCliTarget ? 'CLI target' : 'MCP server';
  const toolLabel = isCliTarget ? 'commands' : 'tools';
  const probeLabel = isCliTarget ? 'Discover commands' : 'Live probe';
  const hostedMcpStatus = isHostedMcpTarget ? (server?.provisioning_status || server?.provisioningStatus) : null;
  const hostedMcpInProgress = isHostedMcpTarget && CLI_PENDING_STATUSES.has(hostedMcpStatus);
  const hostedMcpProgressMessage = hostedMcpStatus === 'discovering'
    ? 'Discovery is running. The tool catalog will refresh when it finishes.'
    : 'Provisioning is running. The tool catalog will appear when setup finishes.';
  const tools = detail.data?.toolCatalog || [];
  const monitorRows = monitors.data?.rows || [];
  const defaultAuthProfile = loadedDefaultAuthProfile;
  const editStoredSecretMatchesSelectedType = defaultAuthProfile?.auth_type === editForm.authType;
  const editHasStoredStaticSecret = Boolean(defaultAuthProfile
    && defaultAuthProfile.auth_type !== 'oauth_authorization_code'
    && editForm.authType !== 'none'
    && editForm.authType !== 'oauth_authorization_code'
    && editStoredSecretMatchesSelectedType);
  const editStoredSecretNotice = editHasStoredStaticSecret
    ? (editForm.authType === 'bearer_token' ? 'Token already stored' : 'Key already stored')
    : '';
  const canReconnectOAuth = defaultAuthProfile?.auth_type === 'oauth_authorization_code';
  const editConnectedOAuthNotice = canReconnectOAuth && editForm.authType === 'oauth_authorization_code'
    ? 'OAuth already connected'
    : '';
  // `auth_revoked` is set by:
  //   - `lib/oauth/refresh.js` when the provider returns `invalid_grant`
  //     during a refresh, OR
  //   - `lib/oauth/access-token.js` (markProfileRevoked) when the upstream
  //     MCP server itself returns 401 — relevant for long-lived,
  //     non-refreshable access tokens that we kept serving from cache until
  //     the provider finally rejected them.
  // Either way, the user needs to reconnect; surface a banner alongside the
  // always-on Reconnect button so the state is unambiguous.
  const oauthReconnectRequired = canReconnectOAuth && defaultAuthProfile?.last_test_status === 'auth_revoked';
  const pendingOAuth = (isHostedMcpTarget || isCliTarget) && server?.pending_auth?.auth_type === 'oauth_authorization_code'
    ? server.pending_auth
    : null;
  const canRetryHostedProvisioning = (isCliTarget || isHostedMcpTarget) && server?.provisioning_status === 'failed';
  const canStartPendingOAuth = pendingOAuth
    && !defaultAuthProfile
    && ['auth_required', 'ready', 'failed'].includes(server?.provisioning_status);
  const monitoredToolNames = new Set(monitorRows.map((m) => m.tool_name));
  const probeTools = probe?.ok && probe.status !== 'queued' && Array.isArray(probe.tools) ? probe.tools : null;
  // The reviews endpoint returns rows including those an operator marked
  // `disabled` (or that discovery flagged as missing). Since this drawer no
  // longer exposes enable/disable controls, surface only enabled rows so
  // disabled commands don't appear as if they're usable.
  const commandReviewRows = isCliTarget
    ? (commandReviews.data?.rows || []).filter((row) => row.review_status !== 'disabled')
    : [];
  const commandReviewsPending = isCliTarget && commandReviews.loading && !commandReviews.data && !commandReviews.error;
  const filteredCommandReviewRows = isCliTarget
    ? commandReviewRows
        .filter((row) => {
          const risk = row.risk || 'unknown';
          if (commandRiskFilter === 'read' && risk !== 'read') return false;
          if (commandRiskFilter === 'mutating' && risk !== 'mutating') return false;
          if (commandRiskFilter === 'unknown' && (risk === 'read' || risk === 'mutating')) return false;
          if (commandMonitoredFilter === 'monitored' && !monitoredToolNames.has(row.tool_name)) return false;
          if (commandMonitoredFilter === 'unmonitored' && monitoredToolNames.has(row.tool_name)) return false;
          return true;
        })
        .slice()
        .sort((a, b) => {
          if (commandSort === 'risk') {
            const rank = { mutating: 0, unknown: 1, read: 2 };
            const ra = rank[a.risk] ?? rank.unknown;
            const rb = rank[b.risk] ?? rank.unknown;
            if (ra !== rb) return ra - rb;
          }
          return String(a.tool_name).localeCompare(String(b.tool_name));
        })
    : commandReviewRows;
  const displayedTools = isCliTarget
    ? commandReviewRows.map((row) => ({ name: row.tool_name, description: row.description }))
    : (probeTools || tools.map((t) => ({ name: t.tool_name, description: t.description, inputSchema: t.input_schema })));
  const hostedProbeBlocked = isHostedMcpTarget && probe && !probe.ok
    && (
      probe.errorCode === 'hosted_provisioning_in_progress'
      || /queued or running/i.test(probe.errorMessage || '')
    );

  async function runProbe() {
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    setProbing(true);
    setProbe(null);
    try {
      const result = await apiFetch(`/api/mcp-servers/${serverId}/probe`, { method: 'POST' });
      if (result?.status === 'queued') {
        setProbe({ ...result, ok: true, tools: null });
      } else {
        setProbe(result);
      }
      try {
        await Promise.all([detail.reload(), onRefreshed?.()].filter(Boolean));
      } catch (_error) {}
    } catch (err) {
      setProbe({ ok: false, errorCode: err.code || 'request_failed', errorMessage: err.message, tools: [] });
    } finally {
      setProbing(false);
    }
  }

  async function saveMcpEditAuth(hostedMcp) {
    const desiredAuthType = editForm.authType;
    const existingProfile = defaultAuthProfile;
    if (desiredAuthType === 'none') {
      if (existingProfile?.id) {
        await apiFetch(`/api/auth-profiles/${existingProfile.id}`, { method: 'DELETE' });
      }
      return null;
    }

    const authPayload = buildEditAuthPayload({
      formState: editForm,
      hostedMcp,
      existingProfile,
    });

    if (desiredAuthType === 'oauth_authorization_code') {
      const requestedScopes = editForm.oauthScopesText
        .split(/\s+/)
        .map((value) => value.trim())
        .filter(Boolean);
      const providerSlug = editForm.oauthProviderSlug || null;
      if (providerSlug && requestedScopes.length === 0) {
        throw new Error('Scopes are required when overriding the OAuth provider.');
      }
      const currentScopes = authScopesTextForProfile(existingProfile);
      const currentProvider = authProviderSlugForProfile(existingProfile) || null;
      const currentEnvVar = authEnvVarForProfile(existingProfile);
      const nextEnvVar = hostedMcp ? editForm.hostedMcpAuthEnvVar.trim() : '';
      const oauthChanged = existingProfile?.auth_type !== 'oauth_authorization_code'
        || currentProvider !== providerSlug
        || currentScopes !== requestedScopes.join(' ')
        || (hostedMcp && currentEnvVar !== nextEnvVar);
      if (!oauthChanged) return null;
      const startResult = existingProfile?.auth_type === 'oauth_authorization_code' && currentProvider === providerSlug
        ? await startOAuthReauthorize({
          profileId: existingProfile.id,
          requestedScopes,
          authConfig: authPayload.authConfig,
        })
        : await startOAuthConnect({
          mcpServerId: serverId,
          providerSlug,
          requestedScopes,
          authConfig: authPayload.authConfig,
        });
      const popupResult = await runMcpOAuthFlow(startResult);
      if (popupResult.outcome === 'navigated') return 'navigated';
      if (popupResult.outcome === 'failed') {
        throw new Error(popupResult.message || 'OAuth authorization did not complete.');
      }
      if (popupResult.outcome === 'closed') {
        throw new Error('The authorization window was closed before consent finished.');
      }
      if (popupResult.outcome === 'pending') {
        toast.show({
          tone: 'warn',
          title: 'OAuth completed; setup is still in progress',
          description: 'The target will finish updating once OAuth setup completes.',
        });
      }
      return null;
    }

    if (existingProfile?.id) {
      await apiFetch(`/api/auth-profiles/${existingProfile.id}`, {
        method: 'PATCH',
        body: JSON.stringify(authPayload),
      });
    } else {
      await apiFetch(`/api/mcp-servers/${serverId}/auth-profiles`, {
        method: 'POST',
        body: JSON.stringify(authPayload),
      });
    }
    return null;
  }

  async function saveEdit() {
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    setEditSaving(true);
    setEditError('');
    try {
      const body = { name: (isCliTarget ? editName : editForm.serverName).trim() };
      const snapshot = editHostedSnapshot;
      const isHostedNpm = isCliTarget && snapshot != null;
      let nextSnapshot = snapshot;
      let nextHostedMcp = false;
      if (isCliTarget) {
        if (isHostedNpm) {
          // Hosted CLI npm package. Keep the existing conservative behavior:
          // only re-send provisioning inputs when package source or domains
          // actually changed.
          const nextName = editPackageName.trim();
          const nextVersion = editPackageVersion.trim();
          const nextBin = editPackageBin.trim();
          const allowedDomains = editAllowedDomains
            .split(',')
            .map((value) => value.trim())
            .filter(Boolean);
          const sourceChanged = nextName !== snapshot.package_name
            || nextVersion !== snapshot.version
            || nextBin !== snapshot.bin;
          const previousDomains = Array.isArray(snapshot.allowed_domains) ? snapshot.allowed_domains : [];
          const domainsChanged = allowedDomains.length !== previousDomains.length
            || allowedDomains.some((domain, index) => domain !== previousDomains[index]);
          if (sourceChanged) {
            if (!nextName || !nextVersion || !nextBin) {
              throw new Error('NPM package, version, and command/bin name are all required.');
            }
            body.connectionConfig = {
              cli: {
                command: nextBin,
                package: {
                  type: 'npm',
                  packageName: nextName,
                  version: nextVersion,
                  bin: nextBin,
                },
                version_args: ['--version'],
                discovery: { root_help_args: ['--help'], subcommand_help_arg: '--help' },
              },
            };
          }
          if (sourceChanged || domainsChanged) {
            body.networkPolicy = { allowedDomains };
          }
          nextSnapshot = {
            package_name: nextName,
            version: nextVersion,
            bin: nextBin,
            allowed_domains: allowedDomains.slice(),
          };
        }
      } else if (editForm.mcpSourceMode === HOSTED_MCP_SOURCE_NPM) {
        nextHostedMcp = true;
        const nextName = editForm.cliPackageName.trim();
        const nextVersion = editForm.cliPackageVersion.trim();
        const nextBin = editForm.cliBinName.trim();
        const allowedDomains = editForm.cliAllowedDomainList.slice();
        if (!nextName || !nextVersion || !nextBin) {
          throw new Error('NPM package, version, and command/bin name are all required.');
        }
        if (!isExactSemver(nextVersion)) {
          throw new Error('Enter an exact npm package version such as 1.2.3');
        }
        if (allowedDomains.length === 0) {
          throw new Error('At least one allowed domain is required');
        }
        const hostedEnv = editForm.authType === 'none' ? {} : {
          [editForm.hostedMcpAuthEnvVar.trim()]: editForm.authType === 'oauth_authorization_code'
            ? '${OAUTH_RUNTIME_TOKEN}'
            : '${MCP_AUTH_SECRET}',
        };
        body.transportType = 'stdio_hosted';
        body.connectionConfig = {
          hosted: {
            source_type: 'npm',
            package: {
              name: nextName,
              version: nextVersion,
              bin: nextBin,
            },
          },
          env: hostedEnv,
        };
        body.networkPolicy = { allowedDomains };
      } else {
        const extraHeaders = parseJsonObjectField(editForm.extraHeadersJson, 'extra headers');
        body.transportType = 'streamable_http';
        body.baseUrl = editForm.serverUrl.trim() || null;
        body.connectionConfig = extraHeaders ? { headers: extraHeaders } : {};
      }
      if (!isCliTarget) {
        buildEditAuthPayload({
          formState: editForm,
          hostedMcp: nextHostedMcp,
          existingProfile: defaultAuthProfile,
        });
        if (editForm.authType === 'oauth_authorization_code'
          && editForm.oauthProviderSlug
          && !editForm.oauthScopesText.trim()) {
          throw new Error('Scopes are required when overriding the OAuth provider.');
        }
      }
      await apiFetch(`/api/mcp-servers/${serverId}`, {
        method: 'PATCH',
        body: JSON.stringify(body),
      });
      if (!isCliTarget) {
        const authResult = await saveMcpEditAuth(nextHostedMcp);
        if (authResult === 'navigated') return;
      }
      // Refresh the snapshot so a follow-up edit in the same session doesn't
      // compare against pre-save values. The `useEffectMcp` that rehydrates
      // from `detail.data` only fires on `mcpServer.id` changes, which a
      // PATCH doesn't trigger.
      if (isHostedNpm) setEditHostedSnapshot(nextSnapshot);
      setEditReplacingSecret(false);
      setEditing(false);
      detail.reload();
      onEdited?.();
    } catch (err) {
      setEditError(err.message);
    } finally {
      setEditSaving(false);
    }
  }

  async function deleteServer() {
    if (!server) return;
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    if (!window.confirm(`Delete ${targetLabel} "${server.name}"? This will remove it and its monitors.`)) return;
    setDeleting(true);
    try {
      await apiFetch(`/api/mcp-servers/${serverId}`, { method: 'DELETE' });
      onDeleted?.(server.name);
    } catch (err) {
      setEditError(err.message);
    } finally {
      setDeleting(false);
    }
  }

  async function retryHostedProvisioning() {
    if (!server) return;
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    setRetryingProvisioning(true);
    setEditError('');
    try {
      await apiFetch(`/api/mcp-servers/${serverId}`, {
        method: 'PATCH',
        body: JSON.stringify({ retryProvisioning: true }),
      });
      detail.reload();
      onEdited?.();
    } catch (err) {
      setEditError(err.message);
    } finally {
      setRetryingProvisioning(false);
    }
  }

  async function reconnectOAuthProfile() {
    if (!defaultAuthProfile?.id) return;
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    setReconnectingOAuth(true);
    setEditError('');
    try {
      const result = await startOAuthReauthorize({
        profileId: defaultAuthProfile.id,
        requestedScopes: defaultAuthProfile.auth_config?.requested_scopes || [],
      });
      const popupResult = await runMcpOAuthFlow(result);
      if (popupResult.outcome === 'navigated') return;
      detail.reload();
      onEdited?.();
      if (popupResult.outcome === 'failed') {
        setEditError(popupResult.message || 'OAuth authorization did not complete.');
      } else if (popupResult.outcome === 'closed') {
        setEditError('The authorization window was closed before consent finished.');
      } else if (popupResult.outcome === 'pending') {
        toast.show({
          tone: 'warn',
          title: 'OAuth completed; provisioning is still in progress',
          description: 'Tools will appear once provisioning finishes.',
        });
      }
    } catch (err) {
      setEditError(err.message);
    } finally {
      setReconnectingOAuth(false);
    }
  }

  async function connectPendingOAuthProfile() {
    if (!pendingOAuth) return;
    if (!requirePaidAction(auth, navigate, { requiredRole: 'editor' })) return;
    setReconnectingOAuth(true);
    setEditError('');
    try {
      const result = await startOAuthConnect({
        mcpServerId: serverId,
        providerSlug: pendingOAuth.provider_slug || pendingOAuth.auth_config?.provider_slug || 'linear',
        requestedScopes: pendingOAuth.auth_config?.requested_scopes || [],
        authConfig: pendingOAuth.auth_config || {},
      });
      const popupResult = await runMcpOAuthFlow(result);
      if (popupResult.outcome === 'navigated') return;
      detail.reload();
      onEdited?.();
      if (popupResult.outcome === 'failed') {
        setEditError(popupResult.message || 'OAuth authorization did not complete.');
      } else if (popupResult.outcome === 'closed') {
        setEditError('The authorization window was closed before consent finished.');
      } else if (popupResult.outcome === 'pending') {
        toast.show({
          tone: 'warn',
          title: 'OAuth completed; provisioning is still in progress',
          description: 'Tools will appear once provisioning finishes.',
        });
      } else {
        toast.show({
          tone: 'success',
          title: 'OAuth completed; discovery is starting',
          description: 'Tools will appear once discovery finishes.',
        });
      }
    } catch (err) {
      setEditError(err.message);
    } finally {
      setReconnectingOAuth(false);
    }
  }

  const provisioningStatus = server?.provisioning_status || server?.last_connection_status;
  const statusLabelText = isCliTarget || isHostedMcpTarget
    ? cliStatusLabel(provisioningStatus)
    : (server?.last_connection_status || 'Unknown');
  const statusTone = isCliTarget || isHostedMcpTarget
    ? cliStatusTone(provisioningStatus)
    : (server?.last_connection_status === 'success' ? 'ok' : server?.last_connection_status === 'failed' ? 'bad' : 'neutral');
  const statusDotClass = statusTone === 'ok' ? 'badge-success'
    : statusTone === 'bad' ? 'badge-fail'
    : statusTone === 'warn' ? 'badge-pending'
    : 'badge-neutral';
  const sourceTypeLabel = isCliTarget ? 'CLI' : isHostedMcpTarget ? 'Hosted MCP' : 'Remote MCP';
  const lastTestedRel = server?.latest_connection_test?.created_at
    ? formatRelative(server.latest_connection_test.created_at)
    : null;
  const headerIconName = isCliTarget ? 'terminal' : 'mcp';
  const probeIconName = isCliTarget ? 'search' : 'zap';
  const monitorsLabel = isCliTarget ? 'Command monitors' : 'Tool monitors';
  const hasDiscoveredCatalog = displayedTools.length > 0;
  const probeButtonLabel = isCliTarget
    ? (hasDiscoveredCatalog ? 'Re-discover commands' : 'Discover commands')
    : 'Live probe';
  const probeButtonVariant = isCliTarget && hasDiscoveredCatalog ? 'secondary' : 'primary';
  const probeButtonTooltip = cliDiscoveryDisabled
    ? 'Wait for CLI package setup to finish before discovering commands.'
    : isCliTarget
      ? (hasDiscoveredCatalog
          ? `Re-run discovery against this CLI. Last refreshed ${lastTestedRel || 'recently'}.`
          : 'Run discovery to populate the command catalog.')
      : 'Probe the MCP server for its current tool list.';
  const editButtonTooltip = editing
    ? 'Discard pending edits.'
    : 'Edit the target name, command, or connection settings.';
  const monitorsButtonTooltip = isCliTarget
    ? 'Open command monitors for this CLI.'
    : 'Open tool monitors for this server.';
  const displayedToolCount = commandReviewsPending ? null : displayedTools.length;

  return (
    <div className="slideover-backdrop" onClick={onClose} role="presentation">
      <aside className={`slideover mcp-detail-panel mcp-detail ${isCliTarget ? 'mcp-detail-cli' : ''}`} role="dialog" aria-modal="true" aria-label="Target details" onClick={(e) => e.stopPropagation()}>
        <header className="mcp-detail-header">
          <div className={`mcp-detail-headicon ${isCliTarget ? 'mcp-detail-headicon-cli' : ''}`}>
            <Icon name={headerIconName} size={20} />
          </div>
          <div className="mcp-detail-headtext">
            <div className="mcp-detail-headtop">
              <h2 className="mcp-detail-title">
                {server?.name || (detail.loading ? 'Loading…' : 'Target')}
              </h2>
              <span className={`badge ${statusDotClass} mcp-detail-status-pill`} aria-label={`Status ${statusLabelText}`}>
                <span className="dot"></span>{statusLabelText}
              </span>
            </div>
            <div className="mcp-detail-headsub">
              <span className="mcp-detail-locator mono">{targetLocator}</span>
            </div>
          </div>
          <button className="slideover-close" type="button" onClick={onClose} aria-label="Close">
            <Icon name="x" size={16} />
          </button>
        </header>
        <div className="slideover-body mcp-detail-body">
          {detail.loading && !detail.data ? (
            <div className="text-sm muted">Loading target details…</div>
          ) : detail.error ? (
            <div className="auth-message error">{detail.error.message}</div>
          ) : (
            <>
              <div className="mcp-detail-meta-strip" aria-label="Target metadata">
                <div className="mcp-detail-meta-item">
                  <Icon name="tool" size={12} />
                  {displayedToolCount == null ? (
                    <span className="mcp-detail-meta-pending" aria-label="Command catalog pending">
                      <LoadingSpinner size="sm" decorative />
                    </span>
                  ) : (
                    <strong>{displayedToolCount}</strong>
                  )}
                  <span>{toolLabel}</span>
                </div>
                <span className="mcp-detail-meta-sep" aria-hidden="true">·</span>
                <div className="mcp-detail-meta-item">
                  <Icon name="bell" size={12} />
                  <strong>{monitorRows.length}</strong>
                  <span>monitor{monitorRows.length === 1 ? '' : 's'}</span>
                </div>
                <span className="mcp-detail-meta-sep" aria-hidden="true">·</span>
                <div className="mcp-detail-meta-item">
                  <Icon name="clock" size={12} />
                  <span>{lastTestedRel ? `Tested ${lastTestedRel}` : 'Never tested'}</span>
                </div>
                <span className="mcp-detail-meta-sep" aria-hidden="true">·</span>
                <div className="mcp-detail-meta-item">
                  <Icon name="layers" size={12} />
                  <span>{sourceTypeLabel}</span>
                </div>
                <span className="mcp-detail-meta-sep" aria-hidden="true">·</span>
                <div className="mcp-detail-meta-item">
                  <Icon name="key" size={12} />
                  <span>{defaultAuthProfile ? formatAuthType(defaultAuthProfile) : 'No auth'}</span>
                </div>
              </div>

              {oauthReconnectRequired && (
                <div className="mcp-oauth-revoked-banner" role="alert">
                  <Icon name="alert" size={14} />
                  <div>
                    <strong>Reconnect required.</strong>{' '}
                    The OAuth grant for this server was rejected by the provider. Click <em>Reconnect</em> to refresh credentials. Any tool monitors that were paused after the rejection will resume automatically once the reconnect succeeds.
                  </div>
                </div>
              )}

              <div className="mcp-detail-actionbar">
                <Button
                  size="sm"
                  variant={probeButtonVariant}
                  onClick={runProbe}
                  loading={probing}
                  loadingLabel={isCliTarget ? 'Queueing...' : 'Probing...'}
                  disabled={!canManageSources || cliDiscoveryDisabled || hostedMcpInProgress}
                  title={canManageSources ? probeButtonTooltip : sourceActionDisabledReason}>
                  <Icon name={probeIconName} size={12} />{probeButtonLabel}
                </Button>
                {isCliTarget && (
                  <Button
                    size="sm"
                    variant="primary"
                    onClick={onCreateWorkflow}
                    disabled={!canManageSources}
                    title={canManageSources ? 'Create a workflow for this CLI.' : sourceActionDisabledReason}>
                    <Icon name="plus" size={12} />Create workflow
                  </Button>
                )}
                {!isCliTarget && (
                  <Button
                    size="sm"
                    onClick={onCreateMonitor || onViewMonitors}
                    disabled={!canManageSources && Boolean(onCreateMonitor)}
                    title={canManageSources || !onCreateMonitor ? monitorsButtonTooltip : sourceActionDisabledReason}>
                    <Icon name="zap" size={12} />{monitorsLabel}
                  </Button>
                )}
                {canReconnectOAuth && (
                  <Button
                    size="sm"
                    onClick={reconnectOAuthProfile}
                    disabled={!canManageSources}
                    title={canManageSources ? undefined : sourceActionDisabledReason}
                    loading={reconnectingOAuth}
                    loadingLabel="Starting...">
                    <Icon name="refresh" size={12} />Reconnect OAuth
                  </Button>
                )}
                {canStartPendingOAuth && (
                  <Button
                    size="sm"
                    variant="primary"
                    onClick={connectPendingOAuthProfile}
                    disabled={!canManageSources}
                    title={canManageSources ? undefined : sourceActionDisabledReason}
                    loading={reconnectingOAuth}
                    loadingLabel="Starting...">
                    <Icon name="refresh" size={12} />Connect OAuth
                  </Button>
                )}
                {canRetryHostedProvisioning && (
                  <Button
                    size="sm"
                    onClick={retryHostedProvisioning}
                    disabled={!canManageSources}
                    title={canManageSources ? undefined : sourceActionDisabledReason}
                    loading={retryingProvisioning}
                    loadingLabel="Retrying...">
                    <Icon name="refresh" size={12} />Retry provisioning
                  </Button>
                )}
                <Button
                  size="sm"
                  onClick={() => {
                    setEditing((v) => !v);
                    setEditReplacingSecret(false);
                    editForm.setApiKey('');
                  }}
                  disabled={!canManageSources}
                  title={canManageSources ? editButtonTooltip : sourceActionDisabledReason}>
                  <Icon name="edit" size={12} />{editing ? 'Cancel edit' : 'Edit details'}
                </Button>
              </div>

              {editError && !editing && (
                <div className="auth-message error" style={{ marginTop: 10 }}>
                  <Icon name="alert" size={12} /> {editError}
                </div>
              )}

              {editing && (
                <div className="card" style={{ padding: 14, marginTop: 12 }}>
                  <div className="field-label">Edit target</div>
                  <div className="col" style={{ gap: 10, marginTop: 8 }}>
                    {!isCliTarget ? (
                      <>
                        <McpConnectFormBody
                          formState={editForm}
                          saving={editSaving}
                          storedSecretNotice={editStoredSecretNotice}
                          replacingStoredSecret={editReplacingSecret}
                          onStartSecretReplacement={() => setEditReplacingSecret(true)}
                          connectedOAuthNotice={editConnectedOAuthNotice}
                        />
                        <div className="field-help">
                          Leave secret value fields blank to keep the current stored secret. Saving OAuth changes may open the provider sign-in flow.
                        </div>
                      </>
                    ) : (
                      <>
                        <label className="field">
                          <span className="field-label">Name</span>
                          <input className="input" value={editName} onChange={(e) => setEditName(e.target.value)} />
                        </label>
                        {editHostedSnapshot ? (
                          <>
                            <div className="row" style={{ gap: 8 }}>
                              <label className="field" style={{ flex: 2 }}>
                                <span className="field-label">NPM package</span>
                                <input
                                  className="input mono"
                                  value={editPackageName}
                                  placeholder="@acme/mcp-server"
                                  onChange={(e) => setEditPackageName(e.target.value)} />
                              </label>
                              <label className="field" style={{ flex: 1 }}>
                                <span className="field-label">Exact version</span>
                                <input
                                  className="input mono"
                                  value={editPackageVersion}
                                  placeholder="0.4.3"
                                  onChange={(e) => setEditPackageVersion(e.target.value)} />
                              </label>
                            </div>
                            <label className="field">
                              <span className="field-label">Command/bin name</span>
                              <input
                                className="input mono"
                                value={editPackageBin}
                                placeholder="acme"
                                onChange={(e) => setEditPackageBin(e.target.value)} />
                              <span className="field-help">The executable inside the package — usually the package name without the scope.</span>
                            </label>
                            <label className="field">
                              <span className="field-label">Allowed domains</span>
                              <input
                                className="input mono"
                                value={editAllowedDomains}
                                placeholder="api.example.com, app.example.com"
                                onChange={(e) => setEditAllowedDomains(e.target.value)} />
                              <span className="field-help">Comma-separated runtime hosts the package may call from the sandbox.</span>
                            </label>
                            <div className="field-help">Changing the package, version, or bin will re-provision the sandbox.</div>
                          </>
                        ) : null}
                      </>
                    )}
                    {editError && <div className="auth-message error">{editError}</div>}
                    <div className="row" style={{ gap: 8 }}>
                      <Button
                        variant="primary"
                        onClick={saveEdit}
                        loading={editSaving}
                        loadingLabel="Saving..."
                        disabled={!canManageSources || !(isCliTarget ? editName.trim() : editForm.serverName.trim())}
                        title={canManageSources ? undefined : sourceActionDisabledReason}>
                        <Icon name="check" size={12} />Save changes
                      </Button>
                      <Button
                        onClick={() => {
                          setEditing(false);
                          setEditReplacingSecret(false);
                          editForm.setApiKey('');
                        }}
                        disabled={editSaving}>
                        Cancel
                      </Button>
                    </div>
                  </div>
                </div>
              )}

              <div className="mcp-detail-section">
                <div className="mcp-detail-tabbar">
                  <div className="tabs mcp-detail-tabs" role="tablist" aria-label="Target detail sections">
                    <DetailTab id="tools" current={tab} onSelect={setTab}>
                      <Icon name="tool" size={12} />{isCliTarget ? 'Commands' : 'Tools'}
                      <span className="text-xs muted" style={{ marginLeft: 6 }}>{displayedToolCount == null ? 'syncing' : displayedToolCount}</span>
                    </DetailTab>
                    <DetailTab id="monitors" current={tab} onSelect={setTab}>
                      <Icon name="bell" size={12} />Monitors
                      <span className="text-xs muted" style={{ marginLeft: 6 }}>{monitorRows.length}</span>
                    </DetailTab>
                    <DetailTab id="history" current={tab} onSelect={setTab}>
                      <Icon name="clock" size={12} />Connection history
                    </DetailTab>
                  </div>
                  {isCliTarget && tab === 'tools' && commandReviewRows.length > 0 && (
                    <div className="mcp-detail-toolfilters" aria-label="Command filters">
                      <div className="mcp-detail-segmented" role="group" aria-label="Risk filter">
                        <button type="button" className={commandRiskFilter === 'all' ? 'active' : ''} onClick={() => setCommandRiskFilter('all')} title="Show all commands">All</button>
                        <button type="button" className={commandRiskFilter === 'read' ? 'active' : ''} onClick={() => setCommandRiskFilter('read')} title="Show only read-only commands">Read</button>
                        <button type="button" className={commandRiskFilter === 'mutating' ? 'active' : ''} onClick={() => setCommandRiskFilter('mutating')} title="Show only side-effecting commands">Mutating</button>
                        <button type="button" className={commandRiskFilter === 'unknown' ? 'active' : ''} onClick={() => setCommandRiskFilter('unknown')} title="Show only commands with unknown risk">Unknown</button>
                      </div>
                      <div className="mcp-detail-segmented" role="group" aria-label="Monitored filter">
                        <button type="button" className={commandMonitoredFilter === 'all' ? 'active' : ''} onClick={() => setCommandMonitoredFilter('all')} title="Show all">All</button>
                        <button type="button" className={commandMonitoredFilter === 'monitored' ? 'active' : ''} onClick={() => setCommandMonitoredFilter('monitored')} title="Show only monitored commands">Monitored</button>
                      </div>
                      <label className="mcp-detail-sort" title="Sort commands">
                        <span className="mcp-detail-sort-label">Sort</span>
                        <Select
                          className="mcp-detail-sort-select"
                          ariaLabel="Sort commands"
                          value={commandSort}
                          onChange={setCommandSort}
                          options={[
                            { value: 'name', label: 'Name' },
                            { value: 'risk', label: 'Risk' },
                          ]} />
                      </label>
                    </div>
                  )}
                </div>

                  {tab === 'tools' && (
                    <div className="mcp-detail-tools">
                      {hostedMcpInProgress && (
                        <div className="auth-message">
                          <Icon name="clock" size={12} /> {hostedMcpProgressMessage}
                        </div>
                      )}
                      {isHostedMcpTarget && hostedMcpStatus === 'failed' && server?.provisioning_error && (
                        <div className="auth-message error">
                          <Icon name="alert" size={12} /> Hosted setup failed: {targetUserErrorMessage(
                            latestConnectionTest?.error_code || server.provisioning_error_code || server.provisioning_error,
                            targetKind,
                            'provisioning',
                            latestConnectionTest?.error_message || server.provisioning_error
                          )}
                        </div>
                      )}
                      {probe?.status === 'queued' && (
                        <div className="auth-message">
                          <Icon name="zap" size={12} /> Discovery queued. The catalog will refresh when the worker finishes.
                        </div>
                      )}
                      {hostedProbeBlocked && !displayedTools.length && (
                        <div className="auth-message">
                          <Icon name="clock" size={12} /> Hosted discovery is already queued or running. The catalog will refresh automatically.
                        </div>
                      )}
                      {probe && !probe.ok && !hostedProbeBlocked && (
                        <div className="auth-message error">
                          <Icon name="alert" size={12} /> Probe failed: {targetUserErrorMessage(probe.errorCode, targetKind, 'runtime', probe.errorMessage)}
                        </div>
                      )}
                      {commandReviewsPending ? (
                        <div className="mcp-command-review-loading" role="status" aria-live="polite">
                          <LoadingSpinner size="sm" decorative />
                          <span>Fetching command catalog</span>
                          <div className="mcp-command-review-loading-lines" aria-hidden="true">
                            <span></span>
                            <span></span>
                            <span></span>
                          </div>
                        </div>
                      ) : isCliTarget && commandReviews.error ? (
                        <div className="auth-message error">
                          <Icon name="alert" size={12} /> {commandReviews.error.message}
                        </div>
                      ) : hostedMcpInProgress && !displayedTools.length ? (
                        <div className="text-sm muted">No {toolLabel} cataloged yet.</div>
                      ) : !displayedTools.length ? (
                        <div className="text-sm muted">No {toolLabel} cataloged yet. Click <strong>{probeLabel}</strong> to refresh the catalog.</div>
                      ) : isCliTarget && filteredCommandReviewRows.length === 0 ? (
                        <div className="text-sm muted">No commands match the current filters. <button type="button" className="link-inline" onClick={() => { setCommandRiskFilter('all'); setCommandMonitoredFilter('all'); }}>Reset filters</button>.</div>
                      ) : isCliTarget ? (
                        <ul className="mcp-tool-list">
                          {filteredCommandReviewRows.map((row) => {
                            const helpDetails = row.source_metadata?.source?.help;
                            const helpFallback = row.source_metadata?.source?.helpExcerpt || row.source_metadata?.source?.rawHelpExcerpt;
                            const risk = row.risk || 'unknown';
                            return (
                              <li key={row.tool_name} className="mcp-tool-item mcp-command-review-item">
                                <div className="mcp-tool-row">
                                  <Icon name="tool" size={12} className="mcp-tool-icon" />
                                  <div className="mono mcp-tool-name">{row.tool_name}</div>
                                  <div className="mcp-command-review-badges">
                                    {monitoredToolNames.has(row.tool_name) && (
                                      <span className="badge badge-success text-xs"><span className="dot"></span>monitored</span>
                                    )}
                                    <span className={`badge ${cliRiskBadgeClass(risk)} text-xs`}>{formatCliRiskLabel(risk)}</span>
                                  </div>
                                </div>
                                {row.description && <div className="text-xs muted mcp-tool-desc">{row.description}</div>}
                                {renderCliHelpDetails(helpDetails, helpFallback, { suppressSummary: Boolean(row.description) })}
                              </li>
                            );
                          })}
                        </ul>
                      ) : (
                        <ul className="mcp-tool-list">
                          {displayedTools.map((tool) => (
                            <li key={tool.name} className="mcp-tool-item">
                              <div className="mcp-tool-row">
                                <Icon name="tool" size={12} className="mcp-tool-icon" />
                                <div className="mono mcp-tool-name">{tool.name}</div>
                                {monitoredToolNames.has(tool.name) && <span className="badge badge-success text-xs"><span className="dot"></span>monitored</span>}
                              </div>
                              {tool.description && <div className="text-xs muted mcp-tool-desc">{tool.description}</div>}
                            </li>
                          ))}
                        </ul>
                      )}
                    </div>
                  )}

                  {tab === 'monitors' && (
                    <div className="mcp-detail-monitors">
                      {monitorRows.length === 0 ? (
                        <div className="text-sm muted">No monitors for this target yet.</div>
                      ) : (
                        <ul className="mcp-tool-list">
                          {monitorRows.map((m) => (
                            <li key={m.id} className="mcp-tool-item">
                              <div className="mcp-tool-row">
                                <span className={`badge ${m.last_status === 'pass' ? 'badge-success' : (m.last_status === 'fail' || m.last_status === 'error') ? 'badge-fail' : 'badge-pending'} text-xs`}>
                                  <span className="dot"></span>{m.last_status || 'pending'}
                                </span>
                                <div className="mono mcp-tool-name">{m.tool_name}</div>
                                <span className="text-xs muted" style={{ marginLeft: 'auto' }}>every {formatMonitorInterval(m.interval_seconds)}</span>
                              </div>
                            </li>
                          ))}
                        </ul>
                      )}
                      <Button size="sm" onClick={onViewMonitors} style={{ marginTop: 10 }}>
                        <Icon name="chevronRight" size={11} />Manage monitors
                      </Button>
                    </div>
                  )}

                {tab === 'history' && (
                  <div className="mcp-detail-history">
                    {(detail.data?.latestConnectionTests || []).length === 0 ? (
                      <div className="text-sm muted">No discovery or connection tests yet.</div>
                    ) : (
                      <table className="table" style={{ fontSize: 12 }}>
                        <thead><tr><th>Status</th><th>When</th><th>Detail</th></tr></thead>
                        <tbody>
                          {detail.data.latestConnectionTests.map((row) => (
                            <tr key={row.id} style={{ cursor: 'default' }}>
                              <td>
                                <span className={`badge ${isConnectionSuccessStatus(row.status) ? 'badge-success' : 'badge-fail'} text-xs`}>
                                  <span className="dot"></span>{row.status}
                                </span>
                              </td>
                              <td className="muted">{formatRelative(row.created_at)}</td>
                              <td className="text-xs muted" style={{ whiteSpace: 'normal' }}>
                                {isConnectionSuccessStatus(row.status)
                                  ? (row.detail || 'Completed')
                                  : targetUserErrorMessage(row.error_code, targetKind, 'runtime', row.error_message)}
                              </td>
                            </tr>
                          ))}
                        </tbody>
                      </table>
                    )}
                  </div>
                )}
              </div>

              <div className="mcp-detail-footer">
                <button
                  type="button"
                  className="mcp-detail-delete"
                  onClick={deleteServer}
                  disabled={!canManageSources || deleting}
                  title={!canManageSources ? sourceActionDisabledReason : undefined}
                  aria-busy={deleting || undefined}>
                  <Icon name="trash" size={12} />
                  {deleting ? 'Deleting…' : `Delete this ${targetLabel.toLowerCase()}`}
                </button>
              </div>
            </>
          )}
        </div>
      </aside>
    </div>
  );
}

function formatMonitorInterval(seconds) {
  if (seconds === 60) return '1m';
  if (seconds === 300) return '5m';
  if (seconds === 900) return '15m';
  if (seconds === 3600) return '1h';
  return `${seconds}s`;
}

// ---------- Tool monitor wizard helpers ----------

function fieldKindFor(schema) {
  if (!schema || typeof schema !== 'object') return 'json';
  if (Array.isArray(schema.enum)) return 'enum';
  const type = Array.isArray(schema.type) ? schema.type.find((t) => t !== 'null') : schema.type;
  if (type === 'boolean') return 'boolean';
  if (type === 'integer') return 'integer';
  if (type === 'number') return 'number';
  if (type === 'string') {
    if (schema.format === 'date') return 'date';
    if (schema.format === 'date-time') return 'datetime-local';
    if (schema.format === 'email') return 'email';
    if (schema.format === 'uri' || schema.format === 'url') return 'url';
    return 'string';
  }
  if (type === 'array') return 'array';
  if (type === 'object') return 'object';
  return 'json';
}

function defaultValueForKind(kind, schema) {
  if (schema && Object.prototype.hasOwnProperty.call(schema, 'default')) {
    const def = schema.default;
    if (kind === 'array' || kind === 'object' || kind === 'json') {
      // For structured editors, keep native value; JSON-textarea fallback will stringify on render.
      try { return JSON.parse(JSON.stringify(def)); } catch { /* fall through */ }
    }
    if (kind === 'boolean') return Boolean(def);
    if (def == null) return '';
    if (typeof def === 'object') {
      // Schema declares a primitive kind but default is structured — stringify so the input doesn't render "[object Object]".
      try { return JSON.stringify(def); } catch { return ''; }
    }
    return String(def);
  }
  if (kind === 'boolean') return false;
  if (kind === 'array') return [];
  if (kind === 'object') return {};
  return '';
}

function safeInputValue(value) {
  if (value == null) return '';
  if (typeof value === 'string' || typeof value === 'number') return value;
  if (typeof value === 'boolean') return String(value);
  try { return JSON.stringify(value); } catch { return ''; }
}

// Empty value of right type for "Add item" / object property creation.
function emptyValueForSchema(schema) {
  const kind = fieldKindFor(schema);
  if (kind === 'boolean') return false;
  if (kind === 'integer' || kind === 'number') return '';
  if (kind === 'array') return [];
  if (kind === 'object') return {};
  if (kind === 'json') return '';
  return '';
}

function buildInitialToolState(tools) {
  const out = {};
  for (const tool of tools) {
    const props = (tool.inputSchema && tool.inputSchema.properties) || {};
    const values = {};
    for (const key of Object.keys(props)) {
      const kind = fieldKindFor(props[key]);
      values[key] = defaultValueForKind(kind, props[key]);
    }
    out[tool.name] = {
      checked: true,
      interval: 300,
      values,
      extras: [], // [{ key, value }]
      errors: {}, // { [fieldKey]: message }
      saveError: null,
    };
  }
  return out;
}

function coerceValueForSubmit(rawValue, schema, kind) {
  if (kind === 'boolean') return Boolean(rawValue);
  if (kind === 'integer') {
    if (rawValue === '' || rawValue == null) return undefined;
    return parseInt(rawValue, 10);
  }
  if (kind === 'number') {
    if (rawValue === '' || rawValue == null) return undefined;
    return Number(rawValue);
  }
  if (kind === 'array') {
    if (!Array.isArray(rawValue)) return undefined;
    const itemSchema = schema?.items && typeof schema.items === 'object' ? schema.items : null;
    const out = [];
    for (const v of rawValue) {
      if (itemSchema) {
        const itemKind = fieldKindFor(itemSchema);
        const c = coerceValueForSubmit(v, itemSchema, itemKind);
        if (c !== undefined) out.push(c);
      } else {
        out.push(v);
      }
    }
    return out;
  }
  if (kind === 'object') {
    if (!rawValue || typeof rawValue !== 'object') return undefined;
    const props = schema?.properties || {};
    const out = {};
    for (const k of Object.keys(props)) {
      const sub = props[k];
      const subKind = fieldKindFor(sub);
      const c = coerceValueForSubmit(rawValue[k], sub, subKind);
      if (c !== undefined) out[k] = c;
    }
    return out;
  }
  if (kind === 'json') {
    if (rawValue === '' || rawValue == null) return undefined;
    if (typeof rawValue !== 'string') return rawValue;
    return JSON.parse(rawValue);
  }
  if (rawValue === '') return undefined;
  return rawValue;
}

function validateField(rawValue, schema, kind, isRequired) {
  if (isRequired) {
    if (kind === 'boolean') {
      // boolean required just means it must be present — checkbox always provides true/false, accept both
    } else if (kind === 'array') {
      if (!Array.isArray(rawValue) || rawValue.length === 0) return 'Required';
    } else if (kind === 'object') {
      if (!rawValue || typeof rawValue !== 'object') return 'Required';
    } else if (rawValue === '' || rawValue == null) {
      return 'Required';
    }
  }
  if (rawValue === '' || rawValue == null) {
    if (kind !== 'array' && kind !== 'object') return null;
  }
  if (kind === 'integer' || kind === 'number') {
    const num = Number(rawValue);
    if (Number.isNaN(num)) return 'Must be a number';
    if (kind === 'integer' && !Number.isInteger(num)) return 'Must be an integer';
    if (typeof schema?.minimum === 'number' && num < schema.minimum) return `Must be ≥ ${schema.minimum}`;
    if (typeof schema?.maximum === 'number' && num > schema.maximum) return `Must be ≤ ${schema.maximum}`;
  }
  if (kind === 'string' || kind === 'email' || kind === 'url' || kind === 'date' || kind === 'datetime-local') {
    if (typeof schema?.minLength === 'number' && String(rawValue).length < schema.minLength) {
      return `Must be at least ${schema.minLength} character${schema.minLength === 1 ? '' : 's'}`;
    }
    if (typeof schema?.maxLength === 'number' && String(rawValue).length > schema.maxLength) {
      return `Must be at most ${schema.maxLength} character${schema.maxLength === 1 ? '' : 's'}`;
    }
    if (schema?.pattern) {
      try {
        const re = new RegExp(schema.pattern);
        if (!re.test(String(rawValue))) return `Doesn't match expected format (${schema.pattern})`;
      } catch { /* ignore bad regex */ }
    }
    if (Array.isArray(schema?.enum) && !schema.enum.includes(rawValue)) {
      return `Must be one of: ${schema.enum.join(', ')}`;
    }
  }
  if (kind === 'enum') {
    if (Array.isArray(schema?.enum) && !schema.enum.includes(rawValue)) {
      return `Must be one of: ${schema.enum.join(', ')}`;
    }
  }
  if (kind === 'array') {
    if (rawValue != null && !Array.isArray(rawValue)) return 'Must be a list';
    const arr = Array.isArray(rawValue) ? rawValue : [];
    if (typeof schema?.minItems === 'number' && arr.length < schema.minItems) {
      return `Must have at least ${schema.minItems} item${schema.minItems === 1 ? '' : 's'}`;
    }
    if (typeof schema?.maxItems === 'number' && arr.length > schema.maxItems) {
      return `Must have at most ${schema.maxItems} item${schema.maxItems === 1 ? '' : 's'}`;
    }
    const itemSchema = schema?.items && typeof schema.items === 'object' ? schema.items : null;
    if (itemSchema) {
      const itemKind = fieldKindFor(itemSchema);
      for (let i = 0; i < arr.length; i += 1) {
        const sub = validateField(arr[i], itemSchema, itemKind, false);
        if (sub) return `Item ${i + 1}: ${sub}`;
      }
    }
  }
  if (kind === 'object') {
    if (rawValue != null && (typeof rawValue !== 'object' || Array.isArray(rawValue))) {
      return 'Must be an object';
    }
    const props = schema?.properties || {};
    const required = Array.isArray(schema?.required) ? schema.required : [];
    for (const k of Object.keys(props)) {
      const sub = props[k];
      const subKind = fieldKindFor(sub);
      const e = validateField(rawValue?.[k], sub, subKind, required.includes(k));
      if (e) return `${k}: ${e}`;
    }
  }
  if (kind === 'json') {
    if (typeof rawValue === 'string') {
      try { JSON.parse(rawValue); } catch { return 'Must be valid JSON'; }
    }
  }
  return null;
}

function validateAllSelectedTools(tools, selectedTools) {
  const next = { ...selectedTools };
  let hasErrors = false;
  for (const tool of tools) {
    const cfg = next[tool.name];
    if (!cfg?.checked) continue;
    const props = (tool.inputSchema && tool.inputSchema.properties) || {};
    const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
    const ignored = cfg.ignoredFields || {};
    const errors = {};
    for (const key of Object.keys(props)) {
      if (ignored[key]) continue;
      const schema = props[key];
      const kind = fieldKindFor(schema);
      const err = validateField(cfg.values?.[key], schema, kind, required.includes(key));
      if (err) errors[key] = err;
    }
    // extras: keys must be non-empty and unique
    const seen = new Set(Object.keys(props));
    (cfg.extras || []).forEach((row, idx) => {
      if (!row.key || !row.key.trim()) errors[`__extra_${idx}_key`] = 'Field name required';
      else if (seen.has(row.key)) errors[`__extra_${idx}_key`] = 'Duplicate field name';
      else seen.add(row.key);
    });
    next[tool.name] = { ...cfg, errors };
    if (Object.keys(errors).length > 0) hasErrors = true;
  }
  return { next, hasErrors };
}

function buildArgsFromState(tool, cfg) {
  const props = (tool.inputSchema && tool.inputSchema.properties) || {};
  const ignored = cfg.ignoredFields || {};
  const out = {};
  for (const key of Object.keys(props)) {
    if (ignored[key]) continue;
    const schema = props[key];
    const kind = fieldKindFor(schema);
    const coerced = coerceValueForSubmit(cfg.values?.[key], schema, kind);
    if (coerced !== undefined) out[key] = coerced;
  }
  for (const row of (cfg.extras || [])) {
    if (!row.key) continue;
    let v = row.value;
    if (typeof v === 'string') {
      const trimmed = v.trim();
      if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed === 'true' || trimmed === 'false' || trimmed === 'null' || /^-?\d+(\.\d+)?$/.test(trimmed)) {
        try { v = JSON.parse(trimmed); } catch { /* keep as string */ }
      }
    }
    out[row.key] = v;
  }
  return out;
}

function isToolReady(tool, cfg) {
  if (!cfg?.checked) return true;
  // A prior POST failure is retry state, not argument readiness. Keep Save
  // enabled so the user can retry without toggling or editing the monitor.
  // Fast path: trust cached errors when present. setValue() optimistically
  // clears the edited field's error, so an empty `errors` map can mask a
  // freshly-typed invalid value — fall through to full validation in that
  // case. validateAllSelectedTools is O(args + extras) per call.
  if (cfg.errors && Object.keys(cfg.errors).length > 0) return false;
  const validation = validateAllSelectedTools([tool], { [tool.name]: cfg });
  return !validation.hasErrors;
}

// ---------- Components ----------

// Full-bleed loading panel shown the moment the user kicks off probe + agent
// fill. We deliberately use the same panel during probing AND agent fill so
// there's no skeleton-then-loader flicker — it's one continuous "we're
// working on it" view that just swaps its caption when the agent starts.
function ToolWizardLoading({ phase = 'agent' }) {
  // phase: 'probe' (asking the MCP server what tools it has) | 'agent' (asking
  // the Armature agent to draft fills) | 'saving' (creating monitors and
  // running them once). Same chrome, different copy.
  const title = phase === 'probe'
    ? 'Reading tools from your MCP server'
    : phase === 'saving'
      ? 'Creating your tool monitors'
      : 'Armature agent is pre-filling your tool monitors';
  const sub = phase === 'probe'
    ? 'Asking the server which tools it advertises so we can configure monitors for them.'
    : phase === 'saving'
      ? 'Setting up the schedule and running each one once so you can see results immediately.'
      : 'Drafting input arguments with relevant examples so you only have to review them.';
  return (
    <div className="tool-wizard-loading">
      <div className="tool-wizard-loading-avatar thinking">
        <div className="tool-wizard-loading-mark">{ARMATURE_MARK_SVG}</div>
      </div>
      <div className="tool-wizard-loading-title">{title}</div>
      <div className="tool-wizard-loading-sub">{sub}</div>
      <div className="tool-wizard-loading-bar" aria-hidden="true">
        <div className="tool-wizard-loading-bar-fill" />
      </div>
    </div>
  );
}

const ARMATURE_MARK_SVG = (
  <svg viewBox="0 0 1440 1706" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
    <path d="M972.882 1203.44L171.306 1666.23L0 1421.58L860.331 924.866L972.882 1203.44Z" fill="#ffffff" />
    <path d="M1439.35 1415.31L1235.87 1705.91L740.582 480.04L901.027 82.9216L1439.35 1415.31Z" fill="#ffffff" />
    <path d="M425.517 1109.35L6.99902 1350.98L552.831 0H873.724L425.517 1109.35Z" fill="#ffffff" />
  </svg>
);

function mergeAgentFills(currentState, tools, fills) {
  const out = { ...currentState };
  for (const tool of tools) {
    const fill = fills?.[tool.name];
    const cur = out[tool.name] || { checked: true, interval: 300, values: {}, extras: [], errors: {}, saveError: null };
    // Already-monitored tools live in their own group; the agent's verdict is
    // irrelevant here because the user can't change anything about them.
    if (cur.existing) {
      out[tool.name] = { ...cur, agentClassification: null, agentReason: null, bucket: 'existing', checked: false };
      continue;
    }
    const props = tool.inputSchema?.properties || {};
    const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
    if (!fill) {
      // No agent verdict — every required field needs explicit user commit.
      const committedFields = {};
      for (const key of Object.keys(props)) {
        if (!required.includes(key)) committedFields[key] = true;
      }
      out[tool.name] = { ...cur, agentClassification: null, agentReason: null, fieldSources: cur.fieldSources || {}, committedFields, bucket: 'needs' };
      continue;
    }
    const fieldSources = { ...(cur.fieldSources || {}) };
    const values = { ...(cur.values || {}) };
    if (fill.classification !== 'skip_side_effecting' && fill.fields) {
      for (const [key, info] of Object.entries(fill.fields)) {
        fieldSources[key] = { source: info.source, reason: info.reason };
        if (info.source === 'agent' && info.value !== undefined && info.value !== null) {
          values[key] = info.value;
        } else if (info.source === 'requires_manual') {
          // Agent didn't endorse a value here — clear any schema-default so
          // the field renders empty (and so the bucket can correctly say
          // "needs your input").
          values[key] = '';
        }
      }
    }
    // Seed committedFields: agent-endorsed fields are pre-validated; any field
    // the user must touch (requires_manual, or required-by-schema with no
    // agent value) starts uncommitted; everything else is "no action needed".
    const committedFields = {};
    for (const key of Object.keys(props)) {
      const source = fieldSources[key]?.source;
      const userMustAct = source === 'requires_manual'
        || (required.includes(key) && source !== 'agent');
      if (!userMustAct) committedFields[key] = true;
    }
    const nextCfg = {
      ...cur,
      checked: fill.classification !== 'skip_side_effecting',
      values,
      fieldSources,
      committedFields,
      agentClassification: fill.classification,
      agentReason: fill.reason,
    };
    nextCfg.bucket = computeBucket(tool, nextCfg);
    out[tool.name] = nextCfg;
  }
  return out;
}

// Decide which bucket a tool belongs in.
//   - skip_side_effecting           → 'skipped'
//   - any pending field uncommitted → 'needs'
//   - any field has a validation    → 'needs'
//     error
//   - everything else               → 'ready'
// "Pending" means a field the user must explicitly review and commit (via
// the inline check icon): either flagged requires_manual by the agent, or
// required by schema with no agent value. Once a pending field is in
// committedFields, it counts as ready. Bucket only moves on commit click —
// never mid-keystroke — so rows don't jump around.
// Detect arguments shared by 2+ currently-checked, non-existing tools. A
// "shared" bucket is keyed by (argument name, JSON kind) — two tools with
// `project_id: string` and `project_id: number` are distinct buckets, so the
// shared input we offer the user matches the type each tool actually expects.
// The wizard renders one row per returned bucket in its Shared values panel.
function computeSharedArgBuckets(tools, selectedTools) {
  /** @type {Map<string, {key:string, kind:string, required:boolean, toolNames:string[], representativeSchema:any}>} */
  const buckets = new Map();
  for (const tool of tools || []) {
    const cfg = selectedTools?.[tool.name];
    if (!cfg || !cfg.checked || cfg.existing) continue;
    const props = (tool.inputSchema && tool.inputSchema.properties) || null;
    if (!props || typeof props !== 'object') continue;
    const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
    for (const [key, schema] of Object.entries(props)) {
      if (!schema || typeof schema !== 'object') continue;
      const kind = fieldKindFor(schema);
      const bucketId = `${key}::${kind}`;
      let bucket = buckets.get(bucketId);
      if (!bucket) {
        bucket = {
          key,
          kind,
          required: false,
          toolNames: [],
          representativeSchema: schema,
        };
        buckets.set(bucketId, bucket);
      }
      bucket.toolNames.push(tool.name);
      if (required.includes(key)) bucket.required = true;
      // Prefer a representative schema that carries a description so the
      // shared-row hint is informative — first non-empty one wins.
      if (!bucket.representativeSchema?.description && schema.description) {
        bucket.representativeSchema = schema;
      }
    }
  }
  const out = [];
  for (const bucket of buckets.values()) {
    if (bucket.toolNames.length < 2) continue;
    out.push(bucket);
  }
  out.sort((a, b) => {
    if (a.required !== b.required) return a.required ? -1 : 1;
    if (a.toolNames.length !== b.toolNames.length) return b.toolNames.length - a.toolNames.length;
    return a.key.localeCompare(b.key);
  });
  return out;
}

// "Dirty pending" = the user has typed a value into a required-but-uncommitted
// field but hasn't clicked Validate yet. Used as the trigger for both the
// pulse on the row's Validate button (next-likely-action affordance) and the
// row-level wiggle when the user tries to abandon the row without validating.
function hasDirtyPendingFields(tool, cfg) {
  if (!cfg?.checked || cfg?.existing) return false;
  const props = (tool.inputSchema && tool.inputSchema.properties) || {};
  const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
  const sources = cfg.fieldSources || {};
  const committed = cfg.committedFields || {};
  const ignored = cfg.ignoredFields || {};
  const values = cfg.values || {};
  for (const key of Object.keys(props)) {
    if (ignored[key]) continue;
    const userMustAct = sources[key]?.source === 'requires_manual'
      || (required.includes(key) && sources[key]?.source !== 'agent');
    if (!userMustAct) continue;
    if (committed[key]) continue;
    const v = values[key];
    const hasValue = !(v === '' || v == null || (Array.isArray(v) && v.length === 0));
    if (hasValue) return true;
  }
  return false;
}

function computeBucket(tool, cfg) {
  // "Already monitored" trumps every other state: the user can't act on these
  // and the agent's verdict doesn't matter.
  if (cfg.existing) return 'existing';
  if (cfg.agentClassification === 'skip_side_effecting' && !cfg.checked) return 'skipped';
  if (!cfg.checked) return 'skipped';
  const props = tool.inputSchema?.properties || {};
  const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
  const sources = cfg.fieldSources || {};
  const committed = cfg.committedFields || {};
  const errors = cfg.errors || {};
  for (const key of Object.keys(props)) {
    if (errors[key]) return 'needs';
    const userMustAct = sources[key]?.source === 'requires_manual'
      || (required.includes(key) && sources[key]?.source !== 'agent');
    if (userMustAct && !committed[key]) return 'needs';
  }
  return 'ready';
}

function bucketForToolState(tool, cfg) {
  if (cfg?.existing) return 'existing';
  return cfg?.bucket || (cfg?.checked ? computeBucket(tool, cfg) : 'skipped');
}

function setToolCheckedState(tool, cfg, checked, options = {}) {
  const cur = cfg || { checked: false, interval: 300, values: {}, extras: [], errors: {} };
  if (!checked) {
    const currentBucket = bucketForToolState(tool, cur);
    return {
      ...cur,
      checked: false,
      bucket: 'skipped',
      disabledFromNeeds: options.markDisabledFromNeeds === true && currentBucket === 'needs',
    };
  }
  const next = {
    ...cur,
    checked: true,
    disabledFromNeeds: false,
  };
  next.bucket = computeBucket(tool, {
    ...next,
    agentClassification: cur.agentClassification === 'skip_side_effecting' ? null : cur.agentClassification,
  });
  return next;
}

function setToolsCheckedInBucket(currentState, tools, bucket, checked) {
  const out = { ...currentState };
  for (const tool of tools) {
    const cur = out[tool.name] || { checked: false, interval: 300, values: {}, extras: [], errors: {} };
    const currentBucket = bucket === 'disabledFromNeeds' && cur.disabledFromNeeds
      ? 'disabledFromNeeds'
      : bucketForToolState(tool, cur);
    if (currentBucket !== bucket) continue;
    out[tool.name] = setToolCheckedState(tool, cur, checked, {
      markDisabledFromNeeds: !checked && bucket === 'needs',
    });
  }
  return out;
}

function AgentStrip({ status, ready, manual, skipped, error, onRetry }) {
  const isThinking = status === 'loading';
  let title = 'Armature agent';
  let sub = null;
  if (status === 'loading') {
    title = 'Armature agent is pre-filling your tool monitors';
    sub = <span>Drafting input arguments with relevant examples so you only have to review them.</span>;
  } else if (status === 'ready') {
    title = 'Armature agent has pre-filled your monitors';
    sub = (
      <span>
        Review below. <strong className="agent-strip-stat-ready">{ready} ready</strong> · <strong className="agent-strip-stat-manual">{manual} need your input</strong> · <strong className="agent-strip-stat-skipped">{skipped} skipped</strong>.
      </span>
    );
  } else if (status === 'error') {
    title = 'Armature agent could not draft fills';
    sub = (
      <span className="agent-strip-error">
        {error || 'Try again or fill the arguments manually.'}
        {onRetry && <button className="btn btn-sm btn-ghost" style={{ marginLeft: 6 }} onClick={onRetry}>Retry</button>}
      </span>
    );
  } else {
    sub = <span>Waiting for tools to inspect.</span>;
  }
  return (
    <div className={`agent-strip agent-strip-${status || 'idle'}`}>
      <div className={`agent-strip-avatar ${isThinking ? 'thinking' : ''}`}>{ARMATURE_MARK_SVG}</div>
      <div className="agent-strip-text">
        <div className="agent-strip-title">{title}</div>
        <div className="agent-strip-sub">{sub}</div>
      </div>
      {status === 'loading' && (
        <div className="agent-strip-progress" aria-hidden="true"><div className="agent-strip-progress-fill indeterminate" /></div>
      )}
    </div>
  );
}

// Sentence-style frequency control. Reads as "🕒 Run every [select] for all
// monitors". The user picks a value; we only show the Apply-to-all button
// once they actually pick something so the row doesn't sit cluttered with a
// CTA they haven't earned yet.
// Above the per-tool list when 2+ checked tools share an argument by (name +
// kind). Lets the user type the value once and apply it to every tool that
// needs it, replacing the repetitive per-tool typing today's wizard required.
// Per-tool override is still allowed: editing the same field inside a tool's
// own body flips it to "overridden", and the panel's count shifts accordingly.
function SharedValuesPanel({
  buckets,
  selectedTools,
  saving,
  onApply,
  onClear,
  onApplyToAll,
}) {
  const [drafts, setDrafts] = useStateMcp(/** @type {Record<string, string>} */ ({}));
  const [expanded, setExpanded] = useStateMcp(true);
  const [openTools, setOpenTools] = useStateMcp(/** @type {Record<string, boolean>} */ ({}));

  if (!buckets || buckets.length === 0) return null;

  function setDraft(key, value) {
    setDrafts((s) => ({ ...s, [key]: value }));
  }
  function toggleToolList(key) {
    setOpenTools((s) => ({ ...s, [key]: !s[key] }));
  }

  // Count tools where the value has been applied AND not subsequently
  // overridden — that's the "this row is currently in sync" count we show in
  // the applied label. Overrides are tracked separately so we can offer the
  // re-apply-everywhere escape hatch when any exist.
  function countsFor(bucket) {
    let applied = 0;
    let overridden = 0;
    for (const name of bucket.toolNames) {
      const cfg = selectedTools[name];
      if (!cfg) continue;
      if (cfg.sharedOverriddenFields?.[bucket.key]) overridden += 1;
      else if (cfg.sharedAppliedFields?.[bucket.key]) applied += 1;
    }
    return { applied, overridden, total: bucket.toolNames.length };
  }

  // The value any currently-applied (non-overridden) tool holds for the
  // bucket's key. We use it as the default payload for "Replace overrides
  // too" so the user doesn't have to retype the value they already applied.
  function currentlyAppliedValue(bucket) {
    for (const name of bucket.toolNames) {
      const cfg = selectedTools[name];
      if (!cfg) continue;
      if (cfg.sharedOverriddenFields?.[bucket.key]) continue;
      if (!cfg.sharedAppliedFields?.[bucket.key]) continue;
      return cfg.values?.[bucket.key];
    }
    return undefined;
  }

  return (
    <div className="tool-wizard-shared" role="group" aria-label="Shared values across selected tools">
      <div className="tool-wizard-shared-head">
        <button
          type="button"
          className="btn btn-sm btn-ghost tool-wizard-shared-toggle"
          aria-label={expanded ? 'Collapse shared values' : 'Expand shared values'}
          aria-expanded={expanded}
          onClick={() => setExpanded((v) => !v)}>
          <Icon name={expanded ? 'chevronDown' : 'chevronRight'} size={12} />
        </button>
        <span className="tool-wizard-shared-title">Shared values</span>
        <span className="tool-wizard-shared-sub muted">
          {buckets.length === 1
            ? '1 argument is used by 2 or more tools'
            : `${buckets.length} arguments are used by 2 or more tools`}
        </span>
      </div>

      {expanded && (
        <div className="tool-wizard-shared-rows">
          {buckets.map((bucket) => {
            const { applied, overridden, total } = countsFor(bucket);
            const draft = drafts[bucket.key] ?? '';
            const isApplied = applied > 0;
            const isOpen = Boolean(openTools[bucket.key]);
            const titleText = bucket.representativeSchema?.title || bucket.key;
            const description = bucket.representativeSchema?.description;
            return (
              <div
                key={`${bucket.key}::${bucket.kind}`}
                className={`tool-wizard-shared-row ${isApplied ? 'tool-wizard-shared-row-applied' : ''}`}>
                <div className="tool-wizard-shared-row-head">
                  <div className="tool-wizard-shared-row-label">
                    <span className="mono">{titleText}</span>
                    <span className="text-xs muted">{bucket.kind}</span>
                    {bucket.required && (
                      <span className="tool-wizard-shared-required" aria-label="Required">required</span>
                    )}
                  </div>
                  <button
                    type="button"
                    className="btn btn-sm btn-ghost tool-wizard-shared-used-by"
                    aria-expanded={isOpen}
                    onClick={() => toggleToolList(bucket.key)}>
                    Used by {total} tool{total === 1 ? '' : 's'}
                    <Icon name={isOpen ? 'chevronDown' : 'chevronRight'} size={11} />
                  </button>
                </div>

                {description && (
                  <div className="tool-wizard-shared-row-desc">{description}</div>
                )}

                {isOpen && (
                  <ul className="tool-wizard-shared-tool-list">
                    {bucket.toolNames.map((name) => {
                      const cfg = selectedTools[name];
                      const isOverride = Boolean(cfg?.sharedOverriddenFields?.[bucket.key]);
                      const isAppliedOne = Boolean(cfg?.sharedAppliedFields?.[bucket.key] && !isOverride);
                      return (
                        <li key={name} className="tool-wizard-shared-tool-list-item">
                          <span className="mono text-xs">{name}</span>
                          {isOverride && <span className="tool-wizard-shared-tool-pill tool-wizard-shared-tool-pill-warn">Override</span>}
                          {isAppliedOne && <span className="tool-wizard-shared-tool-pill tool-wizard-shared-tool-pill-ok">Applied</span>}
                          {!isOverride && !isAppliedOne && <span className="tool-wizard-shared-tool-pill">Pending</span>}
                        </li>
                      );
                    })}
                  </ul>
                )}

                <div className="tool-wizard-shared-row-body">
                  <input
                    className="input"
                    placeholder={`Value for ${bucket.key}`}
                    value={draft}
                    disabled={saving}
                    onChange={(e) => setDraft(bucket.key, e.target.value)}
                    onKeyDown={(e) => {
                      if (e.key === 'Enter' && draft !== '') {
                        e.preventDefault();
                        onApply(bucket, draft, { replaceOverrides: false });
                        setDraft(bucket.key, '');
                      }
                    }}
                  />
                  <button
                    type="button"
                    className="btn btn-sm btn-primary"
                    disabled={saving || draft === ''}
                    onClick={() => {
                      onApply(bucket, draft, { replaceOverrides: false });
                      setDraft(bucket.key, '');
                    }}>
                    Apply to {total - overridden} tool{total - overridden === 1 ? '' : 's'}
                  </button>
                </div>

                {isApplied && (
                  <div className="tool-wizard-shared-row-applied-status">
                    <Icon name="check" size={11} />
                    <span>
                      Applied to {applied} of {total} tools
                      {overridden > 0 && ` · ${overridden} overridden`}
                    </span>
                    {overridden > 0 && (() => {
                      // Default the payload to whatever is currently applied
                      // so the user isn't forced to retype the value they
                      // just set. If they typed a new draft, that wins.
                      const applied = currentlyAppliedValue(bucket);
                      const replaceValue = draft !== '' ? draft : applied;
                      const hasReplaceValue = replaceValue !== undefined && replaceValue !== '';
                      const sourceLabel = draft !== ''
                        ? 'new value above'
                        : 'currently-applied value';
                      return (
                        <button
                          type="button"
                          className="btn btn-sm btn-ghost tool-wizard-shared-replace"
                          disabled={saving || !hasReplaceValue}
                          title={hasReplaceValue
                            ? `Re-apply the ${sourceLabel}, replacing the ${overridden} override${overridden === 1 ? '' : 's'}`
                            : 'Nothing to apply — type a value above or use Apply first'}
                          onClick={() => {
                            onApplyToAll(bucket, replaceValue);
                            setDraft(bucket.key, '');
                          }}>
                          Replace overrides too
                        </button>
                      );
                    })()}
                    <button
                      type="button"
                      className="btn btn-sm btn-ghost"
                      disabled={saving}
                      onClick={() => onClear(bucket)}>
                      Clear
                    </button>
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

function ToolWizardFrequencyControl({ saving, applyIntervalToAll }) {
  const [draft, setDraft] = useStateMcp(/** @type {string} */ (''));
  return (
    <div className="tool-wizard-frequency" role="group" aria-label="Set frequency for all monitors">
      <span className="tool-wizard-frequency-icon" aria-hidden="true"><Icon name="clock" size={13} /></span>
      <span className="tool-wizard-frequency-label">Run every</span>
      <Select
        className="tool-wizard-frequency-select"
        ariaLabel="Pick a frequency"
        placeholder="…"
        disabled={saving}
        value={draft}
        onChange={setDraft}
        options={[
          { value: '60', label: '1 minute' },
          { value: '300', label: '5 minutes' },
          { value: '900', label: '15 minutes' },
          { value: '3600', label: '1 hour' },
        ]} />
      <span className="tool-wizard-frequency-trail">for all monitors</span>
      {draft && (
        <button
          className="btn btn-sm btn-primary tool-wizard-frequency-apply"
          disabled={saving}
          onClick={() => { applyIntervalToAll(Number(draft)); setDraft(''); }}>
          Apply to all
        </button>
      )}
    </div>
  );
}

function ToolMonitorWizard({ tools: rawTools, selectedTools: rawSelectedTools, setSelectedTools, saving, onSave, onCancel, showCancel = true, agentState = null, onAgentRetry = null, monitorNotice = '', savingMode = 'inline', hidePrimaryAction = false, onPrimaryActionChange = null, sectionMode = 'status' }) {
  const tools = Array.isArray(rawTools) ? rawTools : [];
  const selectedTools = rawSelectedTools || {};
  // Accordion: only one row may be open at a time. Holds the tool name of
  // the currently-open row, or null when all are collapsed.
  const [openTool, setOpenTool] = useStateMcp(/** @type {string | null} */ (null));
  const [skipConfirming, setSkipConfirming] = useStateMcp(false);
  // FLIP animation refs. We keep the row DOM nodes stable (single flat list
  // with stable React keys), then on every render compute each row's position
  // delta vs. the previous render and play a Web Animation that starts at the
  // old position and settles at the new one — no remount, no CSS keyframe
  // replay tricks. Stable DOM is what makes the motion smooth.
  const itemRefs = useRefMcp(/** @type {Map<string, HTMLElement>} */ (new Map()));
  const prevRectsRef = useRefMcp(/** @type {Map<string, {x:number,y:number}>} */ (new Map()));
  const prevBucketsRef = useRefMcp(/** @type {Record<string, string>} */ ({}));
  // The flat-list container — we lock its min-height during a FLIP run so the
  // surrounding modal doesn't recenter as section headers come and go (that
  // recenter is exactly the "screen jump" the user was complaining about).
  const flatListRef = useRefMcp(/** @type {HTMLDivElement | null} */ (null));
  const flatListHoldTimerRef = useRefMcp(/** @type {number | null} */ (null));
  // Scroll pinning: we track the modal scroll container's scrollTop via a
  // scroll event listener (see effect below), then on render commit the
  // useLayoutEffect restores it ONCE before measuring rects so the FLIP math
  // is stable. We don't keep restoring after the layout effect finishes —
  // that's what was breaking user scrolling for ~1.7s after every move.
  const prevScrollTopRef = useRefMcp(/** @type {number | null} */ (null));
  // Bumped each time the user attempts to Save while a tool is still in
  // 'needs' (or clicks a check icon on an invalid value). The pending check
  // buttons key off this nonce so they remount and replay their wiggle
  // animation.
  const [wiggleNonce, setWiggleNonce] = useStateMcp(0);
  // Per-tool wiggle nonce — bumped when the user tries to open a different
  // row while this one has dirty pending fields. The row's className carries
  // the nonce so each bump remounts the wiggle keyframe and replays the
  // shake. Tracked per-tool (not a single global) so collapsing tool A while
  // tool B is also dirty wiggles only A.
  const [rowWiggleNonces, setRowWiggleNonces] = useStateMcp(/** @type {Record<string, number>} */ ({}));
  // Tool name currently showing a one-shot "validate now" attention pulse on
  // its Validate button. Set when a leave-without-validate is blocked, then
  // cleared a moment later so a future block can re-trigger it.
  const [pulseAttentionTool, setPulseAttentionTool] = useStateMcp(/** @type {string | null} */ (null));
  const pulseAttentionTimerRef = useRefMcp(/** @type {number | null} */ (null));
  // Latest `selectedTools` and `openTool` mirrored into refs so toggleExpanded
  // can decide whether to allow navigation BEFORE calling setOpenTool. We
  // used to do the dirty check inside the setOpenTool functional updater,
  // which violated React's pure-updater contract: triggerLeaveBlockedFeedback
  // enqueues more setStates and a setTimeout, and in Strict Mode the updater
  // double-fires — bumping nonces by 2 and leaking timer callbacks. Doing
  // the check up-front against refs (always fresh, no closure staleness) and
  // then issuing a plain setOpenTool keeps the updater pure.
  const selectedToolsRef = useRefMcp(selectedTools);
  selectedToolsRef.current = selectedTools;
  const openToolRef = useRefMcp(/** @type {string | null} */ (null));
  openToolRef.current = openTool;
  // Per-tool: is the "N shared values applied" strip expanded (showing each
  // shared-applied field as an editable ToolArgField with a "shared" chip)?
  // Collapsed by default so the per-tool form stays compact when most args
  // came from the shared panel.
  const [sharedStripExpanded, setSharedStripExpanded] = useStateMcp(/** @type {Record<string, boolean>} */ ({}));
  const [openCliRiskSections, setOpenCliRiskSections] = useStateMcp({
    read: true,
    mutating: false,
    unknown: false,
    existing: true,
  });
  const useCliRiskSections = sectionMode === 'cliRisk';
  const agentStatus = agentState?.status || '';

  // Track the scroll container's scrollTop continuously via a real scroll
  // event listener. Doing this after-paint instead would lag by a frame —
  // the user could scroll, trigger a state change, and we'd restore to the
  // pre-scroll position (perceived as a scroll bug). The listener fires the
  // moment scrollTop actually changes, so prevScrollTopRef always reflects
  // the user's current viewport.
  React.useEffect(() => {
    const list = flatListRef.current;
    if (!list) return;
    let scrollEl = null;
    let p = list.parentElement;
    while (p) {
      const oy = window.getComputedStyle(p).overflowY;
      if (oy === 'auto' || oy === 'scroll') { scrollEl = p; break; }
      p = p.parentElement;
    }
    if (!scrollEl) return;
    prevScrollTopRef.current = scrollEl.scrollTop;
    const handler = () => { prevScrollTopRef.current = scrollEl.scrollTop; };
    scrollEl.addEventListener('scroll', handler, { passive: true });
    return () => scrollEl.removeEventListener('scroll', handler);
  }, [agentStatus, tools.length]);

  // Auto-expand the first tool with a validation/save error so the user sees
  // what's wrong without hunting. Accordion-style: one tool at a time.
  React.useEffect(() => {
    if (openTool) return;
    for (const t of tools) {
      const cfg = selectedTools[t.name];
      if (!cfg) continue;
      const hasFieldErr = cfg.errors && Object.keys(cfg.errors).length > 0;
      if (hasFieldErr || cfg.saveError) {
        setOpenTool(t.name);
        return;
      }
    }
  }, [selectedTools, tools, openTool]);

  // FLIP animation: every render, capture each tracked item's bounding rect,
  // diff against the rects we recorded last render, and for any element that
  // moved play a Web Animation from old → new position. The "mover" — the
  // row whose bucket actually changed — gets a slight scale + opacity dip in
  // the middle so it visually recedes and "flies" to its new spot while its
  // siblings smoothly close the gap behind and open space in front. All
  // other rows do a plain translate FLIP. Stable DOM (flat list, stable keys)
  // is what makes this work — without it the moving node would unmount and
  // the animation would have no element to play on.
  React.useLayoutEffect(() => {
    if (saving) return;

    const itemMap = itemRefs.current;

    // Find the closest scrolling ancestor and pin its scrollTop. When a row
    // moves between sections, the browser's automatic scroll anchoring tries
    // to keep its anchor element on screen — but the anchor may now sit far
    // away in DOM order, so the viewport jumps. We force scrollTop back to
    // what it was right before this render committed, then keep watching
    // briefly so any in-flight scroll-anchor adjustment also gets undone.
    const list = flatListRef.current;
    let scrollEl = null;
    if (list && typeof list.closest === 'function') {
      let p = list.parentElement;
      while (p) {
        const oy = window.getComputedStyle(p).overflowY;
        if (oy === 'auto' || oy === 'scroll') { scrollEl = p; break; }
        p = p.parentElement;
      }
    }
    if (scrollEl && prevScrollTopRef.current == null) {
      prevScrollTopRef.current = scrollEl.scrollTop;
    }
    const desiredScrollTop = scrollEl ? prevScrollTopRef.current : 0;

    // Cancel any in-flight FLIP animations first. If we measure before
    // cancelling, getBoundingClientRect returns the mid-animation transformed
    // position (a moving row reads as "already partway home") and the next
    // animation starts from a wrong place — visible as a sudden jump.
    if (typeof document !== 'undefined') {
      for (const [, el] of itemMap) {
        if (!el || typeof el.getAnimations !== 'function') continue;
        for (const a of el.getAnimations()) {
          if (a.id === 'tool-monitor-flip') a.cancel();
        }
      }
    }

    // Restore scroll BEFORE measuring so rects are taken in the same scroll
    // frame the user was viewing. Without this, a row that moved above the
    // current scroll position would push everything down and the FLIP would
    // animate from a position that's no longer where the user saw it.
    if (scrollEl && scrollEl.scrollTop !== desiredScrollTop) {
      scrollEl.scrollTop = desiredScrollTop;
    }

    /** @type {Map<string, {x:number, y:number}>} */
    const newRects = new Map();
    for (const [k, el] of itemMap) {
      if (!el || !document.contains(el)) continue;
      const r = el.getBoundingClientRect();
      newRects.set(k, { x: r.left, y: r.top });
    }

    // Decide which tools changed bucket — those are the "movers" that get
    // the lift-and-fly treatment. Buckets are tracked alongside positions
    // because two adjacent rows can swap order without crossing groups, and
    // we don't want to give them the dramatic mover styling.
    const prevBuckets = prevBucketsRef.current;
    /** @type {Record<string, string>} */
    const nextBuckets = {};
    /** @type {Set<string>} */
    const movers = new Set();
    for (const t of tools) {
      const cfg = selectedTools[t.name];
      const b = cfg?.bucket || (cfg?.checked ? 'needs' : 'skipped');
      nextBuckets[t.name] = b;
      if (prevBuckets[t.name] != null && prevBuckets[t.name] !== b) movers.add(t.name);
    }

    const prevRects = prevRectsRef.current;
    let didAnimate = false;
    if (prevRects.size > 0) {
      for (const [k, el] of itemMap) {
        if (!el || !document.contains(el)) continue;
        const oldR = prevRects.get(k);
        const newR = newRects.get(k);
        if (!oldR || !newR) continue;
        const dx = oldR.x - newR.x;
        const dy = oldR.y - newR.y;
        if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;

        const isMover = movers.has(k);
        // Mover: starts at its old spot, lifts a little (scale up + soft
        // shadow + raised z-index so it visually flies above the others),
        // then settles flush at its new spot. Siblings close/open space with
        // a shorter plain translate, so bulk toggles feel responsive.
        // Sibling (non-mover): just a plain translate FLIP closing the gap.
        const keyframes = isMover ? [
          {
            transform: `translate(${dx}px, ${dy}px) scale(1)`,
            opacity: 1,
            boxShadow: 'none',
            zIndex: 5,
            offset: 0,
          },
          {
            transform: `translate(${dx}px, ${dy - 8}px) scale(1.05)`,
            opacity: 1,
            boxShadow: '0 14px 30px rgba(9, 8, 8, 0.18)',
            zIndex: 5,
            offset: 0.22,
          },
          {
            transform: `translate(${dx * 0.45}px, ${dy * 0.45 - 6}px) scale(1.03)`,
            opacity: 0.95,
            boxShadow: '0 18px 34px rgba(9, 8, 8, 0.20)',
            zIndex: 5,
            offset: 0.62,
          },
          {
            transform: 'translate(0, 0) scale(1)',
            opacity: 1,
            boxShadow: 'none',
            zIndex: 5,
            offset: 1,
          },
        ] : [
          { transform: `translate(${dx}px, ${dy}px)` },
          { transform: 'translate(0, 0)' },
        ];
        try {
          const anim = el.animate(keyframes, {
            duration: isMover ? TOOL_MONITOR_FLIP_MOVER_DURATION_MS : TOOL_MONITOR_FLIP_SIBLING_DURATION_MS,
            easing: isMover
              ? 'cubic-bezier(0.34, 1.06, 0.42, 1)'
              : 'cubic-bezier(0.22, 0.8, 0.28, 1)',
            fill: 'none',
          });
          anim.id = 'tool-monitor-flip';
          didAnimate = true;
        } catch { /* WAAPI not available — skip animation */ }
      }
    }

    // Pin the flat-list min-height for the duration of the run so the
    // surrounding modal doesn't recenter (= "screen jump") when section
    // headers come and go. We use the LARGER of the previous and current
    // height so the list never collapses while a row is mid-flight, then
    // release after a small grace period past the longest animation.
    if (didAnimate) {
      if (list) {
        const measured = list.getBoundingClientRect().height;
        const prior = parseFloat(list.style.minHeight) || 0;
        const lockHeight = Math.max(measured, prior);
        list.style.minHeight = `${lockHeight}px`;
        if (flatListHoldTimerRef.current != null) {
          window.clearTimeout(flatListHoldTimerRef.current);
        }
        flatListHoldTimerRef.current = window.setTimeout(() => {
          if (flatListRef.current) flatListRef.current.style.minHeight = '';
          flatListHoldTimerRef.current = null;
        }, TOOL_MONITOR_FLIP_HOLD_DURATION_MS);
      }

    }

    prevRectsRef.current = newRects;
    prevBucketsRef.current = nextBuckets;
    // We deliberately do NOT auto-close the accordion when the open row's
    // bucket changes. Letting the body fly with the row keeps the user's
    // edit context, and FLIP animates the whole row (header + body) as one
    // unit — collapsing it on every move was the second source of the
    // perceived "jump".
  });

  function setToolField(name, patch) {
    setSelectedTools((s) => ({ ...s, [name]: { ...s[name], ...patch } }));
  }
  function setValue(name, key, value) {
    setSelectedTools((s) => {
      const cur = s[name] || {};
      const tool = tools.find((t) => t.name === name);
      const nextErrors = { ...(cur.errors || {}) };
      delete nextErrors[key];
      const nextCommitted = { ...(cur.committedFields || {}) };
      const props = tool?.inputSchema?.properties || {};
      const required = Array.isArray(tool?.inputSchema?.required) ? tool.inputSchema.required : [];
      const userMustAct = cur.fieldSources?.[key]?.source === 'requires_manual'
        || (required.includes(key) && cur.fieldSources?.[key]?.source !== 'agent');
      if (userMustAct) delete nextCommitted[key];
      // If the user edits a field that was applied from the Shared values
      // panel, mark this tool as having overridden it. The shared panel
      // shifts to "Applied to N · M overridden", and the field comes out of
      // the per-tool "shared applied" collapse strip into the normal field
      // grid with an "Override" chip. The shared-applied flag is cleared so
      // the panel's count of clean applications stays accurate.
      const wasSharedApplied = Boolean(cur.sharedAppliedFields?.[key]);
      const nextSharedApplied = { ...(cur.sharedAppliedFields || {}) };
      const nextSharedOverridden = { ...(cur.sharedOverriddenFields || {}) };
      if (wasSharedApplied) {
        delete nextSharedApplied[key];
        nextSharedOverridden[key] = true;
      }
      const next = {
        ...cur,
        values: { ...(cur.values || {}), [key]: value },
        committedFields: nextCommitted,
        errors: nextErrors,
        sharedAppliedFields: nextSharedApplied,
        sharedOverriddenFields: nextSharedOverridden,
      };
      if (tool && props[key]) next.bucket = computeBucket(tool, next);
      return {
        ...s,
        [name]: next,
      };
    });
  }

  // Write a shared value into every tool in the bucket. By default, tools
  // that the user has explicitly overridden after a previous apply are left
  // alone — Apply preserves intent. `replaceOverrides` is the escape hatch
  // exposed by the "Replace overrides too" button in the panel.
  function applySharedValue(bucket, value, options = {}) {
    const replaceOverrides = options.replaceOverrides === true;
    setSelectedTools((s) => {
      const next = { ...s };
      for (const name of bucket.toolNames) {
        const cur = next[name];
        if (!cur || !cur.checked || cur.existing) continue;
        const tool = tools.find((t) => t.name === name);
        if (!tool) continue;
        const props = tool.inputSchema?.properties || {};
        const schema = props[bucket.key];
        if (!schema) continue;
        const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
        const isOverride = Boolean(cur.sharedOverriddenFields?.[bucket.key]);
        if (isOverride && !replaceOverrides) continue;
        const nextCommitted = { ...(cur.committedFields || {}) };
        const userMustAct = cur.fieldSources?.[bucket.key]?.source === 'requires_manual'
          || (required.includes(bucket.key) && cur.fieldSources?.[bucket.key]?.source !== 'agent');
        const nextErrors = { ...(cur.errors || {}) };
        delete nextErrors[bucket.key];
        // Validate the typed value against THIS tool's schema before treating
        // Apply as a commit. Same name + same JSON kind can still diverge in
        // constraints — one tool's enum might not accept the value, another's
        // minLength might reject it. Without this check a type-invalid value
        // would land committed, push the tool into Ready, hide the per-row
        // Validate button, and trap the user with Save disabled but no
        // surfaced error to act on. With the check the error shows inline on
        // the field, the row stays in Needs, and Validate stays visible.
        const fieldErr = validateField(value, schema, bucket.kind, userMustAct);
        const nextSharedApplied = { ...(cur.sharedAppliedFields || {}) };
        const nextSharedOverridden = { ...(cur.sharedOverriddenFields || {}) };
        delete nextSharedOverridden[bucket.key];
        if (fieldErr) {
          nextErrors[bucket.key] = fieldErr;
          delete nextCommitted[bucket.key];
          // Don't mark as shared-applied when the value didn't validate.
          // Otherwise the field would disappear into the per-tool collapse
          // strip ("N shared values applied — expand to override"), hiding
          // the inline error and inflating the panel's Applied-to-N count
          // even though the apply effectively failed for this tool. By
          // leaving sharedAppliedFields untouched on failure, the field
          // stays in the regular per-tool body with its error visible and
          // the panel's count reflects what was actually accepted.
          delete nextSharedApplied[bucket.key];
        } else {
          if (userMustAct) nextCommitted[bucket.key] = true;
          nextSharedApplied[bucket.key] = true;
        }
        const updated = {
          ...cur,
          values: { ...(cur.values || {}), [bucket.key]: value },
          committedFields: nextCommitted,
          errors: nextErrors,
          sharedAppliedFields: nextSharedApplied,
          sharedOverriddenFields: nextSharedOverridden,
        };
        updated.bucket = computeBucket(tool, updated);
        next[name] = updated;
      }
      return next;
    });
  }

  // Undo "Applied to N tools". We clear the value out of each non-overridden
  // tool's `values` and drop the shared-applied flag. Overridden tools keep
  // their value — the user's manual edits weren't part of the shared write,
  // so clearing the shared row shouldn't blow them away.
  function clearSharedValue(bucket) {
    setSelectedTools((s) => {
      const next = { ...s };
      for (const name of bucket.toolNames) {
        const cur = next[name];
        if (!cur) continue;
        if (cur.sharedOverriddenFields?.[bucket.key]) continue;
        const tool = tools.find((t) => t.name === name);
        if (!tool) continue;
        const props = tool.inputSchema?.properties || {};
        if (!props[bucket.key]) continue;
        const kind = fieldKindFor(props[bucket.key]);
        const nextValues = { ...(cur.values || {}) };
        nextValues[bucket.key] = defaultValueForKind(kind, props[bucket.key]);
        const nextSharedApplied = { ...(cur.sharedAppliedFields || {}) };
        delete nextSharedApplied[bucket.key];
        const nextCommitted = { ...(cur.committedFields || {}) };
        delete nextCommitted[bucket.key];
        // Drop any prior validation error for this key. If the user had
        // applied + validated, then later cleared, leaving the old error
        // dangling on a now-empty field would show a stale "Must be a
        // number" or "Required" against a freshly-reset input. Symmetric
        // with applySharedValue, which clears errors on apply.
        const nextErrors = { ...(cur.errors || {}) };
        delete nextErrors[bucket.key];
        const updated = {
          ...cur,
          values: nextValues,
          sharedAppliedFields: nextSharedApplied,
          committedFields: nextCommitted,
          errors: nextErrors,
        };
        updated.bucket = computeBucket(tool, updated);
        next[name] = updated;
      }
      return next;
    });
  }
  // Validate all pending arguments for a monitor in one explicit row-level
  // action. This keeps rows from jumping while the user types, without making
  // them discover and click a checkmark beside every required argument.
  function validateMonitorParameters(name) {
    const tool = tools.find((t) => t.name === name);
    if (!tool) return;
    const cc = selectedTools[name];
    if (!cc) return;
    const props = tool.inputSchema?.properties || {};
    const requiredKeys = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
    const nextErrors = {};
    const nextCommitted = { ...(cc.committedFields || {}) };
    const ignored = cc.ignoredFields || {};
    let hasValidationErrors = false;
    for (const key of Object.keys(props)) {
      if (ignored[key]) continue;
      const schema = props[key];
      const kind = fieldKindFor(schema);
      const userMustAct = cc.fieldSources?.[key]?.source === 'requires_manual'
        || (requiredKeys.includes(key) && cc.fieldSources?.[key]?.source !== 'agent');
      const err = validateField(cc.values?.[key], schema, kind, userMustAct);
      if (err) {
        nextErrors[key] = err;
        delete nextCommitted[key];
        hasValidationErrors = true;
      } else if (userMustAct) {
        nextCommitted[key] = true;
      }
    }
    const next = { ...cc, committedFields: nextCommitted, errors: nextErrors };
    next.bucket = computeBucket(tool, next);
    setSelectedTools((s) => (s[name] ? { ...s, [name]: next } : s));
    if (hasValidationErrors) setWiggleNonce((n) => n + 1);
  }
  // The user clicked the per-field "ignore" icon (only available for fields
  // that are not officially required by the schema). The agent flagged the
  // field as requires_manual but the user has decided this caller doesn't
  // need to send it at all — drop it from the request payload, mark it
  // committed so the row can move to Ready, and clear any value or error.
  function ignoreField(name, key) {
    setSelectedTools((s) => {
      const cc = s[name];
      if (!cc) return s;
      const tool = tools.find((t) => t.name === name);
      if (!tool) return s;
      const nextCommitted = { ...(cc.committedFields || {}), [key]: true };
      const nextIgnored = { ...(cc.ignoredFields || {}), [key]: true };
      const nextErrors = { ...(cc.errors || {}) };
      delete nextErrors[key];
      const nextValues = { ...(cc.values || {}) };
      delete nextValues[key];
      const next = {
        ...cc,
        committedFields: nextCommitted,
        ignoredFields: nextIgnored,
        errors: nextErrors,
        values: nextValues,
      };
      next.bucket = computeBucket(tool, next);
      return { ...s, [name]: next };
    });
  }
  function unignoreField(name, key) {
    setSelectedTools((s) => {
      const cc = s[name];
      if (!cc) return s;
      const tool = tools.find((t) => t.name === name);
      if (!tool) return s;
      const nextCommitted = { ...(cc.committedFields || {}) };
      delete nextCommitted[key];
      const nextIgnored = { ...(cc.ignoredFields || {}) };
      delete nextIgnored[key];
      const next = { ...cc, committedFields: nextCommitted, ignoredFields: nextIgnored };
      next.bucket = computeBucket(tool, next);
      return { ...s, [name]: next };
    });
  }
  function toggleTool(name) {
    setSelectedTools((s) => {
      const cur = s[name] || { checked: false };
      const tool = tools.find((t) => t.name === name);
      const next = tool ? setToolCheckedState(tool, cur, !cur.checked) : { ...cur, checked: !cur.checked };
      return { ...s, [name]: next };
    });
  }
  // Bump the per-tool row wiggle nonce and fire the validate-button attention
  // pulse for the same tool. Used when the user tries to leave a dirty row
  // (collapse it, or open a different one) without clicking Validate. The
  // pulse auto-clears after a short delay so a later block can trigger it
  // again cleanly.
  function triggerLeaveBlockedFeedback(toolName) {
    setRowWiggleNonces((m) => ({ ...m, [toolName]: (m[toolName] || 0) + 1 }));
    setPulseAttentionTool(toolName);
    if (pulseAttentionTimerRef.current != null) {
      window.clearTimeout(pulseAttentionTimerRef.current);
    }
    pulseAttentionTimerRef.current = window.setTimeout(() => {
      setPulseAttentionTool((cur) => (cur === toolName ? null : cur));
      pulseAttentionTimerRef.current = null;
    }, 900);
  }

  function toggleExpanded(name) {
    // Read the latest open tool + selectedTools via refs so any batched
    // setState that landed before this call is visible. Doing the dirty
    // check + side effects up front (instead of inside a setOpenTool
    // functional updater) keeps the React updater pure — Strict Mode runs
    // updaters twice, so calling triggerLeaveBlockedFeedback from inside
    // would tick row nonces by 2 and leak timer callbacks.
    const cur = openToolRef.current;
    const currentSelected = selectedToolsRef.current;
    if (cur && cur !== name) {
      const openCfg = currentSelected[cur];
      const openTool_ = tools.find((t) => t.name === cur);
      if (openTool_ && hasDirtyPendingFields(openTool_, openCfg)) {
        triggerLeaveBlockedFeedback(cur);
        return;
      }
    }
    if (cur === name) {
      const openCfg = currentSelected[name];
      const openTool_ = tools.find((t) => t.name === name);
      if (openTool_ && hasDirtyPendingFields(openTool_, openCfg)) {
        triggerLeaveBlockedFeedback(name);
        return;
      }
    }
    setOpenTool(cur === name ? null : name);
  }

  React.useEffect(() => () => {
    if (pulseAttentionTimerRef.current != null) {
      window.clearTimeout(pulseAttentionTimerRef.current);
      pulseAttentionTimerRef.current = null;
    }
  }, []);

  // Play the row wiggle imperatively when the per-tool nonce ticks. We must
  // NOT replay it via a React key change — the row's DOM identity is what
  // the FLIP bucket animation relies on for stable position measurement, so
  // remounting would both break that and cancel mid-flight FLIPs. WAAPI
  // animates the row's existing element from a clean state on every nonce
  // bump.
  const lastPlayedWiggleRef = useRefMcp(/** @type {Record<string, number>} */ ({}));
  React.useEffect(() => {
    const last = lastPlayedWiggleRef.current;
    for (const [name, nonce] of Object.entries(rowWiggleNonces)) {
      if (last[name] === nonce) continue;
      last[name] = nonce;
      const el = itemRefs.current.get(name);
      if (!el || typeof el.animate !== 'function') continue;
      try {
        el.animate(
          [
            { transform: 'translateX(0)' },
            { transform: 'translateX(-6px)' },
            { transform: 'translateX(6px)' },
            { transform: 'translateX(-4px)' },
            { transform: 'translateX(4px)' },
            { transform: 'translateX(-2px)' },
            { transform: 'translateX(0)' },
          ],
          { duration: 420, easing: 'ease-in-out', fill: 'none' },
        );
      } catch { /* WAAPI unavailable — silent skip is fine */ }
    }
  }, [rowWiggleNonces]);
  function addExtra(name) {
    setSelectedTools((s) => {
      const cur = s[name] || {};
      const extras = [...(cur.extras || []), { key: '', value: '' }];
      return { ...s, [name]: { ...cur, extras } };
    });
  }
  function updateExtra(name, idx, patch) {
    setSelectedTools((s) => {
      const cur = s[name] || {};
      const extras = [...(cur.extras || [])];
      extras[idx] = { ...extras[idx], ...patch };
      const nextErrors = { ...(cur.errors || {}) };
      delete nextErrors[`__extra_${idx}_key`];
      return { ...s, [name]: { ...cur, extras, errors: nextErrors } };
    });
  }
  function removeExtra(name, idx) {
    setSelectedTools((s) => {
      const cur = s[name] || {};
      const extras = (cur.extras || []).filter((_, i) => i !== idx);
      return { ...s, [name]: { ...cur, extras } };
    });
  }

  if (tools.length === 0) {
    return (
      <div className="muted" style={{ padding: '8px 0' }}>
        No tools advertised by this server.
        <div className="row" style={{ gap: 8, marginTop: 12 }}>
          <button className="btn btn-sm" onClick={onCancel}>Close</button>
        </div>
      </div>
    );
  }

  // Buckets of arguments that 2+ currently-checked tools share by name + JSON
  // kind. Recomputed when the checked set or per-tool config changes. The
  // panel only renders when this has at least one entry. Memoized so the
  // panel doesn't churn drafts mid-typing on unrelated state churns.
  const sharedArgBuckets = useMemoMcp(
    () => computeSharedArgBuckets(tools, selectedTools),
    [tools, selectedTools],
  );

  // Already-monitored tools are part of `tools` for display, but they're not
  // "selectable" — the user can't save or edit them. Exclude them from every
  // count so the Save button reflects only the new monitors that will go up.
  const actionable = tools.filter((t) => !selectedTools[t.name]?.existing);
  const selectedCount = actionable.filter((t) => selectedTools[t.name]?.checked).length;
  const readySelectedCount = actionable.filter((t) => {
    const cfg = selectedTools[t.name];
    return cfg?.checked && bucketForToolState(t, cfg) === 'ready' && isToolReady(t, cfg);
  }).length;
  const allSelectedReady = selectedCount > 0 && readySelectedCount === selectedCount;

  const agentBucketCounts = { ready: 0, needs: 0, skipped: 0 };
  for (const t of actionable) {
    const bucket = bucketForToolState(t, selectedTools[t.name] || { checked: false });
    if (Object.prototype.hasOwnProperty.call(agentBucketCounts, bucket)) agentBucketCounts[bucket] += 1;
  }
  const agentReadyCount = agentBucketCounts.ready;
  const agentManualCount = agentBucketCounts.needs;
  const agentSkippedCount = agentBucketCounts.skipped;

  // Group tools into Ready / Needs input / Skipped buckets so the user can
  // scan by status. The bucket is the agent's verdict, recomputed only on
  // input commit (blur) — see commitTool() — so a row never jumps groups
  // mid-keystroke. The per-row badges have been removed: the group label
  // is the single source of truth, no contradicting "ready" pills inside
  // a "needs your input" group.
  const groups = { ready: [], needs: [], skipped: [], existing: [] };
  for (const t of tools) {
    const cfg = selectedTools[t.name] || { checked: false };
    const bucket = bucketForToolState(t, cfg);
    if (groups[bucket]) groups[bucket].push(t);
    else groups.needs.push(t);
  }
  const disabledNeedsCount = groups.skipped.filter((t) => selectedTools[t.name]?.disabledFromNeeds).length;

  function setItemRef(key, el) {
    if (el) itemRefs.current.set(key, el);
    else itemRefs.current.delete(key);
  }

  function renderToolRow(tool, groupTone) {
    const cfg = selectedTools[tool.name] || { checked: false, interval: 300, values: {}, extras: [], errors: {} };

    // Already-monitored tools render as a slim, non-interactive row: no toggle,
    // no expand, no Armature comment, just a quiet "Already monitored" pill.
    // Their entire row is muted so the eye treats them as informational.
    if (cfg.existing) {
      return (
        <div
          key={tool.name}
          ref={(el) => setItemRef(tool.name, el)}
          className="tool-monitor-row tool-monitor-row-existing"
          aria-disabled="true">
          <div className="tool-monitor-row-main tool-monitor-row-main-existing">
            <span className="tool-monitor-row-existing-icon" aria-hidden="true">
              <Icon name="check" size={12} />
            </span>
            <div className="tool-monitor-row-identity">
              <div className="tool-monitor-row-headline">
                <span className="tool-monitor-row-name mono">{tool.name}</span>
              </div>
              {tool.description && (
                <div className="tool-monitor-row-desc">{tool.description}</div>
              )}
            </div>
            <div className="tool-monitor-row-meta">
              <span className="badge badge-neutral text-xs">Already monitored</span>
            </div>
          </div>
        </div>
      );
    }

    // Skipped rows (toggle off) are not expandable: there's nothing to
    // configure until the user toggles them on. Suppressing the chevron and
    // the row-click handler avoids the dead-end "expand to an empty body"
    // state, and the only affordance left on the row is the toggle itself.
    const expandable = Boolean(cfg.checked);
    const isOpen = expandable && openTool === tool.name;
    const props = (tool.inputSchema && tool.inputSchema.properties) || {};
    const propKeys = Object.keys(props);
    const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
    const ready = isToolReady(tool, cfg);
    const additional = tool.inputSchema?.additionalProperties;
    const allowExtras = additional === true || (additional && typeof additional === 'object') || propKeys.length === 0;
    const errCount = cfg.errors ? Object.keys(cfg.errors).length : 0;

    const rowClass = ['tool-monitor-row', `tool-monitor-row-${groupTone}`];
    if (isOpen) rowClass.push('tool-monitor-row-expanded');
    if (!expandable) rowClass.push('tool-monitor-row-collapsed-locked');
    if (cfg.saveError) rowClass.push('tool-monitor-row-error');
    else if (cfg.saved) rowClass.push('tool-monitor-row-saved');
    const dirty = hasDirtyPendingFields(tool, cfg);
    const validatePulse = dirty;
    const validatePulseAttention = pulseAttentionTool === tool.name;

    return (
      <div
        key={tool.name}
        ref={(el) => setItemRef(tool.name, el)}
        className={rowClass.join(' ')}>
        <div
          className="tool-monitor-row-main"
          onClick={(e) => {
            if (!expandable) return;
            const tag = (e.target instanceof HTMLElement) ? e.target.tagName : '';
            if (tag === 'INPUT' || tag === 'SELECT' || tag === 'BUTTON' || tag === 'LABEL') return;
            toggleExpanded(tool.name);
          }}>
          <button
            type="button"
            role="switch"
            aria-checked={Boolean(cfg.checked)}
            aria-label={`${cfg.checked ? 'Disable' : 'Enable'} monitor for ${tool.name}`}
            className={`toggle ${cfg.checked ? 'on' : ''}`}
            disabled={saving}
            onClick={(e) => { e.stopPropagation(); toggleTool(tool.name); }}
          />
          <div className="tool-monitor-row-identity">
            <div className="tool-monitor-row-headline">
              <span className="tool-monitor-row-name mono">{tool.name}</span>
            </div>
            {tool.description && (
              <div className="tool-monitor-row-desc">{tool.description}</div>
            )}
            {agentState && cfg.agentReason && (
              <div className={`tool-monitor-row-agent-bubble tool-monitor-row-agent-bubble-${groupTone}`} role="note" aria-label="Armature agent comment">
                <span className="tool-monitor-row-agent-bubble-avatar" aria-hidden="true">{ARMATURE_MARK_SVG}</span>
                <span className="tool-monitor-row-agent-bubble-text">{cfg.agentReason}</span>
              </div>
            )}
          </div>
          <div className="tool-monitor-row-meta">
            {cfg.saveError && <span className="badge badge-fail text-xs">Save failed</span>}
            {cfg.saved && !cfg.saveError && <span className="badge badge-success text-xs">Saved</span>}
            {cfg.checked && bucketForToolState(tool, cfg) === 'needs' && (
              <button
                className={[
                  'btn', 'btn-sm', 'btn-primary', 'tool-monitor-validate',
                  validatePulse ? 'tool-monitor-validate-pulse' : '',
                  validatePulseAttention ? 'tool-monitor-validate-attention' : '',
                ].filter(Boolean).join(' ')}
                disabled={saving || !ready}
                title={ready ? 'Validate all parameters for this monitor' : 'Fill required parameters before validating'}
                onClick={(e) => { e?.stopPropagation?.(); validateMonitorParameters(tool.name); }}>
                <Icon name="check" size={12} />Validate parameters
              </button>
            )}
            {cfg.checked ? (
              <span className="tool-monitor-interval-wrap" onClick={(e) => e.stopPropagation()}>
                <Icon name="clock" size={11} className="tool-monitor-interval-icon" />
                <Select
                  className="tool-monitor-interval-inline"
                  value={cfg.interval ?? 300}
                  ariaLabel="Run frequency"
                  disabled={saving}
                  onChange={(value) => setToolField(tool.name, { interval: Number(value) })}
                  options={[
                    { value: 300, label: 'every 5m' },
                    { value: 900, label: 'every 15m' },
                    { value: 3600, label: 'every 1h' },
                  ]} />
              </span>
            ) : null}
          </div>
          {expandable ? (
            <button
              type="button"
              className="btn btn-sm btn-ghost tool-monitor-row-chevron"
              disabled={saving}
              onClick={(e) => { e.stopPropagation(); toggleExpanded(tool.name); }}
              aria-label={isOpen ? 'Collapse' : 'Expand'}>
              <Icon name={isOpen ? 'chevronDown' : 'chevronRight'} size={12} />
            </button>
          ) : (
            <span className="tool-monitor-row-chevron-spacer" aria-hidden="true" />
          )}
        </div>

        {isOpen && (
          <div className="tool-monitor-row-body">
            {(() => {
              if (!cfg.checked) return null;
              // Split shared-applied (and not-overridden) fields out into a
              // collapsed strip. Each tool gets its own expand state so the
              // user can pop one tool open without affecting others.
              const sharedAppliedKeys = propKeys.filter((k) => (
                cfg.sharedAppliedFields?.[k] && !cfg.sharedOverriddenFields?.[k]
              ));
              const otherKeys = propKeys.filter((k) => !sharedAppliedKeys.includes(k));
              const stripOpen = Boolean(sharedStripExpanded[tool.name]);

              function renderField(key, opts = {}) {
                const sourceInfo = cfg.fieldSources?.[key];
                const isRequired = required.includes(key);
                const userMustAct = sourceInfo?.source === 'requires_manual'
                  || (isRequired && sourceInfo?.source !== 'agent');
                const ignored = Boolean(cfg.ignoredFields?.[key]);
                const committed = Boolean(cfg.committedFields?.[key]);
                const pending = userMustAct && !committed;
                const isOverride = Boolean(cfg.sharedOverriddenFields?.[key]);
                return (
                  <div key={key} className={`tool-monitor-field-wrap ${opts.fromSharedStrip ? 'tool-monitor-field-wrap-shared' : ''}`}>
                    {(opts.fromSharedStrip || isOverride) && (
                      <span className={`tool-monitor-field-chip ${isOverride ? 'tool-monitor-field-chip-warn' : 'tool-monitor-field-chip-ok'}`}>
                        {isOverride ? 'Override' : 'Shared'}
                      </span>
                    )}
                    <ToolArgField
                      fieldKey={key}
                      schema={props[key]}
                      value={cfg.values?.[key]}
                      error={cfg.errors?.[key]}
                      isRequired={isRequired}
                      pending={pending}
                      ignored={ignored}
                      onChange={(v) => setValue(tool.name, key, v)}
                      onIgnore={pending && !isRequired ? () => ignoreField(tool.name, key) : null}
                      onUnignore={ignored ? () => unignoreField(tool.name, key) : null}
                      wiggleNonce={wiggleNonce}
                    />
                  </div>
                );
              }

              return (
                <>
                  {sharedAppliedKeys.length > 0 && (
                    <div className="tool-monitor-shared-strip">
                      <button
                        type="button"
                        className="btn btn-sm btn-ghost tool-monitor-shared-strip-toggle"
                        aria-expanded={stripOpen}
                        onClick={() => setSharedStripExpanded((m) => ({ ...m, [tool.name]: !m[tool.name] }))}>
                        <Icon name={stripOpen ? 'chevronDown' : 'chevronRight'} size={11} />
                        {sharedAppliedKeys.length} shared value{sharedAppliedKeys.length === 1 ? '' : 's'} applied — {stripOpen ? 'hide' : 'expand to override'}
                      </button>
                      {stripOpen && (
                        <div className="tool-monitor-shared-strip-body">
                          {sharedAppliedKeys.map((key) => renderField(key, { fromSharedStrip: true }))}
                        </div>
                      )}
                    </div>
                  )}
                  {otherKeys.map((key) => renderField(key))}
                </>
              );
            })()}

            {cfg.checked && allowExtras && (cfg.extras || []).length > 0 && (
              <div className="tool-monitor-extras">
                {(cfg.extras || []).map((row, idx) => (
                  <div key={idx} className="row" style={{ gap: 6, alignItems: 'flex-start' }}>
                    <div className="col" style={{ flex: '0 0 200px', gap: 2 }}>
                      <input
                        className="input mono text-xs"
                        placeholder="key"
                        value={row.key}
                        disabled={saving}
                        onChange={(e) => updateExtra(tool.name, idx, { key: e.target.value })}
                      />
                      {cfg.errors?.[`__extra_${idx}_key`] && (
                        <div className="text-xs error-text">{cfg.errors[`__extra_${idx}_key`]}</div>
                      )}
                    </div>
                    <input
                      className="input mono text-xs"
                      placeholder="value (string, or JSON literal)"
                      value={row.value}
                      disabled={saving}
                      onChange={(e) => updateExtra(tool.name, idx, { value: e.target.value })}
                      style={{ flex: 1 }}
                    />
                    <button className="btn btn-sm btn-ghost" disabled={saving} onClick={() => removeExtra(tool.name, idx)} aria-label="Remove field">
                      <Icon name="x" size={12} />
                    </button>
                  </div>
                ))}
              </div>
            )}
            {cfg.checked && allowExtras && (
              <button className="btn btn-sm btn-ghost tool-monitor-add-extra" disabled={saving} onClick={() => addExtra(tool.name)}>
                <Icon name="plus" size={12} />Add another argument
              </button>
            )}

            {cfg.saveError && (
              <div className="tool-monitor-save-error text-xs">
                <Icon name="alert" size={12} /> {cfg.saveError}
              </div>
            )}
          </div>
        )}
      </div>
    );
  }

  // Flatten groups into a single list of header + row items so every row is
  // a sibling of every other row. That stable parent is required by FLIP:
  // when a row's bucket changes, React keeps the same DOM node and just moves
  // it to a new index, letting useLayoutEffect measure before/after positions
  // and play a smooth translate animation. With the previous nested structure
  // (one <section> per group), a row crossing groups had to unmount and
  // remount, and there was no element to animate.
  /** @type {Array<{kind:'header', key:string, tone:string, label:string, count:number, hint:string, action?: {label:string, ariaLabel:string, onClick: () => void}, collapsible?: boolean, open?: boolean, onToggle?: () => void} | {kind:'row', key:string, tool:any, tone:string}>} */
  const flatItems = [];

  function cliRiskForTool(tool) {
    if (tool.cliRisk === 'read') return 'read';
    if (tool.cliRisk === 'mutating') return 'mutating';
    return 'unknown';
  }

  function setCliRiskToolsChecked(risk, checked) {
    setSelectedTools((s) => {
      const out = { ...s };
      for (const tool of tools) {
        if (cliRiskForTool(tool) !== risk) continue;
        const cur = out[tool.name] || { checked: false, interval: 300, values: {}, extras: [], errors: {} };
        if (cur.existing) continue;
        out[tool.name] = setToolCheckedState(tool, cur, checked);
      }
      return out;
    });
  }

  function toggleCliRiskSection(risk) {
    setOpenCliRiskSections((cur) => ({ ...cur, [risk]: !cur[risk] }));
  }

  if (useCliRiskSections) {
    const cliNeedsItems = groups.needs.filter((tool) => !selectedTools[tool.name]?.existing);
    const cliNeeds = new Set(cliNeedsItems.map((tool) => tool.name));
    if (cliNeedsItems.length > 0) {
      flatItems.push({
        kind: 'header',
        key: 'header-cli-needs',
        tone: 'warn',
        label: 'Needs your input',
        count: cliNeedsItems.length,
        hint: 'Fill missing arguments to enable monitoring.',
        action: {
          label: 'Disable all',
          ariaLabel: 'Disable all tools that need input',
          onClick: () => setNeedsInputToolsChecked(false),
        },
      });
      for (const t of cliNeedsItems) flatItems.push({ kind: 'row', key: t.name, tool: t, tone: 'warn' });
    }
    const cliRiskGroups = [
      { groupKey: 'read', label: 'Read-only commands', tone: 'ok', hint: 'Selected by default.', defaultOpen: true },
      { groupKey: 'mutating', label: 'Side-effecting commands', tone: 'warn', hint: 'Collapsed and unchecked by default.', defaultOpen: false },
      { groupKey: 'unknown', label: 'Unknown-risk commands', tone: 'neutral', hint: 'Collapsed and unchecked by default.', defaultOpen: false },
      { groupKey: 'existing', label: 'Already monitored', tone: 'neutral', hint: 'These tools are already pinged on a schedule.', defaultOpen: true },
    ];
    for (const g of cliRiskGroups) {
      const items = g.groupKey === 'existing'
        ? groups.existing
        : tools.filter((tool) => (
          !selectedTools[tool.name]?.existing
          && !cliNeeds.has(tool.name)
          && cliRiskForTool(tool) === g.groupKey
        ));
      if (items.length === 0) continue;
      const open = openCliRiskSections[g.groupKey] ?? g.defaultOpen;
      const selectable = items.filter((tool) => !selectedTools[tool.name]?.existing);
      const selectedInGroup = selectable.filter((tool) => selectedTools[tool.name]?.checked).length;
      const allSelectedInGroup = selectable.length > 0 && selectedInGroup === selectable.length;
      flatItems.push({
        kind: 'header',
        key: `header-cli-${g.groupKey}`,
        tone: g.tone,
        label: g.label,
        count: items.length,
        hint: g.hint,
        collapsible: true,
        open,
        onToggle: () => toggleCliRiskSection(g.groupKey),
        action: selectable.length > 0 ? {
          label: allSelectedInGroup ? 'Clear' : 'Select all',
          ariaLabel: `${allSelectedInGroup ? 'Clear' : 'Select all'} ${g.label}`,
          onClick: () => setCliRiskToolsChecked(g.groupKey, !allSelectedInGroup),
        } : null,
      });
      if (open) {
        for (const t of items) flatItems.push({ kind: 'row', key: t.name, tool: t, tone: g.tone });
      }
    }
  } else {
    const groupSpec = [
      {
        groupKey: 'needs',
        label: 'Needs your input',
        tone: 'warn',
        hint: 'Fill missing arguments to enable monitoring.',
        items: groups.needs,
        action: groups.needs.length > 0 ? {
          label: 'Disable all',
          ariaLabel: 'Disable all tools that need input',
          onClick: () => setNeedsInputToolsChecked(false),
        } : null,
      },
      { groupKey: 'ready', label: 'Ready to monitor', tone: 'ok', hint: 'Drafted by the agent — review and save.', items: groups.ready },
      {
        groupKey: 'skipped',
        label: 'Skipped — side-effecting',
        tone: 'bad',
        hint: 'Side-effecting commands are allowed. They start paused unless you choose to start without a test run.',
        items: groups.skipped,
        action: disabledNeedsCount > 0 ? {
          label: 'Enable all input-needed',
          ariaLabel: 'Enable all disabled tools that need input',
          onClick: () => setNeedsInputToolsChecked(true),
        } : null,
      },
      { groupKey: 'existing', label: 'Already monitored', tone: 'neutral', hint: 'These tools are already pinged on a schedule.', items: groups.existing },
    ];
    for (const g of groupSpec) {
      if (g.items.length === 0) continue;
      flatItems.push({ kind: 'header', key: `header-${g.groupKey}`, tone: g.tone, label: g.label, count: g.items.length, hint: g.hint, action: g.action });
      for (const t of g.items) flatItems.push({ kind: 'row', key: t.name, tool: t, tone: g.tone });
    }
  }

  function setNeedsInputToolsChecked(checked) {
    setSelectedTools((s) => setToolsCheckedInBucket(s, tools, checked ? 'disabledFromNeeds' : 'needs', checked));
  }

  function applyIntervalToAll(seconds) {
    setSelectedTools((s) => {
      const out = { ...s };
      for (const t of tools) {
        out[t.name] = { ...(out[t.name] || { checked: false }), interval: seconds };
      }
      return out;
    });
  }

  const blockedSelectedCount = Math.max(0, selectedCount - readySelectedCount);
  const saveDisabled = saving || selectedCount === 0 || !allSelectedReady;

  // Stable wrapper around onSave so the publish effect below doesn't fire on
  // every render. The host's onSave is recreated each render (it closes over
  // probe/selectedTools/etc.); listing it directly in the effect deps would
  // loop. We keep the latest in a ref and publish a once-created wrapper.
  const onSaveRef = useRefMcp(onSave);
  onSaveRef.current = onSave;
  const stablePrimaryClick = useRefMcp(() => {
    const fn = onSaveRef.current;
    if (typeof fn === 'function') fn();
  }).current;

  const primaryActionLabel = saving
    ? 'Saving…'
    : `Save ${selectedCount} monitor${selectedCount === 1 ? '' : 's'}`;
  const primaryAction = {
    label: primaryActionLabel,
    loading: saving,
    loadingLabel: 'Saving…',
    disabled: saveDisabled,
    onClick: stablePrimaryClick,
  };

  // Effect deps deliberately omit onClick (stable wrapper, see above) and
  // track only descriptor primitives that the footer button cares about.
  useEffectMcp(() => {
    if (typeof onPrimaryActionChange !== 'function') return undefined;
    onPrimaryActionChange(primaryAction);
    return () => onPrimaryActionChange(null);
  }, [
    onPrimaryActionChange,
    primaryAction.label,
    primaryAction.disabled,
    primaryAction.loading,
  ]);

  // Full-foreground loading takes over the wizard while the agent drafts. We
  // hide the row list AND the toolbar — there's nothing meaningful for the
  // user to do here yet, and the schema-default rows would just bait edits
  // that the agent fill is about to overwrite.
  if (agentState && agentState.status === 'loading') {
    return <ToolWizardLoading phase="agent" />;
  }

  // Saving takes the same full-foreground takeover when the embedder opts in
  // (currently the onboarding tour). The default 'inline' mode keeps per-row
  // saving badges visible so partial-failure recovery on the standalone
  // wizard still shows which monitors saved and which to retry.
  if (saving && savingMode === 'overlay') {
    return <ToolWizardLoading phase="saving" />;
  }

  return (
    <div className="col tool-wizard-body" aria-busy={saving ? 'true' : 'false'}>
      {agentState && (
        <AgentStrip
          status={agentState.status}
          ready={agentReadyCount}
          manual={agentManualCount}
          skipped={agentSkippedCount}
          error={agentState.error}
          onRetry={onAgentRetry}
        />
      )}

      <SharedValuesPanel
        buckets={sharedArgBuckets}
        selectedTools={selectedTools}
        saving={saving}
        onApply={(bucket, value) => applySharedValue(bucket, value, { replaceOverrides: false })}
        onApplyToAll={(bucket, value) => applySharedValue(bucket, value, { replaceOverrides: true })}
        onClear={clearSharedValue}
      />

      <ToolWizardFrequencyControl
        saving={saving}
        applyIntervalToAll={applyIntervalToAll}
      />

      {monitorNotice && (
        <div className="tool-wizard-note" role="note">
          <Icon name="alert" size={13} />
          <span>{monitorNotice}</span>
        </div>
      )}

      <div className="tool-wizard-flat-list" ref={flatListRef}>
        {flatItems.map((item) => {
          if (item.kind === 'header') {
            return (
              <div
                key={item.key}
                ref={(el) => setItemRef(item.key, el)}
                className={`tool-monitor-group-header tool-monitor-group-${item.tone}`}>
                {item.collapsible && (
                  <button
                    type="button"
                    className="btn btn-sm btn-ghost tool-monitor-group-toggle"
                    aria-label={`${item.open ? 'Collapse' : 'Expand'} ${item.label}`}
                    aria-expanded={Boolean(item.open)}
                    onClick={item.onToggle}>
                    <Icon name={item.open ? 'chevronDown' : 'chevronRight'} size={12} />
                  </button>
                )}
                <span className={`tool-monitor-group-dot tone-${item.tone}`} aria-hidden="true" />
                <span className="tool-monitor-group-label">{item.label}</span>
                <span className="tool-monitor-group-count">{item.count}</span>
                {item.hint && <span className="tool-monitor-group-hint muted">{item.hint}</span>}
                {item.action && (
                  <button
                    type="button"
                    className="btn btn-sm btn-ghost tool-monitor-group-action"
                    disabled={saving}
                    aria-label={item.action.ariaLabel}
                    onClick={(e) => { e.stopPropagation(); item.action.onClick(); }}>
                    {item.action.label}
                  </button>
                )}
              </div>
            );
          }
          return renderToolRow(item.tool, item.tone);
        })}
      </div>

      {skipConfirming && (
        <div className="tool-wizard-skip-confirm" role="alertdialog" aria-labelledby="tool-wizard-skip-confirm-title">
          <div className="tool-wizard-skip-confirm-text">
            <div className="tool-wizard-skip-confirm-title" id="tool-wizard-skip-confirm-title">Skip without saving?</div>
            <div className="tool-wizard-skip-confirm-sub">You can set these monitors up later from the Tool monitors page.</div>
          </div>
          <div className="tool-wizard-skip-confirm-actions">
            <button className="btn btn-sm" disabled={saving} onClick={() => setSkipConfirming(false)}>Keep editing</button>
            <button className="btn btn-sm btn-danger" disabled={saving} onClick={onCancel}>Yes, skip</button>
          </div>
        </div>
      )}

      {!hidePrimaryAction && (
        <div className="row tool-wizard-actions">
          <button
            className="btn btn-primary btn-sm"
            disabled={saveDisabled}
            onClick={() => {
              onSave();
            }}>
            {saving ? 'Saving…' : `Save ${selectedCount} monitor${selectedCount === 1 ? '' : 's'}`}
          </button>
          {showCancel && !skipConfirming && (
            <button
              className="btn btn-sm"
              disabled={saving}
              onClick={() => setSkipConfirming(true)}>
              Skip
            </button>
          )}
        </div>
      )}
      {selectedCount > 0 && !allSelectedReady && !skipConfirming && (
        <div className="tool-wizard-save-blocker" role="status">
          <Icon name="alert" size={13} />
          <span>
            <strong>Save is disabled.</strong> Validate parameters or disable {blockedSelectedCount} selected monitor{blockedSelectedCount === 1 ? '' : 's'} in Needs your input.
          </span>
        </div>
      )}

      {saving && savingMode === 'inline' && (
        <div className="tool-wizard-saving-veil" role="status" aria-live="polite" aria-label="Creating your tool monitors">
          <ToolWizardLoading phase="saving" />
        </div>
      )}
    </div>
  );
}

function ToolArgField({ fieldKey, schema, value, error, isRequired, pending = false, ignored = false, onChange, onCommit = null, onIgnore = null, onUnignore = null, depth = 0, wiggleNonce = 0 }) {
  const kind = fieldKindFor(schema);
  const description = schema?.description;
  const labelText = schema?.title || fieldKey;
  const placeholder = (() => {
    if (schema && Object.prototype.hasOwnProperty.call(schema, 'default')) return undefined;
    if (Array.isArray(schema?.examples) && schema.examples.length > 0) {
      try { return typeof schema.examples[0] === 'string' ? schema.examples[0] : JSON.stringify(schema.examples[0]); } catch { return undefined; }
    }
    return undefined;
  })();

  let control = null;
  const inputClass = `input ${error ? 'input-error' : ''} ${pending ? 'input-pending' : ''}`;
  if (kind === 'enum' || (kind === 'string' && Array.isArray(schema?.enum))) {
    control = (
      <Select
        className={`${error ? 'input-error' : ''} ${pending ? 'input-pending' : ''}`}
        value={String(safeInputValue(value))}
        onChange={onChange}
        options={[
          { value: '', label: isRequired ? 'Select…' : '(none)' },
          ...schema.enum.map((opt) => ({ value: String(opt), label: String(opt) })),
        ]} />
    );
  } else if (kind === 'boolean') {
    // Boolean toggles are discrete, so a value change IS the user's
    // confirmation. No separate check icon for these.
    control = (
      <label className="row" style={{ gap: 6 }}>
        <input
          type="checkbox"
          checked={Boolean(value)}
          onChange={(e) => { onChange(e.target.checked); if (onCommit) onCommit(); }}
        />
        <span className="text-xs muted">{value ? 'true' : 'false'}</span>
      </label>
    );
  } else if (kind === 'integer' || kind === 'number') {
    control = (
      <input
        className={inputClass}
        type="number"
        step={kind === 'integer' ? 1 : 'any'}
        min={typeof schema?.minimum === 'number' ? schema.minimum : undefined}
        max={typeof schema?.maximum === 'number' ? schema.maximum : undefined}
        placeholder={placeholder}
        value={safeInputValue(value)}
        onChange={(e) => onChange(e.target.value)}
        onKeyDown={(e) => { if (e.key === 'Enter' && onCommit) { e.preventDefault(); onCommit(); } }}
      />
    );
  } else if (kind === 'date' || kind === 'datetime-local' || kind === 'email' || kind === 'url') {
    control = (
      <input
        className={inputClass}
        type={kind}
        placeholder={placeholder}
        value={safeInputValue(value)}
        onChange={(e) => onChange(e.target.value)}
        onKeyDown={(e) => { if (e.key === 'Enter' && onCommit) { e.preventDefault(); onCommit(); } }}
      />
    );
  } else if (kind === 'array') {
    control = (
      <ArrayFieldEditor
        schema={schema}
        value={Array.isArray(value) ? value : []}
        error={error}
        onChange={onChange}
        depth={depth}
      />
    );
  } else if (kind === 'object' && depth < 2 && schema?.properties) {
    control = (
      <ObjectFieldEditor
        schema={schema}
        value={value && typeof value === 'object' ? value : {}}
        onChange={onChange}
        depth={depth}
      />
    );
  } else if (kind === 'json' || kind === 'object' || /** @type {string} */ (kind) === 'array') {
    const renderValue = typeof value === 'string'
      ? value
      : (value === undefined || value === null ? '' : (() => { try { return JSON.stringify(value, null, 2); } catch { return ''; } })());
    control = (
      <textarea
        rows={3}
        className={`textarea mono text-xs ${error ? 'input-error' : ''} ${pending ? 'input-pending' : ''}`}
        placeholder={placeholder || 'JSON value, e.g. ["a","b"] or {"k":"v"}'}
        value={renderValue}
        onChange={(e) => onChange(e.target.value)}
      />
    );
  } else {
    control = (
      <input
        className={inputClass}
        type="text"
        minLength={typeof schema?.minLength === 'number' ? schema.minLength : undefined}
        maxLength={typeof schema?.maxLength === 'number' ? schema.maxLength : undefined}
        pattern={schema?.pattern || undefined}
        placeholder={placeholder}
        value={safeInputValue(value)}
        onChange={(e) => onChange(e.target.value)}
        onKeyDown={(e) => { if (e.key === 'Enter' && onCommit) { e.preventDefault(); onCommit(); } }}
      />
    );
  }

  // The check icon appears only when the user explicitly needs to confirm
  // this field. Its key includes wiggleNonce so each save-attempt-while-
  // pending or invalid-commit attempt remounts it and replays the wiggle
  // animation. Boolean fields auto-commit on toggle, so they don't render
  // a check (the toggle itself is the confirmation gesture).
  const showCommitButton = !ignored && pending && onCommit && kind !== 'boolean';
  const showIgnoreButton = !ignored && pending && onIgnore && kind !== 'boolean';

  return (
    <div className={`tool-arg-field ${ignored ? 'tool-arg-field-ignored' : ''}`}>
      <div className="tool-arg-label">
        <span className="mono">{labelText}</span>
        {isRequired && <span className="tool-arg-required" aria-label="Required">required</span>}
        {ignored && <span className="tool-arg-ignored-pill">Skipped</span>}
      </div>
      <div className="tool-arg-control">
        {ignored ? (
          <div className="tool-arg-ignored-note">
            <span className="text-xs muted">Won't be sent in the request.</span>
          </div>
        ) : control}
      </div>
      {(showCommitButton || showIgnoreButton || ignored) && (
        <div className="tool-arg-actions">
          {showCommitButton && (
            <button
              key={`commit-${fieldKey}-${wiggleNonce}`}
              type="button"
              className="tool-arg-commit-btn"
              data-wiggle={wiggleNonce > 0 ? 'true' : 'false'}
              aria-label="Confirm value"
              title="Confirm this value. The monitor moves to Ready when every pending argument is confirmed."
              onClick={onCommit}>
              <Icon name="check" size={13} />
            </button>
          )}
          {showIgnoreButton && (
            <button
              type="button"
              className="tool-arg-ignore-btn"
              aria-label="Skip this argument"
              title="Skip this argument — it won't be sent. Use when the default the server picks is fine."
              onClick={onIgnore}>
              <Icon name="x" size={12} />
            </button>
          )}
          {ignored && onUnignore && (
            <button
              type="button"
              className="tool-arg-unignore-btn"
              aria-label="Don't skip this argument"
              title="Restore this argument so you can fill it in."
              onClick={onUnignore}>
              <Icon name="refresh" size={12} />
              <span>Undo</span>
            </button>
          )}
        </div>
      )}
      {description && <div className="text-xs muted tool-arg-hint">{description}</div>}
      {!ignored && error && <div className="text-xs error-text tool-arg-error">{error}</div>}
    </div>
  );
}

function ArrayFieldEditor({ schema, value, error, onChange, depth }) {
  const itemSchema = schema?.items && typeof schema.items === 'object' && !Array.isArray(schema.items) ? schema.items : null;
  const itemKind = itemSchema ? fieldKindFor(itemSchema) : null;
  const maxItems = typeof schema?.maxItems === 'number' ? schema.maxItems : null;
  const arr = Array.isArray(value) ? value : [];

  // Fall back to JSON textarea if items are unknown, or if item is array (rare),
  // or if we'd recurse deeper than 2 nesting levels for an object-of-array/object.
  const itemTooDeep = itemSchema && (itemKind === 'array' || (itemKind === 'object' && depth >= 1));
  if (!itemSchema || itemTooDeep) {
    const renderValue = typeof value === 'string'
      ? value
      : (() => { try { return JSON.stringify(value ?? [], null, 2); } catch { return ''; } })();
    return (
      <textarea
        rows={3}
        className={`textarea mono text-xs ${error ? 'input-error' : ''}`}
        placeholder='JSON array, e.g. ["a","b"]'
        value={renderValue}
        onChange={(e) => onChange(e.target.value)}
      />
    );
  }

  function updateItem(idx, next) {
    const out = arr.slice();
    out[idx] = next;
    onChange(out);
  }
  function removeItem(idx) {
    onChange(arr.filter((_, i) => i !== idx));
  }
  function addItem() {
    onChange([...arr, emptyValueForSchema(itemSchema)]);
  }

  const atMax = maxItems != null && arr.length >= maxItems;

  return (
    <div className="tool-arg-array">
      {arr.length === 0 && (
        <div className="tool-arg-array-empty text-xs muted">No items</div>
      )}
      {arr.map((item, idx) => (
        <div key={idx} className="tool-arg-array-item">
          <div className="tool-arg-array-item-body">
            <ToolArgField
              fieldKey={`Item ${idx + 1}`}
              schema={itemSchema}
              value={item}
              error={null}
              isRequired={false}
              onChange={(v) => updateItem(idx, v)}
              depth={depth + 1}
            />
          </div>
          <button
            type="button"
            className="btn btn-sm btn-ghost tool-arg-array-remove"
            onClick={() => removeItem(idx)}
            aria-label={`Remove item ${idx + 1}`}>
            <Icon name="trash" size={12} />
          </button>
        </div>
      ))}
      <button
        type="button"
        className="btn btn-sm tool-arg-array-add"
        disabled={atMax}
        onClick={addItem}>
        <Icon name="plus" size={12} />Add item
      </button>
    </div>
  );
}

function ObjectFieldEditor({ schema, value, onChange, depth }) {
  const props = schema?.properties || {};
  const required = Array.isArray(schema?.required) ? schema.required : [];
  const keys = Object.keys(props);
  if (keys.length === 0) {
    const renderValue = typeof value === 'string'
      ? value
      : (() => { try { return JSON.stringify(value ?? {}, null, 2); } catch { return ''; } })();
    return (
      <textarea
        rows={3}
        className="textarea mono text-xs"
        placeholder='JSON object, e.g. {"k":"v"}'
        value={renderValue}
        onChange={(e) => onChange(e.target.value)}
      />
    );
  }
  return (
    <div className="tool-arg-object">
      {keys.map((k) => (
        <ToolArgField
          key={k}
          fieldKey={k}
          schema={props[k]}
          value={value?.[k]}
          error={null}
          isRequired={required.includes(k)}
          onChange={(v) => onChange({ ...(value || {}), [k]: v })}
          depth={depth + 1}
        />
      ))}
    </div>
  );
}

function formatAuthType(profile) {
  if (profile.auth_type === 'bearer_token') return 'Bearer token';
  if (profile.auth_type === 'api_key_header') {
    const headerName = profile.auth_config?.header_name || profile.auth_config?.name || 'x-api-key';
    return `API key header: ${headerName}`;
  }
  if (profile.auth_type === 'oauth_authorization_code') {
    const slug = profile.auth_config?.provider_slug;
    return slug ? `OAuth · ${slug}` : 'OAuth';
  }
  if (profile.auth_type === 'none') return 'No auth';
  return profile.auth_type || 'Auth';
}

window.McpServersPage = McpServersPage;
window.CliTargetsPage = CliTargetsPage;
window.SourcesPage = SourcesPage;
window.ToolMonitorWizard = ToolMonitorWizard;
window.ToolWizardLoading = ToolWizardLoading;
window.ToolArgField = ToolArgField;
window.buildInitialToolState = buildInitialToolState;
window.validateAllSelectedTools = validateAllSelectedTools;
window.buildArgsFromState = buildArgsFromState;
window.mergeAgentFills = mergeAgentFills;
window.setToolCheckedState = setToolCheckedState;
window.setToolsCheckedInBucket = setToolsCheckedInBucket;
window.fieldKindFor = fieldKindFor;
window.computeSharedArgBuckets = computeSharedArgBuckets;
window.hasDirtyPendingFields = hasDirtyPendingFields;
window.defaultValueForKind = defaultValueForKind;
window.coerceValueForSubmit = coerceValueForSubmit;
window.validateField = validateField;
window.McpConnectModal = McpConnectModal;
window.OauthCuratedConfig = OauthCuratedConfig;
// Exposed so other pages (today: pages-onboarding.jsx) can reuse the exact
// same OAuth popup handshake, curated-provider override UI, awaiting-state
// panel, loopback paste-back modal, and shared form state/body as the main
// Connect MCP server modal, instead of forking a parallel implementation.
window.openOAuthPopup = openOAuthPopup;
window.runMcpOAuthFlow = runMcpOAuthFlow;
window.OauthOverrideFields = OauthOverrideFields;
window.McpOauthAwaitingPanel = McpOauthAwaitingPanel;
window.LoopbackOAuthModalRoot = LoopbackOAuthModalRoot;
window.useMcpConnectFormState = useMcpConnectFormState;
window.useMcpProvisioningPoll = useMcpProvisioningPoll;
window.McpConnectFormBody = McpConnectFormBody;
window.McpServerCard = McpServerCard;
window.validateMcpConnectFormInputs = validateMcpConnectFormInputs;
window.buildMcpConnectPayload = buildMcpConnectPayload;
window.CliSetupProgress = CliSetupProgress;
