import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, Dispatch, SetStateAction } from 'react';
import { Button, VisuallyHidden } from '@open-design/components';
import { validateBaseUrl } from '@open-design/contracts/api/connectionTest';
import {
  agentIdToTracking,
  byokProtocolToTracking,
  executionModeToTracking,
  settingsSectionToTracking,
} from '@open-design/contracts/analytics';
import { useAnalytics } from '../analytics/provider';
import { recordAmrEntry } from '../analytics/amr-attribution';
import {
  trackSettingsAppearanceClick,
  trackSettingsByokModelsFetchResult,
  trackSettingsByokTestResult,
  trackSettingsCliTestResult,
  trackSettingsByokFieldClick,
  trackSettingsByokProviderOptionClick,
  trackSettingsConnectorAuthResult,
  trackSettingsLanguageClick,
  trackSettingsLocalCliClick,
  trackSettingsExecutionModeTabClick,
  trackSettingsMediaProvidersClick,
  trackSettingsNotificationsClick,
  trackSettingsPrivacyClick,
  trackSettingsView,
} from '../analytics/events';
import { LOCALE_LABEL, LOCALES, useI18n } from '../i18n';
import type { Locale } from '../i18n';
import type { Dict } from '../i18n/types';
import { AgentIcon } from './AgentIcon';
import { AgentDiagnosticRow } from './AgentDiagnosticRow';
import { AmrLoginPill } from './AmrLoginPill';
import {
  AMR_LOGIN_STATUS_EVENT,
  amrLoginStatusEventReason,
} from './amrLoginPolling';
import {
  fetchVelaLoginStatus,
  type VelaLoginStatus,
} from '../providers/daemon';
import { ExportDiagnosticsRow } from './ExportDiagnosticsButton';
import { Icon } from './Icon';
import {
  CUSTOM_MODEL_SENTINEL,
  SearchableModelSelect,
} from './modelOptions';
import {
  DEFAULT_NOTIFICATIONS,
  DEFAULT_ORBIT,
  isStoredMediaProviderEntryEmpty,
  isStoredMediaProviderEntryPresent,
  KNOWN_PROVIDERS,
  hasAnyConfiguredProvider,
  mergeDaemonMediaProviders,
  syncComposioConfigToDaemon,
  syncConfigToDaemon,
  syncMediaProvidersToDaemon,
} from '../state/config';
import type { KnownProvider } from '../state/config';
import { navigate as navigateRoute, useRoute } from '../router';
import {
  API_PROTOCOL_LABELS,
  API_PROTOCOL_TABS,
  isFixedOriginGateway,
  resolveFixedOriginBaseUrl,
  SUGGESTED_MODELS_BY_PROTOCOL,
} from '../state/apiProtocols';
import {
  mergeProviderModelOptions,
  providerModelsCacheKey,
  type ProviderModelsCache,
} from './providerModelsCache';
export {
  mergeProviderModelOptions,
  providerModelsCacheKey,
} from './providerModelsCache';
import {
  MAX_MAX_TOKENS,
  MIN_MAX_TOKENS,
  modelMaxTokensDefault,
} from '../state/maxTokens';
import type {
  AgentInfo,
  ApiProtocol,
  ApiProtocolConfig,
  AppConfig,
  AppTheme,
  AppVersionInfo,
  ConnectionTestResponse,
  OrbitRunSummary,
  OrbitStatusResponse,
  ExecMode,
  ProviderModelOption,
  ProviderModelsResponse,
  SkillSummary,
} from '../types';
import { testAgent, testApiProvider } from '../providers/connection-test';
import { fetchProviderModels } from '../providers/provider-models';
import {
  fetchConnectors,
  fetchDesignTemplates,
  fetchLatestGithubReleaseInfo,
  openExternalUrl,
} from '../providers/registry';
import { MEDIA_PROVIDERS } from '../media/models';
import { useByokImageModelOptions, useByokVideoModelOptions, useByokSpeechModelOptions } from '../media/aihubmix-image-models';
import { XaiOAuthControl } from './XaiOAuthControl';
import type { MediaProvider } from '../media/models';
import { Toast } from './Toast';
import { PetSettings } from './pet/PetSettings';
import { McpClientSection } from './McpClientSection';
import { SkillsSection } from './SkillsSection';
import { DesignSystemsSection } from './DesignSystemsSection';
import { PrivacySection } from './PrivacySection';
import { ProjectLocationsSection } from './ProjectLocationsSection';
import { RoutinesSection } from './RoutinesSection';
import { ConnectorsBrowser } from './ConnectorsBrowser';
import { MemoryModelInline } from './MemoryModelInline';
import { MemorySection } from './MemorySection';
import { ByokConnectionTestControl } from './byok/ByokConnectionTestControl';
import { ByokKeyField } from './byok/ByokKeyField';
import { ByokModelField } from './byok/ByokModelField';
import { ByokProviderBaseUrl } from './byok/ByokProviderBaseUrl';
import { ByokProviderPicker } from './byok/ByokProviderPicker';
import {
  blockingByokDraftFields,
  blockingByokDraftIssues,
  cleanByokApiKey,
  resolveByokModelPreference,
  validateByokDraft,
  type ByokDraftField,
  type ByokDraftIssue,
  type ByokDraftValidation,
} from './byok/validation';
import {
  setCritiqueTheaterEnabled,
  useCritiqueTheaterEnabled,
} from './Theater';
import {
  ACCENT_SWATCHES,
  DEFAULT_ACCENT_COLOR,
  applyAppearanceToDocument,
  normalizeAccentColor,
  resolveAccentColor,
} from '../state/appearance';
import { isAutosaveDraftOnlyChange } from '../App';
import {
  FAILURE_SOUNDS,
  SUCCESS_SOUNDS,
  notificationPermission,
  playSound,
  requestNotificationPermission,
  showCompletionNotification,
} from '../utils/notifications';

export type SettingsSection =
  | 'execution'
  | 'instructions'
  | 'media'
  | 'composio'
  | 'orbit'
  | 'routines'
  | 'integrations'
  | 'mcpClient'
  | 'language'
  | 'appearance'
  | 'critiqueTheater'
  | 'notifications'
  | 'pet'
  | 'skills'
  | 'designSystems'
  | 'projectLocations'
  | 'memory'
  | 'privacy'
  // 'library' is consumed by the EntryShell library route — App opens it
  // via this same openSettings entry point, so SettingsSection must
  // accept the token even though SettingsDialog itself has no Library
  // section. Reconcile follow-up: route library through a dedicated
  // navigate() call so openSettings only owns dialog-bound sections.
  | 'library'
  | 'about';

// One-shot focus hint when opening the dialog. `'amr'` scrolls the AMR agent
// card into view on the execution section and plays a highlight (plus a
// sign-in coachmark when the user has not authorized AMR yet).
export type SettingsHighlight = 'amr' | null;

interface Props {
  initial: AppConfig;
  agents: AgentInfo[];
  agentsLoading?: boolean;
  daemonLive: boolean;
  appVersionInfo: AppVersionInfo | null;
  welcome?: boolean;
  initialSection?: SettingsSection;
  initialHighlight?: SettingsHighlight;
  providerModelsCache?: ProviderModelsCache;
  /**
   * Persist the current draft. Invoked by the dialog's autosave loop on
   * every committed edit. Returns a promise that resolves once both
   * localStorage and the daemon have caught up so the footer status
   * indicator can flip from "Saving…" to "Saved". Should NOT close the
   * dialog and should NOT mutate onboarding state — it represents an
   * incremental save, not a final commit.
   */
  onPersist: (cfg: AppConfig, options?: { forceMediaProviderSync?: boolean }) => Promise<void> | void;
  /**
   * Persist the Composio API key separately from the broader autosave
   * loop. Composio secrets need an explicit user gesture so half-typed
   * keys never leave the browser, so this is wired to a section-local
   * "Save key" button rather than the autosave channel.
   */
  onPersistComposioKey: (composio: AppConfig['composio']) => Promise<void> | void;
  /**
   * True while the daemon-backed Composio config is still hydrating on
   * first paint after a dev-server / app restart. The Connectors section
   * renders a skeleton over the input + buttons during this window so
   * the user does not mistake the temporarily empty input for "no key
   * saved" and so accidental Save/Clear clicks cannot overwrite the
   * saved state with `''` before the daemon's response lands.
   */
  composioConfigLoading?: boolean;
  onClose: () => void;
  onRefreshAgents: (
    options?: AgentRefreshOptions,
  ) => AgentInfo[] | Promise<AgentInfo[] | void> | void;
  onAmrLoginStatusChange?: (status: VelaLoginStatus | null) => void;
  /** Re-fetch functional skills into App state after Settings mutations. */
  onSkillsRefresh?: () => Promise<void> | void;
  daemonMediaProviders?: AppConfig['mediaProviders'] | null;
  daemonMediaProvidersFetchState?: 'idle' | 'ok' | 'error';
  mediaProvidersNotice?: string | null;
  onReloadMediaProviders?: () => Promise<AppConfig['mediaProviders'] | null>;
  onProjectsRefresh?: () => Promise<void> | void;
  /**
   * Notified by Settings → Skills after a successful skill registry
   * mutation (create / edit / delete). App.tsx uses this to drop preview
   * iframes whose project depends on the affected skill — body-only
   * edits do not move SkillSummary fields, so ProjectView's signature
   * path can miss them.
   */
  onSkillsChanged?: (affectedSkillId?: string) => void;
  /** Same channel for design-system registry mutations. */
  onDesignSystemsChanged?: (affectedDesignSystemId?: string) => void;
  onProviderModelsCacheChange?: Dispatch<SetStateAction<ProviderModelsCache>>;
}

export interface AgentRefreshOptions {
  throwOnError?: boolean;
  agentCliEnv?: AppConfig['agentCliEnv'];
}

// When AMR sign-in completes, vela's live `models` catalog can lag the
// credential write by a beat (the link backend has to register the freshly
// authorized device). Re-detect a few times so a momentarily-empty catalog
// doesn't leave the model picker hidden — the symptom that previously needed
// an app restart / reinstall to clear.
const AMR_SIGN_IN_RESCAN_ATTEMPTS = 4;
const AMR_SIGN_IN_RESCAN_RETRY_MS = 1500;

function codexPathStrings(locale: Locale) {
  if (locale === 'zh-CN') {
    return {
      repairHint: '当前保存的 Codex 路径不适合继续使用。',
      useDetected: '使用检测到的 Codex',
      clearCustom: '清空自定义路径',
      configuredSuccess: (path: string) => `本次测试使用的是已配置的 Codex 路径：${path}。`,
      invalidFallback: (configuredPath: string, detectedPath: string) =>
        `已配置的 Codex 路径无效或不可执行：${configuredPath}。本次测试改用 PATH 中的 Codex CLI：${detectedPath}。建议更新 CODEX_BIN 或清空自定义路径。`,
      failedFallback: (configuredPath: string, detectedPath: string) =>
        `已配置的 Codex 路径启动失败：${configuredPath}。本次测试改用 PATH 中的 Codex CLI：${detectedPath}。建议更新 CODEX_BIN 或清空自定义路径。`,
    };
  }
  if (locale === 'zh-TW') {
    return {
      repairHint: '目前儲存的 Codex 路徑不適合繼續使用。',
      useDetected: '使用偵測到的 Codex',
      clearCustom: '清除自訂路徑',
      configuredSuccess: (path: string) => `本次測試使用的是已設定的 Codex 路徑：${path}。`,
      invalidFallback: (configuredPath: string, detectedPath: string) =>
        `已設定的 Codex 路徑無效或不可執行：${configuredPath}。本次測試改用 PATH 中的 Codex CLI：${detectedPath}。建議更新 CODEX_BIN 或清除自訂路徑。`,
      failedFallback: (configuredPath: string, detectedPath: string) =>
        `已設定的 Codex 路徑啟動失敗：${configuredPath}。本次測試改用 PATH 中的 Codex CLI：${detectedPath}。建議更新 CODEX_BIN 或清除自訂路徑。`,
    };
  }
  return {
    repairHint: 'The saved Codex path is not the binary this test should keep using.',
    useDetected: 'Use detected Codex',
    clearCustom: 'Clear custom path',
    configuredSuccess: (path: string) =>
      `This test used the configured Codex path: ${path}.`,
    invalidFallback: (configuredPath: string, detectedPath: string) =>
      `Configured Codex path is invalid or not executable: ${configuredPath}. This test used the PATH Codex CLI at ${detectedPath}. Update CODEX_BIN or clear the custom path to use the detected binary.`,
    failedFallback: (configuredPath: string, detectedPath: string) =>
      `Configured Codex path failed: ${configuredPath}. This test succeeded with the PATH Codex CLI at ${detectedPath}. Update CODEX_BIN or clear the custom path to use the detected binary.`,
  };
}

function sanitizeHttpsUrl(url: string | undefined): string | undefined {
  if (!url) return undefined;
  try {
    const parsed = new URL(url);
    return parsed.protocol === 'https:' ? parsed.toString() : undefined;
  } catch {
    return undefined;
  }
}

type RescanNotice =
  | { kind: 'success'; count: number }
  | { kind: 'error' };

type TestState =
  | { status: 'idle' }
  | { status: 'running' }
  | { status: 'done'; result: ConnectionTestResponse };

const GATEWAY_API_PROTOCOLS = new Set<ApiProtocol>([
  'ollama',
  'senseaudio',
  'aihubmix',
]);

// Providers whose live model fetch IS their full account catalogue, so the
// per-option "from your account" badge and the "Loaded N from your account"
// hint are noise — every option carries the same badge and distinguishes
// nothing. For these we drop the source label and show a plain count instead.
// Add a protocol here when the same applies to another provider.
const ACCOUNT_MODEL_SOURCE_LABEL_HIDDEN = new Set<ApiProtocol>([
  'aihubmix',
]);

function hidesAccountModelSourceLabel(protocol: ApiProtocol): boolean {
  return ACCOUNT_MODEL_SOURCE_LABEL_HIDDEN.has(protocol);
}

// Fixed-origin gateway helpers (isFixedOriginGateway / resolveFixedOriginBaseUrl)
// live in ../state/apiProtocols so config loading and the top-bar switcher share
// the same single source of truth.

type ProviderModelsState =
  | { status: 'idle' }
  | { status: 'running'; cacheKey: string }
  | { status: 'done'; cacheKey: string; result: ProviderModelsResponse };

type ByokRequiredField = ByokDraftField;
type ByokPreconditionAction = 'test';
type ByokFieldMissing = 'api_key' | 'base_url' | 'model' | 'multiple' | 'none';

function byokFieldMissingFromIssues(issues: readonly ByokDraftIssue[]): ByokFieldMissing {
  const missingFields = new Set<ByokRequiredField>();
  for (const issue of issues) {
    if (
      issue.code === 'api_key_required' ||
      issue.code === 'base_url_required' ||
      issue.code === 'model_required'
    ) {
      missingFields.add(issue.field);
    }
  }
  if (missingFields.size === 0) return 'none';
  if (missingFields.size > 1) return 'multiple';
  return Array.from(missingFields)[0] ?? 'none';
}

function byokErrorKindFromIssues(issues: readonly ByokDraftIssue[]): string | undefined {
  return issues[0]?.code;
}

function byokTrackingTestResult(result: ConnectionTestResponse): 'success' | 'failed' | 'timeout' {
  if (result.ok) return 'success';
  return result.kind === 'timeout' ? 'timeout' : 'failed';
}

// Map a test result to the visual severity of its inline status node so
// the same green/red/amber palette as the Rescan status applies.
export function testStatusVariant(
  result: ConnectionTestResponse,
): 'success' | 'warn' | 'error' {
  if (result.ok) return 'success';
  if (result.kind === 'rate_limited') return 'warn';
  return 'error';
}

export function shouldShowCustomModelInput(
  modelValue: string,
  knownModelIds: readonly string[],
  explicitCustomMode: boolean,
): boolean {
  return (
    explicitCustomMode ||
    !modelValue ||
    !knownModelIds.includes(modelValue)
  );
}

export function canRunProviderConnectionTest(
  config: Pick<AppConfig, 'apiKey' | 'baseUrl' | 'model'>,
  options: { requiresApiKey?: boolean } = {},
): boolean {
  const requiresApiKey = options.requiresApiKey ?? true;
  return (
    (!requiresApiKey || Boolean(config.apiKey.trim())) &&
    Boolean(config.baseUrl.trim()) &&
    Boolean(config.model.trim())
  );
}

export function canFetchProviderModels(
  config: Pick<AppConfig, 'apiKey' | 'baseUrl'>,
  protocol: ApiProtocol,
): boolean {
  return (
    protocol !== 'azure' &&
    protocol !== 'ollama' &&
    Boolean(config.apiKey.trim()) &&
    Boolean(config.baseUrl.trim()) &&
    isValidApiBaseUrl(config.baseUrl)
  );
}

function missingByokConnectionFields(
  config: Pick<AppConfig, 'apiKey' | 'baseUrl' | 'model'>,
  options: { requiresApiKey?: boolean } = {},
): ByokRequiredField[] {
  const requiresApiKey = options.requiresApiKey ?? true;
  const missing: ByokRequiredField[] = [];
  if (requiresApiKey && !config.apiKey.trim()) missing.push('api_key');
  if (!config.baseUrl.trim()) missing.push('base_url');
  if (!config.model.trim()) missing.push('model');
  return missing;
}

function missingByokModelFetchFields(
  config: Pick<AppConfig, 'apiKey' | 'baseUrl'>,
  protocol?: ApiProtocol,
): ByokRequiredField[] {
  const missing: ByokRequiredField[] = [];
  // AIHubMix publishes its catalogue on a public endpoint, so its model list
  // loads without a key (the user shouldn't need to paste a key just to browse
  // models). Every other protocol fetches /v1/models behind the key.
  if (protocol !== 'aihubmix' && !config.apiKey.trim()) missing.push('api_key');
  if (!config.baseUrl.trim()) missing.push('base_url');
  return missing;
}

function providerConnectionTestKey(
  protocol: ApiProtocol,
  config: Pick<AppConfig, 'apiKey' | 'baseUrl' | 'model' | 'apiVersion'>,
): string {
  return [
    protocol,
    config.baseUrl.trim().replace(/\/+$/, ''),
    config.apiKey.trim(),
    config.model.trim(),
    protocol === 'azure' ? config.apiVersion?.trim() ?? '' : '',
  ].join('\n');
}

function isLocalOllamaBaseUrl(baseUrl: string): boolean {
  try {
    const parsed = new URL(baseUrl);
    const hostname = parsed.hostname.toLowerCase();
    return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
  } catch {
    return false;
  }
}

function byokProviderRequiresApiKey(
  protocol: ApiProtocol,
  provider: KnownProvider | undefined,
  baseUrl: string,
): boolean {
  if (provider?.requiresApiKey === false) return false;
  if (protocol === 'ollama' && isLocalOllamaBaseUrl(baseUrl)) return false;
  return true;
}

type ByokFirstPartyBaseUrlHint = {
  baseUrl: string;
  hostTypo: boolean;
};

function byokFirstPartyBaseUrlHint(
  protocol: ApiProtocol,
  baseUrl: string,
  protocolProviders: readonly KnownProvider[],
): ByokFirstPartyBaseUrlHint | undefined {
  if (
    protocol !== 'anthropic' &&
    protocol !== 'openai' &&
    protocol !== 'google'
  ) {
    return undefined;
  }
  const firstPartyBaseUrl = protocolProviders.find(
    (provider) => provider.baseUrl.trim(),
  )?.baseUrl;
  if (!firstPartyBaseUrl) return undefined;

  const firstPartyHost = byokDraftBaseUrlHost(firstPartyBaseUrl);
  const draftHost = byokDraftBaseUrlHost(baseUrl);
  if (!firstPartyHost || !draftHost) return undefined;
  if (draftHost === firstPartyHost) {
    return { baseUrl: firstPartyBaseUrl, hostTypo: false };
  }
  if (!draftHost.startsWith(firstPartyHost)) return undefined;

  const suffix = draftHost.slice(firstPartyHost.length);
  return suffix && !suffix.startsWith('.')
    ? { baseUrl: firstPartyBaseUrl, hostTypo: true }
    : undefined;
}

function byokDraftBaseUrlHost(value: string): string | undefined {
  const trimmed = value.trim();
  if (!trimmed) return undefined;
  const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
    ? trimmed
    : `https://${trimmed}`;
  try {
    return new URL(withProtocol).hostname.toLowerCase();
  } catch {
    return undefined;
  }
}

const API_KEY_CONSOLE_LINKS: Record<ApiProtocol, { host: string; url: string }> = {
  anthropic: {
    host: 'console.anthropic.com',
    url: 'https://console.anthropic.com/settings/keys',
  },
  openai: {
    host: 'platform.openai.com',
    url: 'https://platform.openai.com/api-keys',
  },
  azure: {
    host: 'portal.azure.com',
    url: 'https://portal.azure.com/',
  },
  google: {
    host: 'aistudio.google.com',
    url: 'https://aistudio.google.com/apikey',
  },
  ollama: {
    host: 'ollama.com',
    url: 'https://ollama.com/settings/keys',
  },
  senseaudio: {
    host: 'docs.senseaudio.cn',
    url: 'https://docs.senseaudio.cn',
  },
  aihubmix: {
    host: 'aihubmix.com',
    url: 'https://aihubmix.com/?aff=JA1e',
  },
};

const AGENT_SHORT_DESCRIPTIONS: Record<string, string> = {
  claude: 'Anthropic official CLI',
  codex: 'OpenAI official CLI',
  'cursor-agent': 'Cursor command line',
  gemini: 'Google official CLI',
  opencode: 'Open-source agent CLI',
  qwen: 'Qwen coding CLI',
  copilot: 'GitHub coding CLI',
  devin: 'Cognition terminal CLI',
  kimi: 'Moonshot Kimi CLI',
  qoder: 'Alibaba coding CLI',
  pi: 'Inflection chat CLI',
  kiro: 'Kiro agent CLI',
  kilo: 'Kilo Code CLI',
  vibe: 'Mistral open-source CLI',
  deepseek: 'DeepSeek terminal UI',
  hermes: 'ACP agent CLI',
  'grok-build': 'xAI coding CLI',
  reasonix: 'DeepSeek native coding CLI',
};

function cleanAgentVersionLabel(
  name: string,
  version: string | null | undefined,
): string {
  if (!version) return '';
  const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  return version
    .replace(new RegExp(`\\s*\\(${escapedName}\\)\\s*$`, 'i'), '')
    .replace(new RegExp(`\\s+${escapedName}\\s*$`, 'i'), '')
    .trim();
}

function displayAgentName(agent: Pick<AgentInfo, 'id' | 'name'>): string {
  return agent.id === 'amr' ? 'Open Design AMR' : agent.name;
}

const AGENT_CLI_ENV_FIELDS = [
  {
    agentId: 'claude',
    envKey: 'CLAUDE_CONFIG_DIR',
    labelKey: 'settings.cliEnvClaudeConfigDir',
    placeholder: '~/.claude-2',
  },
  {
    agentId: 'claude',
    envKey: 'ANTHROPIC_BASE_URL',
    labelKey: 'settings.cliEnvClaudeBaseUrl',
    placeholder: 'https://your-proxy.example.com',
  },
  {
    agentId: 'claude',
    envKey: 'ANTHROPIC_API_KEY',
    labelKey: 'settings.cliEnvClaudeApiKey',
    placeholder: 'Paste proxy API key',
    secret: true,
  },
  {
    agentId: 'codex',
    envKey: 'CODEX_HOME',
    labelKey: 'settings.cliEnvCodexHome',
    placeholder: '~/.codex-alt',
  },
  {
    agentId: 'codex',
    envKey: 'CODEX_BIN',
    labelKey: 'settings.cliEnvCodexBin',
    placeholder: '/absolute/path/to/codex',
  },
  {
    agentId: 'codex',
    envKey: 'OPENAI_BASE_URL',
    labelKey: 'settings.cliEnvCodexBaseUrl',
    placeholder: 'https://your-proxy.example.com/v1',
  },
  {
    agentId: 'codex',
    envKey: 'CODEX_API_KEY',
    labelKey: 'settings.cliEnvCodexApiKey',
    labelSuffix: 'CODEX_API_KEY',
    placeholder: 'Paste CODEX_API_KEY',
    secret: true,
  },
  {
    agentId: 'codex',
    envKey: 'OPENAI_API_KEY',
    labelKey: 'settings.cliEnvCodexApiKey',
    labelSuffix: 'OPENAI_API_KEY · proxy/legacy',
    placeholder: 'Paste OPENAI_API_KEY',
    secret: true,
  },
] as const;

function defaultApiProtocolConfig(protocol: ApiProtocol): ApiProtocolConfig {
  const provider = KNOWN_PROVIDERS.find((p) => p.protocol === protocol);
  return {
    apiKey: '',
    baseUrl: provider?.baseUrl ?? '',
    model: provider?.model ?? '',
    apiVersion: '',
    apiProviderBaseUrl: provider ? provider.baseUrl : null,
  };
}

function providerFamilyLabel(provider: KnownProvider): string {
  return provider.label.replace(/\s+—\s+(Anthropic|OpenAI)$/u, '');
}

function siblingProviderForProtocol(
  providerBaseUrl: string | null | undefined,
  protocol: ApiProtocol,
): KnownProvider | null {
  if (!providerBaseUrl) return null;
  const currentProvider = KNOWN_PROVIDERS.find(
    (p) => p.baseUrl === providerBaseUrl,
  );
  if (!currentProvider) return null;

  const currentFamily = providerFamilyLabel(currentProvider);
  return (
    KNOWN_PROVIDERS.find(
      (p) => p.protocol === protocol && providerFamilyLabel(p) === currentFamily,
    ) ?? null
  );
}

function nextApiProtocolConfig(
  config: AppConfig,
  protocol: ApiProtocol,
): ApiProtocolConfig {
  const savedConfig = config.apiProtocolConfigs?.[protocol];
  if (savedConfig) return savedConfig;

  const currentConfig = currentApiProtocolConfig(config);
  const siblingProvider = siblingProviderForProtocol(
    currentConfig.apiProviderBaseUrl,
    protocol,
  );
  if (siblingProvider) {
    return {
      ...defaultApiProtocolConfig(protocol),
      baseUrl: siblingProvider.baseUrl,
      model: siblingProvider.model,
      apiProviderBaseUrl: siblingProvider.baseUrl,
    };
  }

  if (currentConfig.apiProviderBaseUrl === null) {
    return {
      ...currentConfig,
      apiKey: '',
      apiVersion: protocol === 'azure' ? currentConfig.apiVersion : '',
      apiProviderBaseUrl: null,
    };
  }

  return {
    ...defaultApiProtocolConfig(protocol),
  };
}

function currentApiProtocolConfig(config: AppConfig): ApiProtocolConfig {
  return {
    apiKey: config.apiKey,
    baseUrl: config.baseUrl,
    model: config.model,
    apiVersion: config.apiVersion ?? '',
    apiProviderBaseUrl: config.apiProviderBaseUrl ?? null,
    byokImageModel: config.byokImageModel ?? '',
    byokVideoModel: config.byokVideoModel ?? '',
    byokSpeechModel: config.byokSpeechModel ?? '',
    byokSpeechVoice: config.byokSpeechVoice ?? '',
  };
}

function applyApiProtocolConfig(
  config: AppConfig,
  protocol: ApiProtocol,
  apiConfig: ApiProtocolConfig,
): AppConfig {
  return {
    ...config,
    apiProtocol: protocol,
    apiKey: apiConfig.apiKey,
    baseUrl: resolveFixedOriginBaseUrl(protocol, apiConfig.baseUrl),
    model: apiConfig.model,
    apiProviderBaseUrl: apiConfig.apiProviderBaseUrl ?? null,
    apiVersion: protocol === 'azure' ? (apiConfig.apiVersion ?? '') : '',
    // byokImageModel applies to the protocols that inject the daemon-side
    // generate_image tool (SenseAudio, AIHubMix) — flipping to another BYOK
    // tab shouldn't carry an image-model choice into, say, the OpenAI form.
    // Mirrors the apiVersion guarding above.
    byokImageModel:
      protocol === 'senseaudio' || protocol === 'aihubmix'
        ? (apiConfig.byokImageModel ?? '')
        : '',
    // byokVideoModel only applies to AIHubMix today (the only BYOK chat with a
    // video-model picker; SenseAudio's video tool uses a fixed model).
    byokVideoModel:
      protocol === 'aihubmix' ? (apiConfig.byokVideoModel ?? '') : '',
    // Speech model + voice also AIHubMix-only today.
    byokSpeechModel:
      protocol === 'aihubmix' ? (apiConfig.byokSpeechModel ?? '') : '',
    byokSpeechVoice:
      protocol === 'aihubmix' ? (apiConfig.byokSpeechVoice ?? '') : '',
  };
}

export function isValidApiBaseUrl(value: string): boolean {
  const trimmed = value.trim();
  if (!/^https?:\/\//i.test(trimmed)) return false;
  const result = validateBaseUrl(trimmed);
  return Boolean(result.parsed && !result.error);
}

export function updateCurrentApiProtocolConfig(
  config: AppConfig,
  patch: Partial<ApiProtocolConfig>,
): AppConfig {
  const protocol = config.apiProtocol ?? 'anthropic';
  const nextApiConfig: ApiProtocolConfig = {
    ...currentApiProtocolConfig(config),
    ...patch,
  };
  return applyApiProtocolConfig(
    {
      ...config,
      apiProtocolConfigs: {
        ...(config.apiProtocolConfigs ?? {}),
        [protocol]: nextApiConfig,
      },
    },
    protocol,
    nextApiConfig,
  );
}

export function updateAgentCliEnvValue(
  config: AppConfig,
  agentId: string,
  envKey: string,
  rawValue: string,
): AppConfig {
  const value = rawValue.trim();
  const agentCliEnv = { ...(config.agentCliEnv ?? {}) };
  const nextAgentEnv = { ...(agentCliEnv[agentId] ?? {}) };
  if (value) {
    nextAgentEnv[envKey] = value;
  } else {
    delete nextAgentEnv[envKey];
  }

  if (Object.keys(nextAgentEnv).length > 0) {
    agentCliEnv[agentId] = nextAgentEnv;
  } else {
    delete agentCliEnv[agentId];
  }

  return {
    ...config,
    agentCliEnv: Object.keys(agentCliEnv).length > 0 ? agentCliEnv : {},
  };
}

export function agentRefreshOptionsForConfig(cfg: AppConfig): AgentRefreshOptions {
  return {
    throwOnError: true,
    agentCliEnv: cfg.agentCliEnv ?? {},
  };
}

function apiModelOptionLabel(
  model: ProviderModelOption,
  sourceLabel?: string,
): string {
  const baseLabel = model.label && model.label !== model.id
    ? `${model.label} (${model.id})`
    : model.id;
  return sourceLabel ? `${baseLabel} · ${sourceLabel}` : baseLabel;
}

function codexPathRepairState(
  result: ConnectionTestResponse,
): { detectedPath: string; canUseDetected: boolean } | null {
  if (!result.ok) return null;
  if (
    result.usedExecutableSource !== 'fallback_invalid' &&
    result.usedExecutableSource !== 'fallback_failed'
  ) {
    return null;
  }
  const detectedPath = result.detectedExecutablePath?.trim() || '';
  if (!detectedPath) return null;
  return {
    detectedPath,
    canUseDetected: true,
  };
}

/**
 * Returns whether the modal's footer Save button should be enabled for the
 * currently active sidebar section.
 *
 * The mode-completeness check (BYOK requires apiKey + model + valid baseUrl;
 * Local CLI requires a selected available agent) is only meaningful on the
 * execution-mode section, where the user is actively editing those fields.
 * On every other sidebar section (language, appearance, composio, media,
 * integrations, notifications, pet, library, about), partial state from a
 * draft mode toggle (e.g. user clicked BYOK on the execution section without
 * filling in fields, then navigated to language) must NOT block saving
 * changes the user is making in those unrelated sections. Issue #739.
 */
export function shouldEnableSettingsSave(
  cfg: AppConfig,
  activeSection: SettingsSection,
  agents: ReadonlyArray<{ id: string; available: boolean }>,
  isBaseUrlValid: boolean,
): boolean {
  if (activeSection !== 'execution') return true;
  if (cfg.mode === 'daemon') {
    return Boolean(
      cfg.agentId && agents.find((a) => a.id === cfg.agentId)?.available,
    );
  }
  return Boolean(cfg.apiKey.trim() && cfg.model.trim() && isBaseUrlValid);
}

/**
 * Returns the config that should actually be persisted by `onSave`.
 *
 * Counterpart to {@link shouldEnableSettingsSave}: when Save is enabled on a
 * non-execution sidebar section but the user's draft execution config is
 * incomplete (e.g. they toggled BYOK on the execution section, never filled
 * in apiKey, then navigated to Language and clicked Save), the raw `cfg`
 * still carries that broken draft. Persisting it would leave the app in an
 * unusable execution state after the modal closes. This helper reverts the
 * execution-related fields to their `initial` values in that case, so saving
 * an unrelated section change never silently commits an incomplete execution
 * mode.
 *
 * Within the execution section, or when execution is already valid, the
 * config passes through unchanged. Issue #739.
 */
export function sanitizeSettingsSavePayload(
  cfg: AppConfig,
  initial: AppConfig,
  activeSection: SettingsSection,
  agents: ReadonlyArray<{ id: string; available: boolean }>,
  isBaseUrlValid: boolean,
): AppConfig {
  if (activeSection === 'execution') return cfg;
  // Reuse the existing execution-section validity gate so the two helpers
  // share one source of truth for "execution config is complete enough."
  const executionValid = shouldEnableSettingsSave(cfg, 'execution', agents, isBaseUrlValid);
  if (executionValid) return cfg;
  return {
    ...cfg,
    mode: initial.mode,
    apiKey: initial.apiKey,
    apiProtocol: initial.apiProtocol,
    apiVersion: initial.apiVersion,
    apiProtocolConfigs: initial.apiProtocolConfigs,
    apiProviderBaseUrl: initial.apiProviderBaseUrl,
    baseUrl: initial.baseUrl,
    model: initial.model,
    agentId: initial.agentId,
    agentCliEnv: initial.agentCliEnv,
    maxTokens: initial.maxTokens,
  };
}

export function switchApiProtocolConfig(
  config: AppConfig,
  protocol: ApiProtocol,
): AppConfig {
  const currentProtocol = config.apiProtocol ?? 'anthropic';
  const apiProtocolConfigs = {
    ...(config.apiProtocolConfigs ?? {}),
    [currentProtocol]: currentApiProtocolConfig(config),
  };
  const nextApiConfig = nextApiProtocolConfig(
    {
      ...config,
      apiProtocolConfigs,
    },
    protocol,
  );
  return applyApiProtocolConfig(
    {
      ...config,
      mode: 'api',
      apiProtocolConfigs,
    },
    protocol,
    nextApiConfig,
  );
}

export function SettingsDialog({
  initial,
  agents,
  agentsLoading = false,
  daemonLive,
  appVersionInfo,
  welcome,
  initialSection = 'execution',
  initialHighlight = null,
  onPersist,
  onPersistComposioKey,
  composioConfigLoading = false,
  onClose,
  onRefreshAgents,
  onAmrLoginStatusChange,
  onSkillsRefresh,
  daemonMediaProviders,
  daemonMediaProvidersFetchState = 'idle',
  mediaProvidersNotice,
  onReloadMediaProviders,
  onProjectsRefresh,
  onSkillsChanged,
  onDesignSystemsChanged,
  providerModelsCache: sharedProviderModelsCache,
  onProviderModelsCacheChange,
}: Props) {
  const { t, locale, setLocale } = useI18n();
  const analytics = useAnalytics();
  // Backfill the fixed-origin base URL on mount too, so a config persisted with
  // an empty baseUrl (e.g. selected AIHubMix before this resolution existed)
  // isn't stuck blocking the live model fetch until the user re-selects the tab.
  const [cfg, setCfg] = useState<AppConfig>(() => ({
    ...initial,
    baseUrl: resolveFixedOriginBaseUrl(initial.apiProtocol ?? 'anthropic', initial.baseUrl),
  }));
  const [maxTokensInput, setMaxTokensInput] = useState(
    initial.maxTokens == null ? '' : String(initial.maxTokens),
  );
  const [pendingMediaProviderEditIds, setPendingMediaProviderEditIds] = useState<
    ReadonlySet<string>
  >(() => new Set());
  const lastSavedAppearanceRef = useRef({
    theme: initial.theme ?? 'system',
    accentColor: resolveAccentColor(initial.accentColor),
  });

  // settings_view — fire on dialog open and on every section switch so the
  // configuration funnel can see which section the user spent time in.
  // The fire is keyed on section so a section bounce (open → switch →
  // close) emits one event per surface.
  const lastViewSectionRef = useRef<string | null>(null);

  useEffect(() => {
    lastSavedAppearanceRef.current = {
      theme: initial.theme ?? 'system',
      accentColor: resolveAccentColor(initial.accentColor),
    };
  }, [initial.theme, initial.accentColor]);

  // Revert the live theme preview to the most recently persisted appearance.
  // That is the initial appearance until autosave succeeds; after autosave,
  // closing Settings must not roll the document back to stale colors.
  useLayoutEffect(() => {
    return () => {
      applyAppearanceToDocument(lastSavedAppearanceRef.current);
    };
  }, []);
  const [showApiKey, setShowApiKey] = useState(false);
  const [activeSection, setActiveSection] = useState<SettingsSection>(initialSection);
  // Scroll the right-hand content pane back to the top whenever the user
  // picks a different settings section. Without this, switching from a
  // long section the user had scrolled (e.g. Library) into a short one
  // (About) keeps the previous scrollTop, so the new section's header
  // can land out of view and the panel reads as half-loaded. Issue #634.
  const settingsContentRef = useRef<HTMLDivElement | null>(null);
  // AMR-card focus, driven by the failed-run nudge (`initialHighlight==='amr'`).
  const amrCardRef = useRef<HTMLDivElement | null>(null);
  // Card pulse: a brief attention flash that auto-clears after a few seconds.
  const [amrHighlightActive, setAmrHighlightActive] = useState(false);
  // Coachmark: persists (unlike the card pulse) until the real pointer reaches
  // the authorize button — so it won't vanish while the user is still moving
  // toward it.
  const [amrCoachmarkArmed, setAmrCoachmarkArmed] = useState(false);
  // The fake-cursor coachmark dismisses as soon as the real pointer reaches the
  // authorize button — once the user has found it, the hint has done its job.
  const [amrCoachmarkDismissed, setAmrCoachmarkDismissed] = useState(false);
  const [agentRescanRunning, setAgentRescanRunning] = useState(false);
  const [agentRescanNotice, setAgentRescanNotice] =
    useState<RescanNotice | null>(null);
  const [agentTestState, setAgentTestState] = useState<TestState>({
    status: 'idle',
  });
  const [amrCardStatus, setAmrCardStatus] = useState<VelaLoginStatus | null>(null);
  const [amrCardStatusReady, setAmrCardStatusReady] = useState(false);
  const [hoveredAgentCardId, setHoveredAgentCardId] = useState<string | null>(null);
  const [providerTestState, setProviderTestState] = useState<TestState>({
    status: 'idle',
  });

  useEffect(() => {
    onAmrLoginStatusChange?.(amrCardStatus);
  }, [amrCardStatus, onAmrLoginStatusChange]);

  useEffect(() => {
    const hasAmrAgent = agents.some((agent) => agent.id === 'amr' && agent.available);
    if (!hasAmrAgent) {
      setAmrCardStatus(null);
      setAmrCardStatusReady(false);
      setHoveredAgentCardId(null);
      return;
    }
    let cancelled = false;
    // Refetch in place on every agents refresh, but do NOT flip
    // `amrCardStatusReady` back to false here. The post-sign-in model-catalog
    // rescan loop hands down a fresh `agents` array on each retry; tearing the
    // pill down to the hidden `--placeholder` between the reset and the async
    // status read made the Sign out action blink out and back on every tick.
    // Readiness latches true after the first read and only resets when AMR
    // becomes unavailable (handled above).
    void fetchVelaLoginStatus().then((next) => {
      if (!cancelled) {
        setAmrCardStatus(next);
        setAmrCardStatusReady(true);
      }
    });
    return () => {
      cancelled = true;
    };
  }, [agents]);

  // Reconcile AMR sign-in state whenever the user returns to the window. The
  // vela device-login flow completes in an external browser / AMR console; if
  // the in-pill poll has already timed out (or the login finished fully
  // out-of-band), the card would otherwise keep showing the stale signed-out
  // state until Settings is closed and reopened. Refetching on focus /
  // visibility keeps the signed-in state, email, and Sign out action live.
  useEffect(() => {
    const hasAmrAgent = agents.some((agent) => agent.id === 'amr' && agent.available);
    if (!hasAmrAgent) return;
    let cancelled = false;
    // Passive read only. Push the daemon's current status down into the card;
    // the pill mirrors it via `initialStatus` (and clears any stale login error
    // when it sees a signed-in status). Do NOT republish the login-state-change
    // event here — that restarts the pill's poll/pending machine on every focus
    // and, while the external browser is stealing and returning focus during a
    // login, ping-pongs the action between "Signing in…" and "Authorize".
    const resyncAmrStatus = () => {
      if (document.visibilityState === 'hidden') return;
      void fetchVelaLoginStatus().then((next) => {
        if (cancelled || !next) return;
        setAmrCardStatus(next);
      });
    };
    window.addEventListener('focus', resyncAmrStatus);
    document.addEventListener('visibilitychange', resyncAmrStatus);
    return () => {
      cancelled = true;
      window.removeEventListener('focus', resyncAmrStatus);
      document.removeEventListener('visibilitychange', resyncAmrStatus);
    };
  }, [agents]);

  useEffect(() => {
    const hasAmrAgent = agents.some((agent) => agent.id === 'amr' && agent.available);
    if (!hasAmrAgent) return;
    let cancelled = false;
    const resyncAmrStatus = (event: Event) => {
      const reason = amrLoginStatusEventReason(event);
      if (reason === 'login-canceled') return;
      void fetchVelaLoginStatus().then((next) => {
        if (cancelled || !next) return;
        setAmrCardStatus(next);
        setAmrCardStatusReady(true);
      });
    };
    window.addEventListener(AMR_LOGIN_STATUS_EVENT, resyncAmrStatus);
    return () => {
      cancelled = true;
      window.removeEventListener(AMR_LOGIN_STATUS_EVENT, resyncAmrStatus);
    };
  }, [agents]);
  const [byokPreconditionNotice, setByokPreconditionNotice] = useState<{
    action: ByokPreconditionAction;
    field?: ByokRequiredField;
    message: string;
  } | null>(null);
  const [providerModelsState, setProviderModelsState] =
    useState<ProviderModelsState>({ status: 'idle' });
  const [localProviderModelsCache, setLocalProviderModelsCache] =
    useState<ProviderModelsCache>({});
  const hasSharedProviderModelsCache =
    Boolean(sharedProviderModelsCache) && Boolean(onProviderModelsCacheChange);
  const activeProviderModelsCache =
    hasSharedProviderModelsCache
      ? sharedProviderModelsCache!
      : localProviderModelsCache;
  const activeSetProviderModelsCache =
    hasSharedProviderModelsCache
      ? onProviderModelsCacheChange!
      : setLocalProviderModelsCache;
  const [providerModelsCommittedKey, setProviderModelsCommittedKey] =
    useState<string | null>(() => {
      const protocol = initial.apiProtocol ?? 'anthropic';
      if (
        initial.mode !== 'api' ||
        protocol === 'azure' ||
        protocol === 'ollama' ||
        missingByokModelFetchFields(initial, protocol).length > 0 ||
        !isValidApiBaseUrl(initial.baseUrl)
      ) {
        return null;
      }
      return providerModelsCacheKey(
        protocol,
        initial.baseUrl,
        initial.apiKey,
        initial.apiVersion ?? '',
      );
    });
  const agentTestAbortRef = useRef<AbortController | null>(null);
  const providerTestAbortRef = useRef<AbortController | null>(null);
  const providerModelsAbortRef = useRef<AbortController | null>(null);
  const pendingAgentInstallRescanRef = useRef(false);
  // Guards the AMR catalog-chase loop so concurrent renders can't start it
  // twice (see the re-detect effect below).
  const amrRescanInFlightRef = useRef(false);
  const agentTestRevisionRef = useRef(0);
  const providerTestRevisionRef = useRef(0);
  const providerModelsRevisionRef = useRef(0);
  const providerTestFirstResetRef = useRef(true);
  const providerModelsFirstResetRef = useRef(true);
  const deferAfterKeyCleanRef = useRef(false);
  const providerAutoTestKeyRef = useRef<string | null>(null);
  const byokLastUnsuccessfulTestKeyRef = useRef<string | null>(null);
  const apiKeyInputRef = useRef<HTMLInputElement | null>(null);
  const baseUrlInputRef = useRef<HTMLInputElement | null>(null);
  const modelSelectRef = useRef<HTMLButtonElement | null>(null);
  const customModelInputRef = useRef<HTMLInputElement | null>(null);
  const focusByokRequiredFieldAfterProtocolSwitchRef = useRef(false);
  // Tracks whether the current BYOK model value came from an explicit user
  // pick (combobox selection or custom entry) rather than an auto-populated
  // provider preset. The account-model auto-switch must never overwrite a
  // deliberate choice, even when that choice equals the provider preset id.
  const apiModelUserSelectedRef = useRef(false);
  const [apiModelCustomEditing, setApiModelCustomEditing] = useState(false);
  const [agentCustomModelIds, setAgentCustomModelIds] = useState<
    ReadonlySet<string>
  >(() => new Set());
  const [versionChecking, setVersionChecking] = useState(false);
  const [aboutToast, setAboutToast] = useState<string | null>(null);

  const handleInstallLatest = useCallback(async () => {
    if (versionChecking || !appVersionInfo) return;
    setVersionChecking(true);
    try {
      const release = await fetchLatestGithubReleaseInfo();
      const latestTag = (release?.tagName ?? '').replace(/^v/, '');
      if (release?.stale !== true && latestTag && latestTag === appVersionInfo.version) {
        setAboutToast(t('settings.alreadyLatest'));
        return;
      }
    } catch {
      // network error — fall through to open releases page
    } finally {
      setVersionChecking(false);
    }
    window.open('https://github.com/nexu-io/open-design/releases', '_blank', 'noopener,noreferrer');
  }, [versionChecking, appVersionInfo, t]);

  // Imperative handle for the External MCP section. The dialog footer Save
  // routes through this when the MCP tab is active so the user can press the
  // single Save button at the bottom instead of hunting for the inner one.
  useEffect(() => {
    setActiveSection(initialSection);
  }, [initialSection]);

  // settings_view — fires whenever the active section changes (and once on
  // mount). Keying the fire on a section+section-string lets us dedupe
  // accidental double-renders while still capturing genuine tab switches.
  useEffect(() => {
    if (lastViewSectionRef.current === activeSection) return;
    lastViewSectionRef.current = activeSection;
    // v2 settings_view collapses to `{ page=settings, area }`; the
    // execution_mode / has_available_cli / selected_cli_id signal that v1
    // tagged onto every view now lives in the configure-state global
    // properties (registered once and inherited by every event).
    trackSettingsView(analytics.track, {
      page_name: 'settings',
      area: settingsSectionToTracking(activeSection),
    });
  }, [activeSection, analytics.track]);
  useEffect(() => {
    const el = settingsContentRef.current;
    if (el) el.scrollTop = 0;
  }, [activeSection]);

  // One-shot AMR-card focus from the failed-run nudge: scroll the card into
  // view (on the next frame, so it wins over the section's scrollTop reset
  // above) and play a brief highlight + arm the sign-in coachmark. The
  // coachmark only actually shows when the AMR card reports a signed-out state
  // (`amrCardStatus?.loggedIn === false`). If the execution pane is in API mode
  // the AMR card is absent and this no-ops.
  useEffect(() => {
    if (initialHighlight !== 'amr' || activeSection !== 'execution') return;
    let cancelled = false;
    const raf = requestAnimationFrame(() => {
      if (cancelled) return;
      amrCardRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
      setAmrCoachmarkDismissed(false);
      setAmrHighlightActive(true);
      setAmrCoachmarkArmed(true);
    });
    // Only the card pulse auto-clears; the coachmark persists until the pointer
    // reaches the authorize button (or the user signs in).
    const clear = setTimeout(() => {
      if (!cancelled) setAmrHighlightActive(false);
    }, 3200);
    return () => {
      cancelled = true;
      cancelAnimationFrame(raf);
      clearTimeout(clear);
    };
  }, [initialHighlight, activeSection]);

  const selectedMemoryChatAgent =
    cfg.mode === 'daemon' && cfg.agentId
      ? agents.find((agent) => agent.id === cfg.agentId) ?? null
      : null;
  const selectedMemoryChatModel =
    cfg.mode === 'daemon' && cfg.agentId
      ? cfg.agentModels?.[cfg.agentId]?.model
      ?? selectedMemoryChatAgent?.models?.[0]?.id
      ?? null
    : null;
  const agentChoiceForTest =
    cfg.mode === 'daemon' && cfg.agentId
      ? cfg.agentModels?.[cfg.agentId]
      : null;
  useEffect(() => {
    agentTestRevisionRef.current += 1;
    setAgentTestState((state) =>
      state.status === 'running' ? state : { status: 'idle' },
    );
  }, [
    cfg.agentId,
    agentChoiceForTest?.model,
    agentChoiceForTest?.reasoning,
    cfg.agentCliEnv,
  ]);
  // Rescan notices are list-level feedback for a one-shot action and
  // shouldn't linger in the content stream. After 6s, fade them out so
  // repeated Rescan clicks don't pile up; the next click resets the
  // notice immediately, so this only affects "user moved on" cases.
  useEffect(() => {
    if (!agentRescanNotice) return;
    const id = window.setTimeout(() => setAgentRescanNotice(null), 6000);
    return () => window.clearTimeout(id);
  }, [agentRescanNotice]);
  useEffect(() => {
    if (providerTestFirstResetRef.current) {
      providerTestFirstResetRef.current = false;
      return;
    }
    providerTestRevisionRef.current += 1;
    providerAutoTestKeyRef.current = null;
    setByokPreconditionNotice(null);
    setProviderTestState((state) =>
      state.status === 'running' ? state : { status: 'idle' },
    );
  }, [
    cfg.apiProtocol,
    cfg.apiKey,
    cfg.baseUrl,
    cfg.model,
    cfg.apiVersion,
  ]);
  useEffect(() => {
    if (providerModelsFirstResetRef.current) {
      providerModelsFirstResetRef.current = false;
      return;
    }
    providerModelsRevisionRef.current += 1;
    providerModelsAbortRef.current?.abort();
    providerModelsAbortRef.current = null;
    setProviderModelsCommittedKey(null);
    setByokPreconditionNotice(null);
    setProviderModelsState({ status: 'idle' });
  }, [
    cfg.apiProtocol,
    cfg.apiKey,
    cfg.baseUrl,
    cfg.apiVersion,
  ]);
  // Releasing the abort controllers on unmount avoids the "setState after
  // unmount" warning if the dialog closes while a test is still running.
  useEffect(() => {
    return () => {
      agentTestAbortRef.current?.abort();
      providerTestAbortRef.current?.abort();
      providerModelsAbortRef.current?.abort();
    };
  }, []);

  const installedCount = useMemo(
    () => agents.filter((a) => a.available).length,
    [agents],
  );

  const setMode = (mode: ExecMode) => {
    setCfg((c) => {
      const modeBefore = executionModeToTracking(c.mode);
      const modeAfter = executionModeToTracking(mode);
      if (modeBefore !== modeAfter) {
        trackSettingsExecutionModeTabClick(analytics.track, {
          page_name: 'settings',
          area: 'configure_execution_mode',
          element: 'execution_mode_tab',
          action: 'switch_execution_mode',
          mode_before: modeBefore,
          mode_after: modeAfter,
        });
      }
      return { ...c, mode };
    });
  };
  const setApiProtocol = (protocol: ApiProtocol) => {
    setApiModelCustomEditing(false);
    apiModelUserSelectedRef.current = false;
    focusByokRequiredFieldAfterProtocolSwitchRef.current = true;
    setCfg((c) => switchApiProtocolConfig(c, protocol));
  };
  const updateApiConfig = (patch: Partial<ApiProtocolConfig>) =>
    setCfg((c) => updateCurrentApiProtocolConfig(c, patch));
  const updateMaxTokensInput = (raw: string) => {
    setMaxTokensInput(raw);
    const trimmed = raw.trim();
    if (trimmed === '') {
      setCfg((c) => ({ ...c, maxTokens: undefined }));
      return;
    }
    const value = Number(trimmed);
    const nextMaxTokens =
      Number.isInteger(value) &&
      value >= MIN_MAX_TOKENS &&
      value <= MAX_MAX_TOKENS
        ? value
        : undefined;
    setCfg((c) => ({ ...c, maxTokens: nextMaxTokens }));
  };
  const markAgentInstallIntent = () => {
    pendingAgentInstallRescanRef.current = true;
  };
  const handleRefreshAgents = async () => {
    if (agentRescanRunning) return;
    setAgentRescanRunning(true);
    setAgentRescanNotice(null);
    try {
      const refreshed = await onRefreshAgents(agentRefreshOptionsForConfig(cfg));
      const nextAgents = Array.isArray(refreshed) ? refreshed : agents;
      setAgentRescanNotice({
        kind: 'success',
        count: nextAgents.filter((a) => a.available).length,
      });
    } catch {
      setAgentRescanNotice({ kind: 'error' });
    } finally {
      setAgentRescanRunning(false);
    }
  };
  const openAgentFixUrl = (url: string | undefined) => {
    const href = sanitizeHttpsUrl(url);
    if (!href) return;
    markAgentInstallIntent();
    void openExternalUrl(href);
  };
  const diagnosticHandlersForAgent = (agent: AgentInfo) => {
    const docsUrl = sanitizeHttpsUrl(agent.docsUrl);
    const installUrl = sanitizeHttpsUrl(agent.installUrl);
    return {
      onRescan: () => void handleRefreshAgents(),
      ...(docsUrl ? { onOpenDocs: () => openAgentFixUrl(docsUrl) } : {}),
      ...(installUrl ? { onOpenInstall: () => openAgentFixUrl(installUrl) } : {}),
    };
  };
  useEffect(() => {
    const handleReturnToSettings = () => {
      if (
        !pendingAgentInstallRescanRef.current ||
        agentRescanRunning ||
        document.visibilityState === 'hidden'
      ) {
        return;
      }
      pendingAgentInstallRescanRef.current = false;
      void handleRefreshAgents();
    };
    document.addEventListener('visibilitychange', handleReturnToSettings);
    window.addEventListener('focus', handleReturnToSettings);
    return () => {
      document.removeEventListener('visibilitychange', handleReturnToSettings);
      window.removeEventListener('focus', handleReturnToSettings);
    };
  }, [agentRescanRunning, handleRefreshAgents]);

  // Chase AMR's live model catalog whenever the user is signed in but the
  // model list hasn't arrived yet. AMR is detected at app start (often while
  // signed out, so it comes back with an empty, fail-closed list), and the
  // live `vela models` catalog only becomes fetchable once the credential
  // lands — and can lag the credential write by a beat. We must cover every
  // way Settings ends up "signed in + empty", not just an in-Settings
  // sign-in edge: onboarding signs in and re-detects exactly once, so if that
  // single call lands during the propagation window Settings later mounts
  // already signed in with an empty list. Keying on `loggedIn === true` +
  // "AMR has no models" handles both; the picker shows its loading state
  // (see renderAgentModelConfig) until the catalog fills in.
  //
  // `onRefreshAgents` / `agents` are read through refs so re-detecting (which
  // changes their identity) can't tear the retry loop down mid-flight — that
  // is what made the loading row flash and vanish before the catalog arrived.
  // The in-flight ref keeps a single loop running across renders.
  const onRefreshAgentsRef = useRef(onRefreshAgents);
  onRefreshAgentsRef.current = onRefreshAgents;
  const agentsRef = useRef(agents);
  agentsRef.current = agents;
  useEffect(() => {
    if (amrCardStatus?.loggedIn !== true) return;
    const amr = agentsRef.current.find((agent) => agent.id === 'amr');
    if (!amr || (amr.models?.length ?? 0) > 0) return;
    if (amrRescanInFlightRef.current) return;
    amrRescanInFlightRef.current = true;
    let cancelled = false;
    void (async () => {
      try {
        for (
          let attempt = 0;
          attempt < AMR_SIGN_IN_RESCAN_ATTEMPTS && !cancelled;
          attempt += 1
        ) {
          let next: void | AgentInfo[];
          try {
            next = await onRefreshAgentsRef.current();
          } catch {
            return;
          }
          if (cancelled) return;
          const detected = Array.isArray(next) ? next : [];
          const refreshed = detected.find((agent) => agent.id === 'amr');
          // Stop once the live catalog has caught up (or AMR vanished); a
          // still-empty list means vela hasn't published the catalog yet, so
          // retry.
          if (!refreshed || (refreshed.models?.length ?? 0) > 0) return;
          await new Promise((resolve) => {
            setTimeout(resolve, AMR_SIGN_IN_RESCAN_RETRY_MS);
          });
        }
      } finally {
        amrRescanInFlightRef.current = false;
      }
    })();
    return () => {
      cancelled = true;
      amrRescanInFlightRef.current = false;
    };
  }, [amrCardStatus?.loggedIn]);

  const handleTestAgent = async () => {
    if (agentTestState.status === 'running') {
      return;
    }
    const selected = agents.find((a) => a.id === cfg.agentId && a.available);
    if (!selected) return;
    const choice = cfg.agentModels?.[selected.id] ?? {};
    const controller = new AbortController();
    const revision = agentTestRevisionRef.current;
    agentTestAbortRef.current = controller;
    setAgentTestState({ status: 'running' });
    const startedAt = performance.now();
    const cliProviderId = agentIdToTracking(selected.id);
    const clearIfStale = () => {
      if (agentTestAbortRef.current === controller) {
        setAgentTestState({ status: 'idle' });
      }
    };
    try {
      const result = await testAgent(
        {
          agentId: selected.id,
          model: choice.model || undefined,
          reasoning: choice.reasoning || undefined,
          agentCliEnv: cfg.agentCliEnv ?? {},
        },
        controller.signal,
      );
      if (controller.signal.aborted) return;
      if (agentTestRevisionRef.current !== revision) {
        clearIfStale();
        return;
      }
      setAgentTestState({ status: 'done', result });
      trackSettingsCliTestResult(analytics.track, {
        page_name: 'settings',
        area: 'configure_execution_mode',
        cli_provider_id: cliProviderId,
        result: result.ok ? 'success' : 'failed',
        ...(result.ok ? {} : { error_code: result.kind || 'UNKNOWN' }),
        duration_ms: Math.round(performance.now() - startedAt),
      });
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') return;
      if (agentTestRevisionRef.current !== revision) {
        clearIfStale();
        return;
      }
      setAgentTestState({
        status: 'done',
        result: {
          ok: false,
          kind: 'unknown',
          latencyMs: 0,
          model: choice.model || 'default',
          detail: err instanceof Error ? err.message : 'Test request failed',
        },
      });
      trackSettingsCliTestResult(analytics.track, {
        page_name: 'settings',
        area: 'configure_execution_mode',
        cli_provider_id: cliProviderId,
        result: 'failed',
        error_code: err instanceof Error ? err.name : 'UNKNOWN',
        duration_ms: Math.round(performance.now() - startedAt),
      });
    } finally {
      if (agentTestAbortRef.current === controller) {
        agentTestAbortRef.current = null;
      }
    }
  };

  const handleTestProvider = async (
    options: { silentPreconditions?: boolean } = {},
  ) => {
    if (providerTestState.status === 'running') {
      return;
    }
    const blockingIssues = blockingByokDraftIssues(byokDraftValidation);
    const hasFirstPartyHostTypo = Boolean(byokFirstPartyBaseUrl?.hostTypo);
    const currentConfigKey = providerConnectionTestKey(apiProtocol, cfg);
    const lastUnsuccessfulConfigKey = byokLastUnsuccessfulTestKeyRef.current;
    const configKeyChanged = lastUnsuccessfulConfigKey !== null &&
      lastUnsuccessfulConfigKey !== currentConfigKey;
    if (hasFirstPartyHostTypo) {
      if (!options.silentPreconditions) {
        setByokPreconditionNotice({
          action: 'test',
          field: 'base_url',
          message: t('settings.testInvalidBaseUrl'),
        });
        focusByokRequiredField('base_url');
      }
      byokLastUnsuccessfulTestKeyRef.current = currentConfigKey;
      return;
    }
    if (blockingIssues.length > 0) {
      if (options.silentPreconditions) {
        return;
      }
      showByokDraftValidationNotice('test', byokDraftValidation);
      const byokProviderId = byokProtocolToTracking(apiProtocol);
      if (byokProviderId) {
        trackSettingsByokTestResult(analytics.track, {
          page_name: 'settings',
          area: 'execution_model',
          provider_id: byokProviderId,
          result: 'failed',
          error_code: byokErrorKindFromIssues(blockingIssues),
          error_kind: byokErrorKindFromIssues(blockingIssues),
          field_missing: byokFieldMissingFromIssues(blockingIssues),
          config_key_changed: configKeyChanged,
          success_after_action: false,
          duration_ms: 0,
        });
      }
      byokLastUnsuccessfulTestKeyRef.current = currentConfigKey;
      return;
    }
    const controller = new AbortController();
    const revision = providerTestRevisionRef.current;
    providerTestAbortRef.current = controller;
    setProviderTestState({ status: 'running' });
    const startedAt = performance.now();
    const clearIfStale = () => {
      if (providerTestAbortRef.current === controller) {
        setProviderTestState({ status: 'idle' });
      }
    };
    try {
      const result = await testApiProvider(
        {
          protocol: apiProtocol,
          baseUrl: cfg.baseUrl,
          apiKey: cleanByokApiKey(cfg.apiKey),
          model: cfg.model,
          apiVersion:
            apiProtocol === 'azure'
              ? cfg.apiVersion?.trim() || undefined
              : undefined,
        },
        controller.signal,
      );
      if (controller.signal.aborted) return;
      if (providerTestRevisionRef.current !== revision) {
        clearIfStale();
        return;
      }
      setProviderTestState({ status: 'done', result });
      if (!result.ok && result.kind === 'not_found_model') {
        focusByokRequiredField('model');
      }
      const byokProviderId = byokProtocolToTracking(apiProtocol);
      if (byokProviderId) {
        trackSettingsByokTestResult(analytics.track, {
          page_name: 'settings',
          area: 'execution_model',
          provider_id: byokProviderId,
          result: byokTrackingTestResult(result),
          ...(result.ok ? {} : { error_code: result.kind || 'UNKNOWN' }),
          ...(result.ok ? {} : { error_kind: result.kind || 'UNKNOWN' }),
          field_missing: 'none',
          config_key_changed: configKeyChanged,
          success_after_action: result.ok && configKeyChanged,
          duration_ms: Math.round(performance.now() - startedAt),
        });
      }
      byokLastUnsuccessfulTestKeyRef.current = result.ok ? null : currentConfigKey;
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') return;
      if (providerTestRevisionRef.current !== revision) {
        clearIfStale();
        return;
      }
      setProviderTestState({
        status: 'done',
        result: {
          ok: false,
          kind: 'unknown',
          latencyMs: 0,
          model: cfg.model,
          detail: err instanceof Error ? err.message : 'Test request failed',
        },
      });
      const byokProviderId = byokProtocolToTracking(apiProtocol);
      if (byokProviderId) {
        trackSettingsByokTestResult(analytics.track, {
          page_name: 'settings',
          area: 'execution_model',
          provider_id: byokProviderId,
          result: 'failed',
          error_code: err instanceof Error ? err.name : 'UNKNOWN',
          error_kind: err instanceof Error ? err.name : 'UNKNOWN',
          field_missing: 'none',
          config_key_changed: configKeyChanged,
          success_after_action: false,
          duration_ms: Math.round(performance.now() - startedAt),
        });
      }
      byokLastUnsuccessfulTestKeyRef.current = currentConfigKey;
    } finally {
      if (providerTestAbortRef.current === controller) {
        providerTestAbortRef.current = null;
      }
    }
  };

  const handleAutoTestProvider = () => {
    if (providerTestState.status === 'running') {
      return;
    }
    if (byokFirstPartyBaseUrl?.hostTypo) {
      return;
    }
    if (blockingByokDraftIssues(byokDraftValidation).length > 0) {
      return;
    }
    const key = providerConnectionTestKey(apiProtocol, cfg);
    if (providerAutoTestKeyRef.current === key) {
      return;
    }
    providerAutoTestKeyRef.current = key;
    void handleTestProvider({ silentPreconditions: true });
  };

  const handleFetchProviderModels = async (
    options: { silent?: boolean; trigger?: 'auto' | 'manual' } = {},
  ) => {
    const trigger = options.trigger ?? (options.silent ? 'auto' : 'manual');
    const byokProviderId = byokProtocolToTracking(apiProtocol);
    const trackModelsFetchResult = (
      props: Omit<
        Parameters<typeof trackSettingsByokModelsFetchResult>[1],
        'page_name' | 'area' | 'provider_id' | 'trigger' | 'source'
      >,
      source: 'network' | 'cache' = 'network',
    ) => {
      if (!byokProviderId) return;
      trackSettingsByokModelsFetchResult(analytics.track, {
        page_name: 'settings',
        area: 'configure_execution_mode_byok',
        provider_id: byokProviderId,
        trigger,
        source,
        ...props,
      });
    };
    if (providerModelsState.status === 'running') {
      return;
    }
    if (apiProtocol === 'azure') {
      trackModelsFetchResult({
        result: 'failed',
        error_code: 'unsupported_azure',
        error_kind: 'unsupported_azure',
        duration_ms: 0,
      });
      if (!options.silent) {
        setByokPreconditionNotice({
          action: 'test',
          message: t('settings.fetchModelsUnsupportedAzure'),
        });
      }
      return;
    }
    if (apiProtocol === 'ollama') {
      trackModelsFetchResult({
        result: 'failed',
        error_code: 'unsupported_ollama',
        error_kind: 'unsupported_ollama',
        duration_ms: 0,
      });
      if (!options.silent) {
        setByokPreconditionNotice({
          action: 'test',
          message: t('settings.fetchModelsUnsupportedOllama'),
        });
      }
      return;
    }
    const modelFetchBlockingIssues = blockingByokDraftIssues(
      byokModelFetchDraftValidation,
    );
    if (byokFirstPartyBaseUrl?.hostTypo) {
      if (!options.silent) {
        setByokPreconditionNotice({
          action: 'test',
          field: 'base_url',
          message: t('settings.testInvalidBaseUrl'),
        });
        focusByokRequiredField('base_url');
      }
      return;
    }
    if (modelFetchBlockingIssues.length > 0) {
      trackModelsFetchResult({
        result: 'failed',
        error_code: byokErrorKindFromIssues(modelFetchBlockingIssues),
        error_kind: byokErrorKindFromIssues(modelFetchBlockingIssues),
        field_missing: byokFieldMissingFromIssues(modelFetchBlockingIssues),
        duration_ms: 0,
      });
      if (!options.silent) {
        showByokDraftValidationNotice('test', byokModelFetchDraftValidation);
      }
      return;
    }
    const cacheKey = providerModelsCacheKey(
      apiProtocol,
      cfg.baseUrl,
      cfg.apiKey,
      cfg.apiVersion ?? '',
    );
    const cachedModels = activeProviderModelsCache[cacheKey];
    if (cachedModels) {
      trackModelsFetchResult(
        {
          result: 'success',
          model_count: cachedModels.length,
          duration_ms: 0,
        },
        'cache',
      );
      setProviderModelsState({
        status: 'done',
        cacheKey,
        result: {
          ok: true,
          kind: 'success',
          latencyMs: 0,
          models: cachedModels,
        },
      });
      return;
    }
    const controller = new AbortController();
    const revision = providerModelsRevisionRef.current;
    providerModelsAbortRef.current = controller;
    setProviderModelsState({ status: 'running', cacheKey });
    const startedAt = performance.now();
    const clearIfStale = () => {
      if (providerModelsAbortRef.current === controller) {
        setProviderModelsState({ status: 'idle' });
      }
    };
    try {
      const result = await fetchProviderModels(
        {
          protocol: apiProtocol,
          baseUrl: cfg.baseUrl,
          apiKey: cleanByokApiKey(cfg.apiKey),
        },
        controller.signal,
      );
      if (controller.signal.aborted) return;
      if (providerModelsRevisionRef.current !== revision) {
        clearIfStale();
        return;
      }
      if (result.ok && result.models?.length) {
        activeSetProviderModelsCache((prev) => ({
          ...prev,
          [cacheKey]: result.models ?? [],
        }));
      }
      trackModelsFetchResult({
        result: result.ok ? 'success' : 'failed',
        ...(result.ok ? {} : { error_code: result.kind || 'UNKNOWN' }),
        ...(result.ok ? {} : { error_kind: result.kind || 'UNKNOWN' }),
        model_count: result.ok ? result.models?.length ?? 0 : 0,
        duration_ms: Math.round(performance.now() - startedAt),
      });
      setProviderModelsState({ status: 'done', cacheKey, result });
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') return;
      if (providerModelsRevisionRef.current !== revision) {
        clearIfStale();
        return;
      }
      setProviderModelsState({
        status: 'done',
        cacheKey,
        result: {
          ok: false,
          kind: 'unknown',
          latencyMs: 0,
          detail: err instanceof Error ? err.message : 'Model list request failed',
        },
      });
      trackModelsFetchResult({
        result: 'failed',
        error_code: err instanceof Error ? err.name : 'UNKNOWN',
        error_kind: err instanceof Error ? err.name : 'UNKNOWN',
        model_count: 0,
        duration_ms: Math.round(performance.now() - startedAt),
      });
    } finally {
      if (providerModelsAbortRef.current === controller) {
        providerModelsAbortRef.current = null;
      }
    }
  };

  const renderTestMessage = (
    result: ConnectionTestResponse,
    kindForSuccess: 'api' | 'cli',
  ): string => {
    const ms = Math.max(0, Math.round(result.latencyMs));
    const sample = result.sample ?? '';
    const agentName = result.agentName ?? '';
    const testedModel = result.model ?? cfg.model;
    if (result.ok) {
      const baseMessage = kindForSuccess === 'api'
        ? t('settings.testSuccessApi', { ms, sample })
        : t('settings.testSuccessCli', { agentName, ms, sample });
      if (kindForSuccess === 'cli' && cfg.agentId === 'codex') {
        const codexStrings = codexPathStrings(locale);
        if (
          result.usedExecutableSource === 'configured' &&
          result.configuredExecutablePath
        ) {
          return `${baseMessage} ${codexStrings.configuredSuccess(result.configuredExecutablePath)}`;
        }
        if (
          result.usedExecutableSource === 'fallback_invalid' &&
          result.configuredExecutablePath &&
          result.detectedExecutablePath
        ) {
          return `${baseMessage} ${codexStrings.invalidFallback(
            result.configuredExecutablePath,
            result.detectedExecutablePath,
          )}`;
        }
        if (
          result.usedExecutableSource === 'fallback_failed' &&
          result.configuredExecutablePath &&
          result.detectedExecutablePath
        ) {
          return `${baseMessage} ${codexStrings.failedFallback(
            result.configuredExecutablePath,
            result.detectedExecutablePath,
          )}`;
        }
      }
      return result.detail ? `${baseMessage} ${result.detail}` : baseMessage;
    }
    switch (result.kind) {
      case 'auth_failed':
        return t('settings.testAuthFailed');
      case 'forbidden':
        return t('settings.testForbidden');
      case 'not_found_model':
        return t('settings.testNotFoundModel', { model: testedModel });
      case 'invalid_model_id':
        return t('settings.testInvalidModelId', { model: testedModel });
      case 'invalid_base_url':
        return t('settings.testInvalidBaseUrl');
      case 'rate_limited':
        return t('settings.testRateLimited');
      case 'upstream_unavailable':
        return t('settings.testUpstream', { status: result.status ?? 0 });
      case 'timeout':
        return t('settings.testTimeout', { ms });
      case 'agent_not_installed':
        return t('settings.testAgentMissing', { agentName });
      case 'agent_auth_required':
        return result.detail || 'Agent authentication is required.';
      case 'agent_spawn_failed':
        return t('settings.testAgentSpawn', {
          agentName,
          detail: result.detail ?? '',
        });
      default:
        return t('settings.testUnknown', { detail: result.detail ?? '' });
    }
  };

  const applyCodexDetectedPath = (detectedPath: string) => {
    setCfg((c) => updateAgentCliEnvValue(c, 'codex', 'CODEX_BIN', detectedPath));
    setAgentTestState({ status: 'idle' });
  };

  const clearCodexCustomPath = () => {
    setCfg((c) => updateAgentCliEnvValue(c, 'codex', 'CODEX_BIN', ''));
    setAgentTestState({ status: 'idle' });
  };

  const apiProtocol = cfg.apiProtocol ?? 'anthropic';
  const apiKeyConsoleLink = API_KEY_CONSOLE_LINKS[apiProtocol];
  const apiProtocolTabGroups = [
    {
      id: 'protocols',
      label: t('settings.protocolGroupProtocols'),
      tabs: API_PROTOCOL_TABS.filter((tab) => !GATEWAY_API_PROTOCOLS.has(tab.id)),
    },
    {
      id: 'gateways',
      label: t('settings.protocolGroupGateways'),
      tabs: API_PROTOCOL_TABS.filter((tab) => GATEWAY_API_PROTOCOLS.has(tab.id)),
    },
  ];
  const baseUrlValid = isValidApiBaseUrl(cfg.baseUrl);
  const baseUrlInvalid = Boolean(cfg.baseUrl.trim() && !baseUrlValid);
  const byokRequiredLabel = (field: ByokRequiredField): string => {
    switch (field) {
      case 'api_key':
        return t('settings.apiKey');
      case 'base_url':
        return t('settings.baseUrl');
      case 'model':
        return apiProtocol === 'azure'
          ? t('settings.azureDeploymentModel')
          : t('settings.model');
      default: {
        const exhaustive: never = field;
        return exhaustive;
      }
    }
  };
  const formatByokMissingFields = (fields: ByokRequiredField[]): string =>
    fields.map(byokRequiredLabel).join(', ');
  const focusByokRequiredField = (field: ByokRequiredField | undefined) => {
    if (!field) return;
    window.setTimeout(() => {
      if (field === 'api_key') {
        apiKeyInputRef.current?.focus();
        return;
      }
      if (field === 'base_url') {
        baseUrlInputRef.current?.focus();
        return;
      }
      if (customModelInputRef.current) {
        customModelInputRef.current.focus();
        return;
      }
      modelSelectRef.current?.focus();
    }, 0);
  };
  const showByokPreconditionNotice = (
    action: ByokPreconditionAction,
    fields: ByokRequiredField[],
  ) => {
    setByokPreconditionNotice({
      action,
      message: t('settings.testMissingFields', {
        fields: formatByokMissingFields(fields),
      }),
    });
    focusByokRequiredField(fields[0]);
  };
  const byokDraftIssueMessage = (issue: ByokDraftIssue): string => {
    switch (issue.code) {
      case 'api_key_required':
      case 'base_url_required':
      case 'model_required':
        return t('settings.testMissingFields', {
          fields: byokRequiredLabel(issue.field),
        });
      case 'api_key_extra_whitespace':
      case 'api_key_malformed':
      case 'api_key_wrong_protocol':
        return t('settings.apiKeyInvalid');
      case 'base_url_invalid':
        return t('settings.baseUrlInvalid');
      default: {
        const exhaustive: never = issue.code;
        return exhaustive;
      }
    }
  };
  const showByokDraftValidationNotice = (
    action: ByokPreconditionAction,
    validation: ByokDraftValidation,
  ) => {
    const blockingFields = blockingByokDraftFields(validation);
    if (blockingFields.length === 0) return;
    const blockingIssues = blockingByokDraftIssues(validation);
    const missingFields = blockingIssues
      .filter((issue) =>
        issue.code === 'api_key_required' ||
        issue.code === 'base_url_required' ||
        issue.code === 'model_required'
      )
      .map((issue) => issue.field);
    if (missingFields.length > 0) {
      showByokPreconditionNotice(action, missingFields);
      return;
    }
    const firstIssue = blockingIssues[0];
    if (!firstIssue) return;
    setByokPreconditionNotice({
      action,
      field: firstIssue.field,
      message: byokDraftIssueMessage(firstIssue),
    });
    focusByokRequiredField(firstIssue.field);
  };
  // Autosave loop. Every committed edit to `cfg` schedules a debounced
  // sync to localStorage + the daemon. We keep a 400ms debounce so rapid
  // typing in text fields doesn't flood the daemon with PUTs while still
  // feeling near-instant for toggles/selects (which fire once and settle).
  // The Composio API key field is intentionally excluded from this loop —
  // see ConnectorSection for the explicit "Save key" gesture.
  // The status here drives the footer indicator: 'idle' = no draft to
  // flush, 'pending' = scheduled, 'saving' = request in flight, 'saved'
  // = recent successful sync, 'error' = recent failure.
  const [autosaveStatus, setAutosaveStatus] =
    useState<'idle' | 'pending' | 'saving' | 'saved' | 'error'>('idle');
  // Skip the very first effect tick so just opening the dialog doesn't
  // appear to "save" anything before the user has touched a field.
  const autosaveSkipFirstRef = useRef(true);
  const autosaveTimerRef = useRef<number | null>(null);
  const autosaveSavedTimerRef = useRef<number | null>(null);
  const autosaveRetryTimerRef = useRef<number | null>(null);
  const autosavePendingFlushRef = useRef(false);
  const autosaveLatestRef = useRef<AppConfig>(cfg);
  // Baseline used by the draft-only detector: the snapshot at the most
  // recent successful autosave (or the initial cfg on mount). Compared
  // against the current snapshot to decide whether the only edits
  // since last save are intentionally-stripped fields like the
  // Composio API key — in which case we must NOT flash "All changes
  // saved", because the draft has not actually been persisted.
  const autosaveLastSavedRef = useRef<AppConfig>(cfg);
  const mediaProvidersChangeVersionRef = useRef(0);
  const lastSyncedMediaProvidersVersionRef = useRef(0);
  const [autosaveRetryTick, setAutosaveRetryTick] = useState(0);
  autosaveLatestRef.current = cfg;
  useEffect(() => {
    if (autosaveSkipFirstRef.current) {
      autosaveSkipFirstRef.current = false;
      autosaveLastSavedRef.current = cfg;
      return;
    }
    setAutosaveStatus('pending');
    if (autosaveSavedTimerRef.current != null) {
      window.clearTimeout(autosaveSavedTimerRef.current);
      autosaveSavedTimerRef.current = null;
    }
    if (autosaveRetryTimerRef.current != null) {
      window.clearTimeout(autosaveRetryTimerRef.current);
      autosaveRetryTimerRef.current = null;
    }
    if (autosaveTimerRef.current != null) {
      window.clearTimeout(autosaveTimerRef.current);
    }
    autosavePendingFlushRef.current = true;
    autosaveTimerRef.current = window.setTimeout(() => {
      autosavePendingFlushRef.current = false;
      autosaveTimerRef.current = null;
      const snapshot = autosaveLatestRef.current;
      const mediaProvidersVersion = mediaProvidersChangeVersionRef.current;
      const persistOptions = {
        forceMediaProviderSync: mediaProvidersVersion > lastSyncedMediaProvidersVersionRef.current,
      };
      // Draft-only edit (e.g. the user is mid-typing the Composio API
      // key, which only commits via the explicit "Save key" gesture):
      // the persisted shape would be identical to what is already on
      // disk, so a save would be a no-op that mis-reports "Saved" and
      // makes users trust that a sensitive key was persisted when it
      // was not. Skip the persist and settle the indicator to idle.
      // The forced media-provider sync path still runs because that
      // is a real outbound effect even when the persisted shape
      // hasn't changed.
      if (
        !persistOptions.forceMediaProviderSync
        && isAutosaveDraftOnlyChange(snapshot, autosaveLastSavedRef.current)
      ) {
        setAutosaveStatus('idle');
        return;
      }
      setAutosaveStatus('saving');
      void (async () => {
        try {
          await onPersist(snapshot, persistOptions);
          autosaveLastSavedRef.current = snapshot;
          lastSavedAppearanceRef.current = {
            theme: snapshot.theme ?? 'system',
            accentColor: resolveAccentColor(snapshot.accentColor),
          };
          // If a newer edit landed while the request was in flight,
          // leave the status as 'pending' so the next debounce tick
          // owns the indicator instead of flashing "Saved".
          if (autosaveLatestRef.current !== snapshot) {
            setAutosaveStatus('pending');
            return;
          }
          if (persistOptions.forceMediaProviderSync) {
            lastSyncedMediaProvidersVersionRef.current = mediaProvidersVersion;
            setPendingMediaProviderEditIds(new Set());
          }
          setAutosaveStatus('saved');
          autosaveSavedTimerRef.current = window.setTimeout(() => {
            autosaveSavedTimerRef.current = null;
            // Settle to idle after a moment so the indicator doesn't
            // stay on "Saved" forever and become noise.
            setAutosaveStatus((curr) => (curr === 'saved' ? 'idle' : curr));
          }, 1800);
        } catch {
          if (
            persistOptions.forceMediaProviderSync
            && autosaveLatestRef.current === snapshot
            && mediaProvidersChangeVersionRef.current === mediaProvidersVersion
            && lastSyncedMediaProvidersVersionRef.current < mediaProvidersVersion
          ) {
            setAutosaveStatus('pending');
            autosaveRetryTimerRef.current = window.setTimeout(() => {
              autosaveRetryTimerRef.current = null;
              if (
                autosaveLatestRef.current !== snapshot
                || mediaProvidersChangeVersionRef.current !== mediaProvidersVersion
                || lastSyncedMediaProvidersVersionRef.current >= mediaProvidersVersion
              ) {
                return;
              }
              setAutosaveRetryTick((tick) => tick + 1);
            }, 1500);
            return;
          }
          setAutosaveStatus('error');
        }
      })();
    }, 400);
    return () => {
      if (autosaveTimerRef.current != null) {
        window.clearTimeout(autosaveTimerRef.current);
        autosaveTimerRef.current = null;
      }
    };
  }, [cfg, onPersist, autosaveRetryTick]);
  // Flush any pending autosave on unmount so a fast-closing dialog
  // never strands an in-flight edit. We also clear the "Saved" toast
  // timer to avoid setState after unmount.
  useEffect(() => {
    return () => {
      if (autosavePendingFlushRef.current) {
        const mediaProvidersVersion = mediaProvidersChangeVersionRef.current;
        // Best-effort flush; if it rejects, localStorage already has
        // the latest copy from the synchronous saveConfig call inside
        // onPersist.
        autosavePendingFlushRef.current = false;
        void Promise.resolve(onPersist(autosaveLatestRef.current, {
          forceMediaProviderSync: mediaProvidersVersion > lastSyncedMediaProvidersVersionRef.current,
        })).catch(() => undefined);
      }
      if (autosaveSavedTimerRef.current != null) {
        window.clearTimeout(autosaveSavedTimerRef.current);
        autosaveSavedTimerRef.current = null;
      }
      if (autosaveRetryTimerRef.current != null) {
        window.clearTimeout(autosaveRetryTimerRef.current);
        autosaveRetryTimerRef.current = null;
      }
    };
  }, [onPersist]);

  // Global Escape closes the dialog. With no footer button anymore the
  // close affordances are: top-right X · backdrop click · Escape.
  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key !== 'Escape') return;
      onClose();
    }
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);

  const protocolProviders = useMemo(
    () => KNOWN_PROVIDERS.filter((p) => p.protocol === apiProtocol),
    [apiProtocol],
  );
  const selectedProviderIndex =
    cfg.apiProviderBaseUrl == null
      ? -1
      : protocolProviders.findIndex(
          (p) => p.baseUrl === cfg.apiProviderBaseUrl && p.baseUrl === cfg.baseUrl,
        );
  const selectedProvider = selectedProviderIndex >= 0 ? protocolProviders[selectedProviderIndex] : undefined;
  const showProviderPreset =
    protocolProviders.length > 0 && !isFixedOriginGateway(apiProtocol);
  // Fixed-origin gateways resolve their Base URL automatically; nothing for the
  // user to edit, so hide the field entirely.
  const showBaseUrlField = !isFixedOriginGateway(apiProtocol);
  const byokRequiresApiKey = byokProviderRequiresApiKey(
    apiProtocol,
    selectedProvider,
    cfg.baseUrl,
  );
  const byokFirstPartyBaseUrl = useMemo(
    () => byokFirstPartyBaseUrlHint(
      apiProtocol,
      cfg.baseUrl,
      protocolProviders,
    ),
    [apiProtocol, cfg.baseUrl, protocolProviders],
  );
  const byokKeyValidationBaseUrl = byokFirstPartyBaseUrl?.baseUrl;
  const byokDraftValidation = useMemo(
    () => validateByokDraft(
      apiProtocol,
      {
        apiKey: cfg.apiKey,
        baseUrl: cfg.baseUrl,
        model: cfg.model,
      },
      {
        requiresApiKey: byokRequiresApiKey,
        keyValidationBaseUrl: byokKeyValidationBaseUrl,
      },
    ),
    [
      apiProtocol,
      byokKeyValidationBaseUrl,
      byokRequiresApiKey,
      cfg.apiKey,
      cfg.baseUrl,
      cfg.model,
    ],
  );
  const byokBlockingDraftIssues = useMemo(
    () => blockingByokDraftIssues(byokDraftValidation),
    [byokDraftValidation],
  );
  const apiKeyDraftInvalid = byokBlockingDraftIssues.some((issue) =>
    issue.field === 'api_key' && issue.code !== 'api_key_required'
  );
  const byokModelFetchDraftValidation = useMemo(
    () => validateByokDraft(
      apiProtocol,
      {
        apiKey: cfg.apiKey,
        baseUrl: cfg.baseUrl,
        model: cfg.model,
      },
      {
        requiresApiKey: byokRequiresApiKey,
        requireModel: false,
        keyValidationBaseUrl: byokKeyValidationBaseUrl,
      },
    ),
    [
      apiProtocol,
      byokKeyValidationBaseUrl,
      byokRequiresApiKey,
      cfg.apiKey,
      cfg.baseUrl,
      cfg.model,
    ],
  );
  const providerModelsKey = useMemo(
    () => providerModelsCacheKey(
      apiProtocol,
      cfg.baseUrl,
      cfg.apiKey,
      cfg.apiVersion ?? '',
    ),
    [apiProtocol, cfg.baseUrl, cfg.apiKey, cfg.apiVersion],
  );
  const fetchedApiModelOptions =
    activeProviderModelsCache[providerModelsKey] ?? [];
  const commitProviderModelsInputs = () => {
    if (
      byokFirstPartyBaseUrl?.hostTypo ||
      blockingByokDraftIssues(byokModelFetchDraftValidation).length > 0
    ) {
      setProviderModelsCommittedKey(null);
      return;
    }
    setProviderModelsCommittedKey(providerModelsKey);
  };
  const onByokKeyCommit = () => {
    // Normalize the stored key on blur so the value that flows into the
    // connection-test / model-fetch requests below (and back to the daemon
    // via autosave) is already free of pasted whitespace / zero-width
    // characters — otherwise a key like "sk-ant-...\n" would only raise a
    // non-blocking warning yet still go out malformed over the wire.
    const cleanedApiKey = cleanByokApiKey(cfg.apiKey);
    if (cleanedApiKey !== cfg.apiKey) {
      // Writing the cleaned key changes cfg.apiKey, which re-runs the reset
      // effects above: one nulls providerModelsCommittedKey, the other bumps
      // providerTestRevisionRef / clears providerAutoTestKeyRef. So committing
      // the model key or starting the auto-test here would be clobbered — the
      // model commit before the auto-fetch effect reads it, and the auto-test
      // result dropped by the stale-revision guard. Defer both until the
      // cleaned value has landed (effect below), otherwise account models
      // never auto-load and the auto-test success/error never reaches the UI
      // for the exact dirty-paste case this handles.
      deferAfterKeyCleanRef.current = true;
      updateApiConfig({ apiKey: cleanedApiKey });
      return;
    }
    commitProviderModelsInputs();
    handleAutoTestProvider();
  };
  useEffect(() => {
    if (!deferAfterKeyCleanRef.current) return;
    deferAfterKeyCleanRef.current = false;
    if (
      byokFirstPartyBaseUrl?.hostTypo ||
      blockingByokDraftIssues(byokModelFetchDraftValidation).length > 0
    ) {
      setProviderModelsCommittedKey(null);
    } else {
      setProviderModelsCommittedKey(providerModelsKey);
    }
    // Runs after the provider-test reset effect (declaration order) bumped the
    // revision for the cleaned key, so this auto-test is not flagged stale.
    handleAutoTestProvider();
  }, [
    byokFirstPartyBaseUrl?.hostTypo,
    byokModelFetchDraftValidation,
    cfg.apiKey,
    providerModelsKey,
  ]);
  useEffect(() => {
    if (cfg.mode !== 'api') return;
    if (providerTestState.status === 'running') return;
    if (byokFirstPartyBaseUrl?.hostTypo) return;
    if (blockingByokDraftIssues(byokDraftValidation).length > 0) return;
    const key = providerConnectionTestKey(apiProtocol, cfg);
    if (providerAutoTestKeyRef.current === key) return;
    const timer = window.setTimeout(() => {
      handleAutoTestProvider();
    }, 500);
    return () => window.clearTimeout(timer);
  }, [
    apiProtocol,
    byokFirstPartyBaseUrl?.hostTypo,
    byokDraftValidation,
    cfg.apiKey,
    cfg.apiVersion,
    cfg.baseUrl,
    cfg.mode,
    cfg.model,
    providerTestState.status,
  ]);
  useEffect(() => {
    if (cfg.mode !== 'api') return;
    if (apiProtocol === 'azure' || apiProtocol === 'ollama') return;
    if (byokFirstPartyBaseUrl?.hostTypo) return;
    if (blockingByokDraftIssues(byokModelFetchDraftValidation).length > 0) return;
    // AIHubMix needs no key and prefills its base URL, so there's nothing to
    // debounce-commit — fetch as soon as the tab is selected. Every other
    // protocol waits until the key/baseUrl inputs are committed (on blur) so we
    // don't fire on each keystroke.
    if (apiProtocol !== 'aihubmix' && providerModelsCommittedKey !== providerModelsKey) return;
    const timer = window.setTimeout(() => {
      void handleFetchProviderModels({ silent: true });
    }, 300);
    return () => window.clearTimeout(timer);
  }, [
    apiProtocol,
    byokFirstPartyBaseUrl?.hostTypo,
    cfg.apiKey,
    cfg.baseUrl,
    cfg.mode,
    cfg.apiVersion,
    byokModelFetchDraftValidation,
    providerModelsCommittedKey,
    providerModelsKey,
  ]);
  const currentProviderModelsResult =
    providerModelsState.status === 'done' &&
    providerModelsState.cacheKey === providerModelsKey
      ? providerModelsState.result
      : null;
  const loadedAccountModelCount =
    currentProviderModelsResult?.ok && currentProviderModelsResult.models?.length
      ? currentProviderModelsResult.models.length
      : 0;
  const apiKeyAuthFailed =
    currentProviderModelsResult?.ok === false &&
    currentProviderModelsResult.kind === 'auth_failed';
  const providerModelsFailureMessage =
    currentProviderModelsResult?.ok === false && !apiKeyAuthFailed
      ? t('settings.fetchModelsFailed', {
          detail:
            currentProviderModelsResult.detail ||
            currentProviderModelsResult.kind,
        })
      : null;
  const providerTestBaseUrlInvalid =
    providerTestState.status === 'done' &&
    !providerTestState.result.ok &&
    providerTestState.result.kind === 'invalid_base_url';
  const providerTestApiKeyAuthFailed =
    providerTestState.status === 'done' &&
    !providerTestState.result.ok &&
    providerTestState.result.kind === 'auth_failed';
  const apiKeyFieldAuthFailed =
    providerTestApiKeyAuthFailed ||
    (apiKeyAuthFailed && providerTestState.status === 'idle');
  const baseUrlErrorMessage = baseUrlInvalid
    ? t('settings.baseUrlInvalid')
    : providerTestBaseUrlInvalid || byokFirstPartyBaseUrl?.hostTypo
      ? t('settings.testInvalidBaseUrl')
      : null;
  const suggestedApiModelIds = useMemo(
    () => Array.from(new Set(
      selectedProvider?.models?.length
        ? selectedProvider.models
        : SUGGESTED_MODELS_BY_PROTOCOL[apiProtocol],
    )),
    [apiProtocol, selectedProvider],
  );
  const apiModelOptions = useMemo(
    () => mergeProviderModelOptions(
      fetchedApiModelOptions,
      suggestedApiModelIds,
    ),
    [fetchedApiModelOptions, suggestedApiModelIds],
  );
  // Shared hook: live AIHubMix catalogue for aihubmix, static registry for
  // other providers (same list the chat composer's image picker uses).
  const byokImageModelOptions = useByokImageModelOptions(apiProtocol);
  const byokVideoModelOptions = useByokVideoModelOptions(apiProtocol);
  const byokSpeechModelOptions = useByokSpeechModelOptions(apiProtocol);
  const fetchedApiModelIds = useMemo(
    () => new Set(fetchedApiModelOptions.map((model) => model.id.trim())),
    [fetchedApiModelOptions],
  );
  const apiModelIds = useMemo(
    () => apiModelOptions.map((m) => m.id),
    [apiModelOptions],
  );
  const providerDefaultModel =
    selectedProvider?.model.trim() || suggestedApiModelIds[0] || '';
  useEffect(() => {
    if (cfg.mode !== 'api') return;
    if (apiModelCustomEditing) return;
    // Respect an explicit user pick — even when it equals the provider preset
    // id, the user deliberately chose it and discovery must not rewrite it.
    if (apiModelUserSelectedRef.current) return;
    if (fetchedApiModelOptions.length === 0) return;
    const currentModel = cfg.model.trim();
    if (currentModel && fetchedApiModelIds.has(currentModel)) return;
    if (currentModel && currentModel !== providerDefaultModel) return;

    const preference = resolveByokModelPreference({
      currentModel: '',
      accountModels: fetchedApiModelOptions,
      providerDefaultModel,
    });
    if (preference.source !== 'account') return;
    if (preference.model === currentModel) return;
    updateApiConfig({ model: preference.model });
  }, [
    apiModelCustomEditing,
    cfg.mode,
    cfg.model,
    fetchedApiModelIds,
    fetchedApiModelOptions,
    providerDefaultModel,
  ]);
  const apiModelCustomActive =
    shouldShowCustomModelInput(
      cfg.model,
      apiModelIds,
      apiModelCustomEditing,
    );
  const baseUrlReadOnly =
    (apiProtocol === 'anthropic' || apiProtocol === 'google') &&
    cfg.apiProviderBaseUrl !== null &&
    Boolean(cfg.baseUrl.trim()) &&
    !baseUrlInvalid;
  const baseUrlPlaceholder =
    apiProtocol === 'azure'
      ? t('settings.azureBaseUrlPlaceholder')
      : apiProtocol === 'ollama'
        ? 'http://localhost:11434'
        : undefined;
  useEffect(() => {
    if (!focusByokRequiredFieldAfterProtocolSwitchRef.current) return;
    focusByokRequiredFieldAfterProtocolSwitchRef.current = false;
    focusByokRequiredField(
      missingByokConnectionFields(cfg, {
        requiresApiKey: byokRequiresApiKey,
      })[0],
    );
  }, [apiModelCustomActive, cfg, apiProtocol, byokRequiresApiKey]);

  // Header title/subtitle follow the active sidebar section so the dialog
  // header always reflects what the user is looking at, instead of being
  // pinned to one section's copy. The execution section's header doubles
  // as the section heading — there is no inner h3 inside the Local CLI /
  // BYOK content so "Local CLI" only renders once (in the seg-control tab),
  // not twice (heading + tab).
  const sectionHeader: Record<SettingsSection, { title: string; subtitle: string }> = {
    execution: { title: t('settings.title'), subtitle: t('settings.subtitle') },
    instructions: {
      title: 'Instructions / Rules',
      subtitle: 'Fixed behavior the assistant should follow',
    },
    media: { title: t('settings.mediaProviders'), subtitle: t('settings.mediaProvidersHint') },
    composio: { title: t('connectors.title'), subtitle: t('connectors.subtitle') },
    orbit: { title: t('settings.orbit.title'), subtitle: t('settings.orbit.lede') },
    routines: {
      title: t('routines.title'),
      subtitle: t('routines.subtitle'),
    },
    integrations: { title: t('settings.mcpServerTitle'), subtitle: t('settings.mcpServerHint') },
    mcpClient: { title: t('settings.externalMcpTitle'), subtitle: t('settings.externalMcpHint') },
    language: { title: t('settings.language'), subtitle: t('settings.languageHint') },
    appearance: { title: t('settings.appearance'), subtitle: t('settings.appearanceHint') },
    critiqueTheater: {
      title: t('critiqueTheater.settingsNav'),
      subtitle: t('critiqueTheater.settingsNavHint'),
    },
    notifications: { title: t('settings.notifications'), subtitle: t('settings.notificationsHint') },
    privacy: { title: t('settings.privacy'), subtitle: t('settings.privacyHint') },
    pet: { title: t('pet.title'), subtitle: t('pet.subtitle') },
    skills: { title: t('settings.skills'), subtitle: t('settings.skillsHint') },
    designSystems: {
      title: t('settings.designSystems'),
      subtitle: t('settings.designSystemsHint'),
    },
    projectLocations: {
      title: t('settings.projectLocations'),
      subtitle: t('settings.projectLocationsHint'),
    },
    memory: { title: t('settings.memory'), subtitle: t('settings.memoryHint') },
    // 'library' is opened via EntryShell route — SettingsDialog doesn't
    // render it but SettingsSection must accept the token (see type def).
    library: { title: '', subtitle: '' },
    about: { title: t('settings.about'), subtitle: t('settings.aboutHint') },
  };
  const activeHeader = sectionHeader[activeSection];
  const installedAgents = agents.filter((a) => a.available);
  const unavailableAgents = agents.filter((a) => !a.available);
  const initialAgentScanRunning = agentsLoading && agents.length === 0;
  const agentModelOptionLabel = (
    model: ProviderModelOption | undefined,
    fallback: string,
  ) => {
    if (!model) return fallback;
    const label = model.label?.trim();
    const id = model.id.trim();
    if (label && label !== id) {
      return label.toLowerCase().includes(id.toLowerCase())
        ? label
        : `${label} (${id})`;
    }
    return label || id;
  };
  const agentModelSummary = (agent: AgentInfo) => {
    if (!Array.isArray(agent.models) || agent.models.length === 0) return null;
    const choice = cfg.agentModels?.[agent.id] ?? {};
    const modelValue = choice.model ?? agent.models[0]?.id ?? '';
    if (!modelValue) return t('settings.modelCustom');
    return agentModelOptionLabel(
      agent.models.find((m) => m.id === modelValue),
      modelValue,
    );
  };
  const renderAgentModelConfig = (selected: AgentInfo) => {
    const hasModels =
      Array.isArray(selected.models) && selected.models.length > 0;
    const hasReasoning =
      Array.isArray(selected.reasoningOptions) &&
      selected.reasoningOptions.length > 0;
    // AMR's live catalog only lands a beat after sign-in. While the user is
    // signed in but the model list hasn't arrived yet, show the picker in a
    // loading state instead of hiding it — so the dropdown appears at sign-in
    // and simply fills in, rather than popping in seconds later.
    if (selected.id === 'amr' && !hasModels && (amrCardStatus?.loggedIn ?? false)) {
      return (
        <div className="agent-card-config">
          <label className="field">
            <span className="field-label">
              {t('settings.modelPicker')}
              <span
                className="agent-model-source-badge live"
                aria-hidden="true"
              >
                {t('settings.modelSourceLive')}
              </span>
            </span>
            <div className="agent-model-select-wrap">
              <div
                className="settings-model-select agent-model-select-loading"
                role="status"
                aria-busy="true"
                data-testid={`settings-agent-model-loading-${selected.id}`}
              >
                <Icon name="spinner" size={13} className="icon-spin" />
                <span>{t('common.loading')}</span>
              </div>
            </div>
          </label>
          <p className="hint agent-model-row-hint">
            {t('settings.modelPickerLiveHint')}
          </p>
        </div>
      );
    }
    if (!hasModels && !hasReasoning) return null;
    const choice = cfg.agentModels?.[selected.id] ?? {};
    const knownModelIds = selected.models?.map((m) => m.id) ?? [];
    // Adapters opt out via `supportsCustomModel: false` on their
    // RuntimeAgentDef when their CLI has no `--model` flag (Antigravity,
    // upstream issue #35) or when free-text ids silently fail at spawn
    // (AMR routes through ACP `session/set_model` and validates against
    // a live catalog). Undefined === allow, matching today's UX.
    const allowCustomModel = selected.supportsCustomModel !== false;
    const configuredModel =
      typeof choice.model === 'string' && choice.model
        ? choice.model
        : null;
    const setChoice = (
      next: { model?: string; reasoning?: string },
    ) => {
      setCfg((c) => {
        const prev = c.agentModels?.[selected.id] ?? {};
        return {
          ...c,
          agentModels: {
            ...(c.agentModels ?? {}),
            [selected.id]: { ...prev, ...next },
          },
        };
      });
    };
    const modelValue =
      selected.id === 'amr' &&
      configuredModel &&
      !knownModelIds.includes(configuredModel)
        ? selected.models?.[0]?.id ?? ''
        : configuredModel ?? selected.models?.[0]?.id ?? '';
    const reasoningValue =
      choice.reasoning ??
      selected.reasoningOptions?.[0]?.id ?? '';
    const customActive =
      allowCustomModel &&
      hasModels &&
      shouldShowCustomModelInput(
        modelValue,
        knownModelIds,
        agentCustomModelIds.has(selected.id),
      );
    const selectValue = customActive
      ? CUSTOM_MODEL_SENTINEL
      : modelValue;
    const modelSource = selected.modelsSource ?? 'fallback';
    const modelSourceLabel =
      modelSource === 'live'
        ? t('settings.modelSourceLive')
        : t('settings.modelSourceFallback');
    const modelSourceHint =
      modelSource === 'live'
        ? selected.supportsCustomModel === false
          ? t('settings.modelPickerLiveCatalogOnlyHint')
          : t('settings.modelPickerLiveHint')
        : t('settings.modelPickerFallbackHint');
    return (
      <div className="agent-card-config">
        {hasModels ? (
          <>
            <label className="field">
              <span className="field-label">
                {t('settings.modelPicker')}
                <span
                  className={`agent-model-source-badge ${modelSource}`}
                  aria-hidden="true"
                >
                  {modelSourceLabel}
                </span>
              </span>
              <div className="agent-model-select-wrap">
                <SearchableModelSelect
                  className="inline-switcher__select settings-model-select"
                  value={selectValue}
                  aria-label={t('settings.modelPicker')}
                  searchPlaceholder={t('designs.searchPlaceholder')}
                  searchInputTestId={`settings-agent-model-search-${selected.id}`}
                  popoverTestId={`settings-agent-model-popover-${selected.id}`}
                  minSearchableOptions={5}
                  popoverMinWidth={340}
                  models={selected.models!}
                  onChange={(nextValue) => {
                    if (nextValue === CUSTOM_MODEL_SENTINEL) {
                      setAgentCustomModelIds((prev) => {
                        const next = new Set(prev);
                        next.add(selected.id);
                        return next;
                      });
                      setChoice({ model: '' });
                    } else {
                      setAgentCustomModelIds((prev) => {
                        if (!prev.has(selected.id)) return prev;
                        const next = new Set(prev);
                        next.delete(selected.id);
                        return next;
                      });
                      setChoice({ model: nextValue });
                    }
                  }}
                  additionalOptions={
                    allowCustomModel
                      ? [
                          {
                            value: CUSTOM_MODEL_SENTINEL,
                            label: t('settings.modelCustom'),
                          },
                        ]
                      : undefined
                  }
                />
              </div>
            </label>
            <p className="hint agent-model-row-hint">
              {modelSourceHint}
            </p>
          </>
        ) : null}
        {customActive ? (
          <label className="field">
            <span className="field-label">
              {t('settings.modelCustomLabel')}
            </span>
            <input
              type="text"
              value={modelValue}
              placeholder={t('settings.modelCustomPlaceholder')}
              onChange={(e) =>
                setChoice({ model: e.target.value.trim() })
              }
            />
          </label>
        ) : null}
        {hasReasoning ? (
          <label className="field">
            <span className="field-label">
              {t('settings.reasoningPicker')}
            </span>
            <div className="agent-model-select-wrap">
              <select
                value={reasoningValue}
                onChange={(e) =>
                  setChoice({ reasoning: e.target.value })
                }
              >
                {selected.reasoningOptions!.map((r) => (
                  <option key={r.id} value={r.id}>
                    {r.label}
                  </option>
                ))}
              </select>
              <Icon
                name="chevron-down"
                size={12}
                className="agent-model-select-chevron"
              />
            </div>
          </label>
        ) : null}
      </div>
    );
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div
        className="modal modal-settings"
        role="dialog"
        aria-modal="true"
        aria-labelledby="settings-dialog-title"
        onClick={(e) => e.stopPropagation()}
      >
        {/* Top-right chrome strip — anchored to the modal corner so the
            autosave indicator and the close button float above the
            sidebar/content rhythm without competing with the title.
            We use `position: absolute` instead of putting these inside
            `.modal-head` so the welcome variant's tall hero (kicker /
            title / subtitle / pet teaser) keeps its centred reading
            measure, and the close button always lands at the same
            optical location regardless of how much copy the header
            renders. */}
        <div className="settings-chrome" aria-hidden={false}>
          {/* Autosave status pill. Only renders something while a save
              is in flight or has just completed — idle = invisible so
              first-open feels calm. The chrome strip itself stays
              mounted so the close button never shifts when the pill
              appears, and the pill is announced via aria-live for
              assistive tech. */}
          <div
            className={`settings-autosave is-${autosaveStatus}`}
            role="status"
            aria-live="polite"
          >
            {autosaveStatus === 'saving' || autosaveStatus === 'pending' ? (
              <>
                <Icon name="spinner" size={12} className="icon-spin" />
                <span>{t('settings.autosaveSaving')}</span>
              </>
            ) : autosaveStatus === 'saved' ? (
              <>
                <Icon name="check" size={12} />
                <span>{t('settings.autosaveSaved')}</span>
              </>
            ) : autosaveStatus === 'error' ? (
              <>
                <Icon name="close" size={12} />
                <span>{t('settings.autosaveError')}</span>
              </>
            ) : null}
          </div>
          <button
            type="button"
            className="settings-close"
            onClick={onClose}
            aria-label={t('common.close')}
            title={t('common.close')}
          >
            <Icon name="close" size={16} strokeWidth={2} />
          </button>
        </div>
        <header className="modal-head" id="settings-dialog-title">
          {welcome ? (
            <>
              <span className="kicker">{t('settings.welcomeKicker')}</span>
              <h2>{t('settings.welcomeTitle')}</h2>
              <p className="subtitle">{t('settings.welcomeSubtitle')}</p>
            </>
          ) : (
            <>
              <span className="kicker">{t('settings.kicker')}</span>
              <div className="modal-head-line">
                <h2>{activeHeader.title}</h2>
                <p className="subtitle">{activeHeader.subtitle}</p>
              </div>
            </>
          )}
        </header>

        <div className="modal-body">
          <aside className="settings-sidebar" aria-label="Settings sections">
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'execution' ? ' active' : ''}`}
              onClick={() => setActiveSection('execution')}
            >
              <Icon name="sliders" size={18} />
              <span>
                <strong>{t('settings.envConfigure')}</strong>
                <small>{`${t('settings.localCli')} / ${t('settings.modeApiMeta')}`}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'instructions' ? ' active' : ''}`}
              onClick={() => setActiveSection('instructions')}
            >
              <Icon name="edit" size={18} />
              <span>
                <strong>Instructions / Rules</strong>
                <small>Fixed assistant behavior</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'memory' ? ' active' : ''}`}
              onClick={() => setActiveSection('memory')}
            >
              <Icon name="history" size={18} />
              <span>
                <strong>{t('settings.memory')}</strong>
                <small>{t('settings.memoryHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'media' ? ' active' : ''}`}
              onClick={() => setActiveSection('media')}
            >
              <Icon name="image" size={18} />
              <span>
                <strong>{t('settings.mediaProviders')}</strong>
                <small>Image / video / audio</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'skills' ? ' active' : ''}`}
              onClick={() => setActiveSection('skills')}
            >
              <Icon name="grid" size={18} />
              <span>
                <strong>{t('settings.skills')}</strong>
                <small>{t('settings.skillsHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'mcpClient' ? ' active' : ''}`}
              onClick={() => setActiveSection('mcpClient')}
            >
              <Icon name="sparkles" size={18} />
              <span>
                <strong>{t('settings.externalMcpTitle')}</strong>
                <small>{t('settings.externalMcpHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'composio' ? ' active' : ''}`}
              onClick={() => setActiveSection('composio')}
            >
              <Icon name="sliders" size={18} />
              <span>
                <strong>{t('connectors.title')}</strong>
                <small>{t('settings.connectorsNavHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'integrations' ? ' active' : ''}`}
              onClick={() => setActiveSection('integrations')}
            >
              <Icon name="link" size={18} />
              <span>
                <strong>{t('settings.mcpServerTitle')}</strong>
                <small>{t('settings.mcpServerHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'language' ? ' active' : ''}`}
              onClick={() => setActiveSection('language')}
            >
              <Icon name="languages" size={18} />
              <span>
                <strong>{t('settings.language')}</strong>
                <small>{t('settings.languageHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'appearance' ? ' active' : ''}`}
              onClick={() => setActiveSection('appearance')}
            >
              <Icon name="sun-moon" size={18} />
              <span>
                <strong>{t('settings.appearance')}</strong>
                <small>{t('settings.appearanceHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'critiqueTheater' ? ' active' : ''}`}
              onClick={() => setActiveSection('critiqueTheater')}
            >
              <Icon name="comment" size={18} />
              <span>
                <strong>{t('critiqueTheater.settingsNav')}</strong>
                <small>{t('critiqueTheater.settingsNavHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'notifications' ? ' active' : ''}`}
              onClick={() => setActiveSection('notifications')}
            >
              <Icon name="bell" size={18} />
              <span>
                <strong>{t('settings.notifications')}</strong>
                <small>{t('settings.notificationsHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'pet' ? ' active' : ''}`}
              onClick={() => setActiveSection('pet')}
            >
              <Icon name="sparkles" size={18} />
              <span>
                <strong>{t('pet.navTitle')}</strong>
                <small>{t('pet.navHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'designSystems' ? ' active' : ''}`}
              onClick={() => setActiveSection('designSystems')}
            >
              <Icon name="draw" size={18} />
              <span>
                <strong>{t('settings.designSystems')}</strong>
                <small>{t('settings.designSystemsHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'projectLocations' ? ' active' : ''}`}
              onClick={() => setActiveSection('projectLocations')}
            >
              <Icon name="folder" size={18} />
              <span>
                <strong>{t('settings.projectLocations')}</strong>
                <small>{t('settings.projectLocationsHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'privacy' ? ' active' : ''}`}
              onClick={() => setActiveSection('privacy')}
            >
              <Icon name="eye" size={18} />
              <span>
                <strong>{t('settings.privacy')}</strong>
                <small>{t('settings.privacyHint')}</small>
              </span>
            </button>
            <button
              type="button"
              className={`settings-nav-item${activeSection === 'about' ? ' active' : ''}`}
              onClick={() => setActiveSection('about')}
            >
              <Icon name="settings" size={18} />
              <span>
                <strong>{t('settings.about')}</strong>
                <small>{t('settings.aboutHint')}</small>
              </span>
            </button>
          </aside>
          <div className="settings-content" ref={settingsContentRef}>
          {activeSection === 'execution' ? (
            <>
              <div
                className="seg-control"
                role="tablist"
                aria-label={t('settings.modeAria')}
                style={{ ['--seg-cols' as string]: 2 } as CSSProperties}
              >
                <button
                  type="button"
                  role="tab"
                  aria-selected={cfg.mode === 'daemon'}
                  className={
                    'seg-btn seg-btn--inline' +
                    (cfg.mode === 'daemon' ? ' active' : '')
                  }
                  disabled={!daemonLive}
                  onClick={() => setMode('daemon')}
                  title={
                    daemonLive
                      ? t('settings.modeDaemonHelp')
                      : t('settings.modeDaemonOffline')
                  }
                >
                  <span className="seg-title">{t('settings.localCli')}</span>
                  <span className="seg-meta">
                    {daemonLive
                      ? t('settings.modeDaemonInstalledMeta', { count: installedCount })
                      : t('settings.modeDaemonOfflineMeta')}
                  </span>
                </button>
                <button
                  type="button"
                  role="tab"
                  aria-selected={cfg.mode === 'api'}
                  className={
                    'seg-btn seg-btn--inline' +
                    (cfg.mode === 'api' ? ' active' : '')
                  }
                  onClick={() => setMode('api')}
                >
                  <span className="seg-title">{t('settings.modeApiMeta')}</span>
                  <span className="seg-meta">{t('settings.modeApi')}</span>
                </button>
              </div>
              {cfg.mode === 'api' ? (
                <div
                  className="protocol-chips"
                  role="tablist"
                  aria-label={t('settings.protocolAria')}
                >
                  {apiProtocolTabGroups.map((group) => (
                    <div className="protocol-chip-group" key={group.id}>
                      <span className="protocol-chip-group-label">
                        {group.label}
                      </span>
                      <div className="protocol-chip-group-options">
                        {group.tabs.map((tab) => (
                          <button
                            key={tab.id}
                            type="button"
                            role="tab"
                            aria-selected={apiProtocol === tab.id}
                            className={'protocol-chip' + (apiProtocol === tab.id ? ' active' : '')}
                            onClick={() => {
                              const byokProviderId = byokProtocolToTracking(tab.id);
                              if (byokProviderId) {
                                trackSettingsByokProviderOptionClick(analytics.track, {
                                  page_name: 'settings',
                                  area: 'configure_execution_mode_byok',
                                  element: 'byok_provider_option',
                                  action: 'select_byok_provider',
                                  provider_id: byokProviderId,
                                  is_selected: apiProtocol === tab.id,
                                });
                              }
                              setApiProtocol(tab.id);
                            }}
                          >
                            {tab.title}
                          </button>
                        ))}
                      </div>
                    </div>
                  ))}
                </div>
              ) : null}
          {cfg.mode === 'daemon' ? (
            <section className="settings-section">
              <div className="section-head">
                <div>
                  <p className="hint">{t('settings.codeAgentHint')}</p>
                </div>
              </div>
              {initialAgentScanRunning ? (
                <div className="agent-scan-card" role="status" aria-live="polite">
                  <div className="agent-scan-card__stage">
                    <span className="agent-scan-card__ring" aria-hidden />
                    <strong>{t('settings.rescanRunning')}</strong>
                    <span>{t('settings.codeAgentHint')}</span>
                    <div className="agent-scan-card__progress" aria-hidden>
                      <span />
                    </div>
                  </div>
                  <div className="agent-scan-card__rows" aria-hidden>
                    <span><i /><b /><em /></span>
                    <span><i /><b /><em /></span>
                    <span><i /><b /><em /></span>
                  </div>
                </div>
              ) : agents.length === 0 ? (
                <div className="empty-card">
                  {t('settings.noAgentsDetected')}
                </div>
              ) : (
                <>
                  <div className="agent-group">
                    <div className="agent-group-head">
                      <h4>
                        {t('settings.agentInstalledGroup', {
                          count: installedAgents.length,
                        })}
                      </h4>
                      <div className="agent-group-head-actions">
                        {agentRescanNotice ? (
                          <span
                            className={
                              'settings-rescan-status settings-rescan-status-inline ' +
                              agentRescanNotice.kind
                            }
                            role={
                              agentRescanNotice.kind === 'error'
                                ? 'alert'
                                : 'status'
                            }
                          >
                            {agentRescanNotice.kind === 'success'
                              ? t('settings.rescanSuccess', {
                                  count: agentRescanNotice.count,
                                })
                              : t('settings.rescanFailed')}
                          </span>
                        ) : null}
                        <button
                          type="button"
                          className={
                            'ghost icon-btn settings-rescan-btn agent-group-rescan-btn' +
                            (agentRescanRunning ? ' loading' : '')
                          }
                          onClick={() => void handleRefreshAgents()}
                          disabled={agentRescanRunning}
                          title={t('settings.rescanTitle')}
                        >
                          {agentRescanRunning ? (
                            <>
                              <Icon
                                name="spinner"
                                size={13}
                                className="icon-spin"
                              />
                              <span>{t('settings.rescanRunning')}</span>
                            </>
                          ) : (
                            t('settings.rescan')
                          )}
                        </button>
                      </div>
                    </div>
                    {installedAgents.length > 0 ? (
                      <div className="agent-grid agent-grid-installed">
                        {installedAgents.map((a) => {
                          const active = cfg.agentId === a.id;
                          const running =
                            active && agentTestState.status === 'running';
                          const isAmrAgent = a.id === 'amr';
                          const description = AGENT_SHORT_DESCRIPTIONS[a.id];
                          const agentName = displayAgentName(a);
                          const diagnosticHandlers = diagnosticHandlersForAgent(a);
                          const modelSummary = agentModelSummary(a);
                          const amrBenefits = [
                            t('settings.amrBenefitOfficial'),
                            t('settings.amrBenefitLowerPrice'),
                            t('settings.amrBenefitManyModels'),
                          ];
                          const versionLabel =
                            isAmrAgent
                              ? ''
                              : cleanAgentVersionLabel(a.name, a.version);
                          const metaLabel =
                            a.authStatus === 'missing'
                              ? t('settings.agentAuthRequired')
                              : a.authStatus === 'unknown'
                                ? t('settings.agentAuthUnknown')
                                : versionLabel
                                  ? versionLabel
                                  : a.id === 'amr'
                                    ? ''
                                    : t('common.installed');
                          const metaTitle =
                            a.authStatus === 'missing' ||
                            a.authStatus === 'unknown'
                              ? (a.authMessage ?? a.path ?? '')
                              : (a.path ?? '');
                          const amrHighlighted = isAmrAgent && amrHighlightActive;
                          const amrCardEmail =
                            isAmrAgent && active && amrCardStatus?.loggedIn
                              ? amrCardStatus.user?.email || t('settings.amrSignedIn')
                              : '';
                          const amrRevealPendingCancelAction =
                            isAmrAgent &&
                            active &&
                            hoveredAgentCardId === a.id &&
                            amrCardStatus?.loggedIn !== true &&
                            amrCardStatus?.loginInFlight === true;
                          const cardEl = (
                            <div
                              key={a.id}
                              ref={isAmrAgent ? amrCardRef : undefined}
                              className={
                                'agent-card agent-card-installed' +
                                (active ? ' active' : '') +
                                (amrHighlighted ? ' agent-card--amr-highlight' : '')
                              }
                              onMouseEnter={() => {
                                if (!isAmrAgent || !active) return;
                                setHoveredAgentCardId(a.id);
                              }}
                              onMouseLeave={() => {
                                if (hoveredAgentCardId !== a.id) return;
                                setHoveredAgentCardId(null);
                              }}
                            >
                              <div className="agent-card-main">
                                <button
                                  type="button"
                                  className="agent-card-select"
                                  onClick={() => {
                                    trackSettingsLocalCliClick(analytics.track, {
                                      page_name: 'settings',
                                      area: 'configure_execution_mode_local_cli',
                                      element: 'cli_provider',
                                      cli_provider_id: agentIdToTracking(a.id),
                                      install_status: 'installed',
                                    });
                                    if (isAmrAgent) {
                                      recordAmrEntry(analytics.track, 'settings_amr_agent_card');
                                    }
                                    setCfg((c) => ({ ...c, agentId: a.id }));
                                  }}
                                  aria-pressed={active}
                                  >
                                    <AgentIcon id={a.id} size={32} />
                                    <div className="agent-card-body">
                                      <div
                                        className={
                                          'agent-card-name' +
                                          (isAmrAgent
                                            ? ' agent-card-name--amr'
                                            : '')
                                        }
                                      >
                                        <span className="agent-card-title">
                                          {agentName}
                                        </span>
                                        {isAmrAgent ? (
                                          <span
                                            className="agent-card-benefits"
                                            aria-hidden="true"
                                          >
                                            {amrBenefits.map((benefit) => (
                                              <span
                                                key={benefit}
                                                className="agent-card-benefit"
                                              >
                                                {benefit}
                                              </span>
                                            ))}
                                          </span>
                                        ) : description ? (
                                          <>
                                            <span
                                              className="agent-card-name-divider"
                                              aria-hidden="true"
                                            >
                                              ·
                                            </span>
                                            <span className="agent-card-tagline">
                                              {description}
                                            </span>
                                          </>
                                        ) : null}
                                      </div>
                                      {metaLabel ? (
                                        <div className="agent-card-meta">
                                          <span title={metaTitle}>
                                            {metaLabel}
                                          </span>
                                        </div>
                                      ) : null}
                                      {amrCardEmail ? (
                                        <div className="agent-card-amr-email">
                                          <span title={amrCardEmail}>
                                            {amrCardEmail}
                                          </span>
                                        </div>
                                      ) : null}
                                      {!active && modelSummary ? (
                                        <div className="agent-card-model-summary">
                                          <span>{t('settings.modelPicker')}</span>
                                          <strong>{modelSummary}</strong>
                                        </div>
                                      ) : null}
                                  </div>
                                </button>
                                {isAmrAgent ? (
                                  active && amrCardStatusReady ? (
                                    <span
                                      className="amr-auth-anchor"
                                      onMouseEnter={() => setAmrCoachmarkDismissed(true)}
                                    >
                                      {amrCoachmarkArmed &&
                                      amrCardStatus?.loggedIn === false &&
                                      !amrCoachmarkDismissed ? (
                                        <span className="amr-coachmark" aria-hidden="true">
                                          <span className="amr-coachmark__ring" />
                                          <svg
                                            className="amr-coachmark__cursor"
                                            width="22"
                                            height="22"
                                            viewBox="0 0 24 24"
                                            fill="none"
                                          >
                                            <path
                                              d="M9.4 13V8a1.8 1.8 0 0 1 3.6 0v4.6c.35-.55 1-.95 1.75-.95.65 0 1.25.32 1.6.85.32-.5.9-.8 1.55-.8.8 0 1.5.5 1.78 1.2.35-.3.8-.5 1.3-.5 1.1 0 2 .9 2 2v3.05a5.6 5.6 0 0 1-5.6 5.6h-2.5a5 5 0 0 1-3.75-1.7l-4.2-4.75a1.85 1.85 0 0 1 2.65-2.6L9.4 16Z"
                                              fill="#fff"
                                              stroke="#1a1a1a"
                                              strokeWidth="1.1"
                                              strokeLinejoin="round"
                                            />
                                          </svg>
                                        </span>
                                      ) : null}
                                      <AmrLoginPill
                                        className="agent-card-amr-auth"
                                        hideSignedOutStatus
                                        hideSignedInStatus
                                        initialStatus={amrCardStatus}
                                        skipInitialRefresh
                                        signInLabel={t('settings.amrAuthorize')}
                                        showConsoleAction={amrCardStatus?.loggedIn === true}
                                        amrEntrySourceDetail="settings_amr_authorize"
                                        revealPendingCancelAction={amrRevealPendingCancelAction}
                                        onStatusChange={setAmrCardStatus}
                                      />
                                    </span>
                                  ) : (
                                    <div
                                      className="agent-card-amr-auth agent-card-amr-auth--placeholder"
                                      aria-hidden="true"
                                    />
                                  )
                                ) : null}
                                {active && !isAmrAgent ? (
                                  <button
                                    type="button"
                                    className={
                                      'ghost icon-btn settings-test-btn agent-card-test-btn' +
                                      (running ? ' loading' : '')
                                    }
                                    onClick={() => void handleTestAgent()}
                                    disabled={running}
                                    title={t('settings.testTitle')}
                                  >
                                    {running ? (
                                      <>
                                        <Icon
                                          name="spinner"
                                          size={13}
                                          className="icon-spin"
                                        />
                                        <span>{t('settings.test')}</span>
                                      </>
                                    ) : (
                                      t('settings.test')
                                    )}
                                  </button>
                                ) : null}
                              </div>
                              {(a.diagnostics ?? []).map((diagnostic, i) => (
                                <AgentDiagnosticRow
                                  key={`${diagnostic.reason}-${i}`}
                                  diagnostic={diagnostic}
                                  handlers={diagnosticHandlers}
                                />
                              ))}
                              {active ? renderAgentModelConfig(a) : null}
                            </div>
                          );
                          if (active && agentTestState.status !== 'idle') {
                            const resultRow = (
                              <div
                                key={`${a.id}__test-result`}
                                className="agent-test-result-row"
                              >
                                {agentTestState.status === 'running' ? (
                                  <p
                                    className="settings-test-status running"
                                    role="status"
                                    aria-live="polite"
                                  >
                                    {t('settings.testRunning')}
                                  </p>
                                ) : (
                                  <>
                                    <p
                                      className={
                                        'settings-test-status ' +
                                        testStatusVariant(agentTestState.result)
                                      }
                                      role={
                                        agentTestState.result.ok
                                          ? 'status'
                                          : 'alert'
                                      }
                                    >
                                      {renderTestMessage(
                                        agentTestState.result,
                                        'cli',
                                      )}
                                    </p>
                                    {!agentTestState.result.ok ? (
                                      <div className="settings-test-actions">
                                        <div className="settings-test-actions-row">
                                          <button
                                            type="button"
                                            className="ghost icon-btn settings-test-btn"
                                            onClick={() => void handleTestAgent()}
                                          >
                                            <Icon name="reload" size={13} />
                                            <span>{t('settings.testRetry')}</span>
                                          </button>
                                        </div>
                                      </div>
                                    ) : null}
                                    {cfg.agentId === 'codex' && (() => {
                                      const repair = codexPathRepairState(
                                        agentTestState.result,
                                      );
                                      if (!repair) return null;
                                      const codexStrings = codexPathStrings(locale);
                                      return (
                                        <div className="settings-test-actions">
                                          <span className="settings-test-actions-hint">
                                            {codexStrings.repairHint}
                                          </span>
                                          <div className="settings-test-actions-row">
                                            {repair.canUseDetected ? (
                                              <button
                                                type="button"
                                                className="settings-test-btn"
                                                onClick={() =>
                                                  applyCodexDetectedPath(
                                                    repair.detectedPath,
                                                  )
                                                }
                                              >
                                                {codexStrings.useDetected}
                                              </button>
                                            ) : null}
                                            <button
                                              type="button"
                                              className="ghost icon-btn settings-rescan-btn"
                                              onClick={clearCodexCustomPath}
                                            >
                                              {codexStrings.clearCustom}
                                            </button>
                                          </div>
                                        </div>
                                      );
                                    })()}
                                  </>
                                )}
                              </div>
                            );
                            return [cardEl, resultRow];
                          }
                          return [cardEl];
                        })}
                      </div>
                    ) : (
                      <div className="empty-card">
                        {t('settings.noAgentsDetected')}
                      </div>
                    )}
                  </div>
                  {unavailableAgents.length > 0 ? (
                    <details
                      className="agent-install-collapse"
                      open={installedAgents.length > 0 ? undefined : true}
                    >
                      <summary className="agent-install-collapse-summary">
                        <span>
                          {t('settings.agentInstallGroup', {
                            count: unavailableAgents.length,
                          })}
                        </span>
                      </summary>
                      <div className="agent-grid agent-grid-unavailable">
                        {unavailableAgents.map((a) => {
                          const installUrl = sanitizeHttpsUrl(a.installUrl);
                          const docsUrl = sanitizeHttpsUrl(a.docsUrl);
                          const hasLinks = Boolean(installUrl || docsUrl);
                          const description = AGENT_SHORT_DESCRIPTIONS[a.id];
                          const agentName = displayAgentName(a);
                          const diagnosticHandlers = diagnosticHandlersForAgent(a);
                          const cardLabel = `${agentName} · ${t('common.notInstalled')}`;
                          return (
                            <div
                              key={a.id}
                              className="agent-card disabled agent-card-unavailable"
                              role="group"
                              aria-label={cardLabel}
                            >
                              <div className="agent-card-unavailable-row">
                                <AgentIcon id={a.id} size={30} />
                                <div className="agent-card-body">
                                  <div className="agent-card-name">
                                    {agentName}
                                  </div>
                                  {description ? (
                                    <div className="agent-card-description">
                                      {description}
                                    </div>
                                  ) : null}
                                </div>
                                {hasLinks ? (
                                  <div className="agent-card-actions agent-card-actions--inline">
                                    {docsUrl ? (
                                      <a
                                        href={docsUrl}
                                        target="_blank"
                                        rel="noopener noreferrer"
                                        className="agent-card-link agent-card-link--muted agent-card-link--icon"
                                        onClick={markAgentInstallIntent}
                                        title={t('settings.agentInstall.docs')}
                                        aria-label={t('settings.agentInstall.docs')}
                                      >
                                        <Icon name="file" size={15} />
                                      </a>
                                    ) : null}
                                    {installUrl ? (
                                      <a
                                        href={installUrl}
                                        target="_blank"
                                        rel="noopener noreferrer"
                                        className="agent-card-link agent-card-link--ghost"
                                        onClick={markAgentInstallIntent}
                                      >
                                        {t('settings.agentInstall.install')}
                                      </a>
                                    ) : null}
                                  </div>
                                ) : null}
                              </div>
                              {/* Why is it unavailable? not-on-path vs a broken
                                  shim vs a bad *_BIN override each get a
                                  distinct, actionable line. It spans the full
                                  card width on its own row below the
                                  logo/name/links so it never crowds the inline
                                  Docs/Install actions. */}
                              {(a.diagnostics ?? []).map((diagnostic, i) => (
                                <AgentDiagnosticRow
                                  key={`${diagnostic.reason}-${i}`}
                                  diagnostic={diagnostic}
                                  handlers={diagnosticHandlers}
                                />
                              ))}
                            </div>
                          );
                        })}
                      </div>
                    </details>
                  ) : null}
                  {/*
                    Show the install guide only when the user has *no*
                    working agent picked yet. Older logic surfaced it
                    whenever any agent on the support list was missing,
                    which fired for almost everyone (few people install
                    all 14 supported CLIs) — the four-step quickstart
                    then sat between the agent grid and the model picker
                    forever, even after the user had successfully picked
                    Claude Code months ago. Once a working agent is
                    selected, the guide has done its job and only adds
                    noise.
                  */}
                  {!agents.find(
                    (a) => a.id === cfg.agentId && a.available,
                  ) ? (
                    <div className="agent-install-guide">
                      <p className="hint agent-install-path-hint">
                        {t('settings.agentInstall.pathHint')}
                      </p>
                      <ol className="agent-install-steps">
                        <li>{t('settings.agentInstall.stepOpenLinks')}</li>
                        <li>{t('settings.agentInstall.stepAuth')}</li>
                        <li>{t('settings.agentInstall.stepRescan')}</li>
                        <li>{t('settings.agentInstall.stepSelect')}</li>
                      </ol>
                    </div>
                  ) : null}
                </>
              )}
              {(() => {
                const selected = agents.find(
                  (a) => a.id === cfg.agentId && a.available,
                );
                if (!selected) return null;
                const hasModels =
                  Array.isArray(selected.models) && selected.models.length > 0;
                const choice = cfg.agentModels?.[selected.id] ?? {};
                const knownModelIds = selected.models?.map((m) => m.id) ?? [];
                const configuredModel =
                  typeof choice.model === 'string' && choice.model
                    ? choice.model
                    : null;
                const modelValue =
                  selected.id === 'amr' &&
                  configuredModel &&
                  !knownModelIds.includes(configuredModel)
                    ? selected.models?.[0]?.id ?? ''
                    : configuredModel ?? selected.models?.[0]?.id ?? '';
                return (
                  <details className="agent-cli-env settings-memory-advanced">
                    <summary className="agent-cli-env-summary">
                      <span className="agent-cli-env-summary-title">
                        {t('settings.memoryModelInlineLabel')}
                      </span>
                    </summary>
                    <div className="agent-cli-env-body">
                      <MemoryModelInline
                        mode="daemon"
                        apiProtocol={apiProtocol}
                        chatApiKey={cfg.apiKey}
                        chatBaseUrl={cfg.baseUrl}
                        chatApiVersion={cfg.apiVersion ?? ''}
                        chatModel={modelValue}
                        cliAgentId={selected.id}
                        cliModelOptions={
                          hasModels ? selected.models!.map((m) => m.id) : []
                        }
                      />
                    </div>
                  </details>
                );
              })()}
              {(() => {
                /*
                  Per-agent CLI environment overrides — proxy URLs, custom
                  config dirs, and a binary path override. The previous
                  layout listed every supported agent's variables in one
                  long always-expanded block; for users on Claude Code
                  the Codex fields were just visual filler (and vice
                  versa), and the section hijacked Settings real estate
                  on every open even though nine in ten users never
                  touch it. Now: filtered to the *currently selected*
                  agent only, and folded into a collapsed disclosure
                  that opens to "Advanced: proxy & custom paths" — power
                  users who route through LiteLLM or installed the
                  binary out-of-PATH still have one click access; new
                  users no longer wonder "are these fields I forgot to
                  fill in?".
                */
                const cliEnvFields = AGENT_CLI_ENV_FIELDS.filter(
                  (field) => field.agentId === cfg.agentId,
                );
                if (cliEnvFields.length === 0) return null;
                return (
                  <details
                    className="agent-cli-env"
                    data-testid="settings-cli-env"
                  >
                    <summary className="agent-cli-env-summary">
                      <span className="agent-cli-env-summary-title">
                        {t('settings.cliEnvTitle')}
                      </span>
                    </summary>
                    <div className="agent-cli-env-body">
                      <p className="hint">{t('settings.cliEnvHint')}</p>
                      <div className="agent-cli-env-grid">
                        {cliEnvFields.map((field) => (
                          <label
                            className="field"
                            key={`${field.agentId}:${field.envKey}`}
                          >
                            <span className="field-label">
                              {t(field.labelKey)}
                              {'labelSuffix' in field
                                ? ` (${field.labelSuffix})`
                                : ''}
                            </span>
                            <input
                              type={
                                'secret' in field && field.secret
                                  ? 'password'
                                  : 'text'
                              }
                              value={
                                cfg.agentCliEnv?.[field.agentId]?.[
                                  field.envKey
                                ] ?? ''
                              }
                              placeholder={field.placeholder}
                              spellCheck={false}
                              autoComplete="off"
                              onChange={(e) =>
                                setCfg((c) =>
                                  updateAgentCliEnvValue(
                                    c,
                                    field.agentId,
                                    field.envKey,
                                    e.target.value,
                                  ),
                                )
                              }
                            />
                          </label>
                        ))}
                      </div>
                    </div>
                  </details>
                );
              })()}
            </section>
          ) : (
            /*
              BYOK panel — wrap the per-protocol form in a bordered card so
              the chips above (Anthropic / OpenAI / Azure / Gemini / Ollama)
              visually own the content below. Without the card, the chip
              row and the form looked like two unrelated stripes; users
              had no anchor for "this is what I configured for the active
              tab", and switching tabs felt like the whole right column
              just reshuffled. The card lives on the same white-with-soft-
              border pattern as `.agent-model-row` so the two BYOK / CLI
              panels feel like the same family.
            */
            <section className="settings-section settings-section-card settings-section-byok">
              <div className="section-head">
                <div>
                  <div className="settings-byok-title">
                    <h3>{API_PROTOCOL_LABELS[apiProtocol]}</h3>
                    <span className="settings-byok-info-wrap">
                      <button
                        type="button"
                        className="settings-byok-info-button"
                        aria-label={t('settings.byokNoFileToolsNotice')}
                        aria-describedby="settings-byok-no-file-tools-tooltip"
                        data-testid="settings-byok-no-file-tools-trigger"
                      >
                        <Icon name="info" size={13} />
                      </button>
                      <span
                        id="settings-byok-no-file-tools-tooltip"
                        className="settings-byok-info-tooltip"
                        role="tooltip"
                        data-testid="settings-byok-no-file-tools-notice"
                      >
                        {t('settings.byokNoFileToolsNotice')}
                      </span>
                    </span>
                  </div>
                </div>
                <ByokConnectionTestControl
                  baseUrlValid={baseUrlValid}
                  canRunConnectionTest={
                    !byokFirstPartyBaseUrl?.hostTypo &&
                    canRunProviderConnectionTest(cfg, {
                      requiresApiKey: byokRequiresApiKey,
                    })
                  }
                  labels={{
                    readyToTest: t('settings.byokReadyToTest'),
                    test: t('settings.test'),
                    testRetry: t('settings.testRetry'),
                    testRunning: t('settings.testRunning'),
                    testTitle: t('settings.testTitle'),
                  }}
                  providerTestState={providerTestState}
                  renderTestMessage={(result) => renderTestMessage(result, 'api')}
                  suppressResultStatus={
                    providerTestBaseUrlInvalid || providerTestApiKeyAuthFailed
                  }
                  suppressReadyState={Boolean(
                    byokPreconditionNotice ||
                      apiKeyFieldAuthFailed ||
                      providerTestBaseUrlInvalid ||
                      byokBlockingDraftIssues.length > 0,
                  )}
                  onTestProvider={() => handleTestProvider()}
                />
              </div>
              {byokPreconditionNotice && !byokPreconditionNotice.field ? (
                <p
                  className="settings-test-status error"
                  role="alert"
                  aria-live="polite"
                  data-action={byokPreconditionNotice.action}
                >
                  {byokPreconditionNotice.message}
                </p>
              ) : null}
              {showProviderPreset ? (
                <ByokProviderPicker
                  label={t('settings.providerPreset')}
                  customProviderLabel={t('settings.customProvider')}
                  providers={protocolProviders}
                  selectedProviderIndex={selectedProviderIndex}
                  onCustomProviderSelect={() => {
                    setApiModelCustomEditing(false);
                    updateApiConfig({
                      baseUrl: '',
                      model: '',
                      apiProviderBaseUrl: null,
                    });
                  }}
                  onProviderSelect={(p) => {
                    setApiModelCustomEditing(false);
                    updateApiConfig({
                      baseUrl: p.baseUrl,
                      model: p.model,
                      apiProviderBaseUrl: p.baseUrl,
                    });
                  }}
                />
              ) : null}
              <ByokKeyField
                apiKey={cfg.apiKey}
                apiKeyConsoleLink={apiKeyConsoleLink}
                apiProtocol={apiProtocol}
                inputRef={apiKeyInputRef}
                labels={{
                  apiHint: t('settings.apiHint'),
                  apiKey: t('settings.apiKey'),
                  apiKeyCleaned: t('settings.apiKeyCleaned'),
                  apiKeyGetLink: t('settings.apiKeyGetLink', {
                    host: apiKeyConsoleLink.host,
                  }),
                  apiKeyInvalid: t('settings.apiKeyInvalid'),
                  hide: t('settings.hide'),
                  hideKey: t('settings.hideKey'),
                  required: t('settings.required'),
                  show: t('settings.show'),
                  showKey: t('settings.showKey'),
                }}
                requiresApiKey={byokRequiresApiKey}
                showApiKeyInvalid={Boolean(
                  apiKeyFieldAuthFailed ||
                    byokPreconditionNotice?.field === 'api_key' ||
                    apiKeyDraftInvalid,
                )}
                showApiKey={showApiKey}
                onBlur={onByokKeyCommit}
                onChange={(value) => updateApiConfig({ apiKey: value })}
                onFocus={() => {
                  const byokProviderId = byokProtocolToTracking(apiProtocol);
                  if (byokProviderId) {
                    trackSettingsByokFieldClick(analytics.track, {
                      page_name: 'settings',
                      area: 'configure_execution_mode_byok',
                      element: 'api_key',
                      provider_id: byokProviderId,
                      has_value: Boolean(cfg.apiKey?.trim()),
                    });
                  }
                }}
                onToggleShowApiKey={() => setShowApiKey((v) => !v)}
              />
              {showBaseUrlField ? (
                <ByokProviderBaseUrl
                  apiProtocol={apiProtocol}
                  inputRef={baseUrlInputRef}
                  baseUrl={cfg.baseUrl}
                  baseUrlError={baseUrlErrorMessage}
                  baseUrlInvalid={Boolean(baseUrlErrorMessage)}
                  baseUrlPlaceholder={baseUrlPlaceholder}
                  baseUrlReadOnly={baseUrlReadOnly}
                  labels={{
                    baseUrl: t('settings.baseUrl'),
                    required: t('settings.required'),
                    customize: t('settings.baseUrlCustomize'),
                    invalid: t('settings.baseUrlInvalid'),
                    defaultHint: t('settings.baseUrlDefaultHint'),
                    azureHint: t('settings.azureBaseUrlHint'),
                  }}
                  onBlur={commitProviderModelsInputs}
                  onChange={(value) => updateApiConfig({ baseUrl: value, apiProviderBaseUrl: null })}
                  onCustomize={() => {
                    updateApiConfig({ apiProviderBaseUrl: null });
                    window.setTimeout(() => baseUrlInputRef.current?.focus(), 0);
                  }}
                  onFocus={() => {
                    const byokProviderId = byokProtocolToTracking(apiProtocol);
                    if (byokProviderId) {
                      trackSettingsByokFieldClick(analytics.track, {
                        page_name: 'settings',
                        area: 'configure_execution_mode_byok',
                        element: 'base_url',
                        provider_id: byokProviderId,
                        has_value: Boolean(cfg.baseUrl?.trim()),
                      });
                    }
                  }}
                />
              ) : null}
              <label className="field">
                <span className="field-label">{t('settings.maxTokens')}</span>
                <input
                  type="number"
                  min={MIN_MAX_TOKENS}
                  max={MAX_MAX_TOKENS}
                  step={1}
                  placeholder={String(modelMaxTokensDefault(cfg.model))}
                  value={maxTokensInput}
                  onChange={(e) => updateMaxTokensInput(e.target.value)}
                  onBlur={() => setMaxTokensInput(cfg.maxTokens == null ? '' : String(cfg.maxTokens))}
                />
                <p className="hint">{t('settings.maxTokensHint')}</p>
              </label>
              <ByokModelField
                customActive={apiModelCustomActive}
                customInputRef={customModelInputRef}
                labels={{
                  customModel: t('settings.modelCustom'),
                  customModelLabel: apiProtocol === 'azure'
                    ? t('settings.azureCustomDeploymentName')
                    : t('settings.modelCustomLabel'),
                  customModelPlaceholder: apiProtocol === 'azure'
                    ? 'e.g. gpt-4o-production'
                    : t('settings.modelCustomPlaceholder'),
                  fetchModelsUnsupported: t('settings.fetchModelsUnsupported'),
                  model: apiProtocol === 'azure'
                    ? t('settings.azureDeploymentModel')
                    : t('settings.model'),
                  required: t('settings.required'),
                  searchPlaceholder: t('designs.searchPlaceholder'),
                  suggestedModelsHint: t('settings.suggestedModelsHint'),
                }}
                model={cfg.model}
                modelSelectRef={modelSelectRef}
                models={apiModelOptions.map((m) => ({
                  id: m.id,
                  label: apiModelOptionLabel(
                    m,
                    !hidesAccountModelSourceLabel(apiProtocol) &&
                    loadedAccountModelCount > 0
                      ? fetchedApiModelIds.has(m.id)
                        ? t('settings.modelSourceAccount')
                        : t('settings.modelSourceSuggested')
                      : undefined,
                  ),
                }))}
                modelsLoadedFromAccountMessage={
                  loadedAccountModelCount > 0
                    ? t(
                        hidesAccountModelSourceLabel(apiProtocol)
                          ? 'settings.modelsLoadedCount'
                          : 'settings.modelsLoadedFromAccount',
                        { count: loadedAccountModelCount },
                      )
                    : null
                }
                providerModelsFailureMessage={providerModelsFailureMessage}
                showAzureModelFetchHint={apiProtocol === 'azure'}
                showFetchModelsUnsupportedHint={apiProtocol === 'ollama'}
                showSuggestedModelsHint={apiProtocol !== 'azure' && !selectedProvider}
                azureModelFetchHint={t('settings.azureModelFetchHint')}
                onCustomModelChange={(value) => updateApiConfig({ model: value })}
                onCustomModelSelect={() => {
                  apiModelUserSelectedRef.current = true;
                  setApiModelCustomEditing(true);
                  updateApiConfig({ model: '' });
                }}
                onFocus={() => {
                  const byokProviderId = byokProtocolToTracking(apiProtocol);
                  if (byokProviderId) {
                    trackSettingsByokFieldClick(analytics.track, {
                      page_name: 'settings',
                      area: 'configure_execution_mode_byok',
                      element: 'model',
                      provider_id: byokProviderId,
                      has_value: Boolean(cfg.model?.trim()),
                    });
                  }
                }}
                onModelSelect={(nextValue) => {
                  apiModelUserSelectedRef.current = true;
                  setApiModelCustomEditing(false);
                  updateApiConfig({ model: nextValue });
                }}
              />
              <details className="agent-cli-env settings-memory-advanced">
                <summary className="agent-cli-env-summary">
                  <span className="agent-cli-env-summary-title">
                    {t('settings.memoryModelInlineLabel')}
                  </span>
                  <span className="settings-memory-summary-value">
                    {cfg.model.trim()
                      ? t('settings.memoryModelInlineSameAsChatWithModel', {
                          model: cfg.model.trim(),
                        })
                      : t('settings.memoryModelInlineSameAsChat')}
                  </span>
                </summary>
                <div className="agent-cli-env-body">
                  <MemoryModelInline
                    mode="api"
                    apiProtocol={apiProtocol}
                    chatApiKey={cfg.apiKey}
                    chatBaseUrl={cfg.baseUrl}
                    chatApiVersion={cfg.apiVersion ?? ''}
                    chatModel={cfg.model}
                    apiModelOptions={apiModelOptions}
                  />
                </div>
              </details>
              {apiProtocol === 'azure' ? (
                <label className="field">
                  <span className="field-label">{t('settings.apiVersion')}</span>
                  <input
                    type="text"
                    value={cfg.apiVersion ?? ''}
                    placeholder="2024-10-21"
                    onBlur={commitProviderModelsInputs}
                    onChange={(e) => updateApiConfig({ apiVersion: e.target.value.trim() })}
                  />
                </label>
              ) : null}
              {apiProtocol === 'senseaudio' || apiProtocol === 'aihubmix' ? (
                <label className="field">
                  <span className="field-label">{t('settings.byokImageModel')}</span>
                  <SearchableModelSelect
                    className="inline-switcher__select settings-model-select settings-model-select--byok"
                    aria-label={t('settings.byokImageModel')}
                    searchPlaceholder={t('designs.searchPlaceholder')}
                    popoverClassName="settings-byok-select-popover"
                    minSearchableOptions={Number.POSITIVE_INFINITY}
                    // Live catalogue from the shared hook: AIHubMix's image
                    // models for aihubmix, the static SenseAudio registry
                    // otherwise. The default-empty option (first entry) resolves
                    // to the registry default on the daemon side.
                    models={[
                      {
                        id: '',
                        label: byokImageModelOptions[0]?.label
                          ? `${byokImageModelOptions[0].label} (${t('settings.byokModelDefaultOption')})`
                          : t('settings.byokModelDefaultOption'),
                      },
                      ...byokImageModelOptions.map((m) => ({ id: m.id, label: m.label })),
                    ]}
                    value={cfg.byokImageModel ?? ''}
                    onChange={(value) =>
                      updateApiConfig({ byokImageModel: value })
                    }
                  />
                </label>
              ) : null}
              {apiProtocol === 'aihubmix' ? (
                <label className="field">
                  <span className="field-label">{t('settings.byokVideoModel')}</span>
                  <select
                    value={cfg.byokVideoModel ?? ''}
                    onChange={(e) =>
                      updateApiConfig({ byokVideoModel: e.target.value })
                    }
                  >
                    {/* Empty resolves to the default video model on the daemon
                        side. The LLM can still override per-call via the tool's
                        `model` arg. */}
                    <option value="">
                      {byokVideoModelOptions[0]?.label
                        ? `${byokVideoModelOptions[0].label} (${t('settings.byokModelDefaultOption')})`
                        : t('settings.byokModelDefaultOption')}
                    </option>
                    {byokVideoModelOptions.map((m) => (
                      <option key={m.id} value={m.id}>
                        {m.label}
                      </option>
                    ))}
                  </select>
                </label>
              ) : null}
              {apiProtocol === 'aihubmix' ? (
                <label className="field">
                  <span className="field-label">{t('settings.byokSpeechModel')}</span>
                  <select
                    value={cfg.byokSpeechModel ?? ''}
                    onChange={(e) => updateApiConfig({ byokSpeechModel: e.target.value })}
                  >
                    <option value="">
                      {byokSpeechModelOptions[0]?.label
                        ? `${byokSpeechModelOptions[0].label} (${t('settings.byokModelDefaultOption')})`
                        : t('settings.byokModelDefaultOption')}
                    </option>
                    {byokSpeechModelOptions.map((m) => (
                      <option key={m.id} value={m.id}>
                        {m.label}
                      </option>
                    ))}
                  </select>
                </label>
              ) : null}
              {apiProtocol === 'aihubmix' ? (
                <label className="field">
                  <span className="field-label">{t('settings.byokSpeechVoice')}</span>
                  <select
                    value={cfg.byokSpeechVoice ?? ''}
                    onChange={(e) => updateApiConfig({ byokSpeechVoice: e.target.value })}
                  >
                    <option value="">alloy (default)</option>
                    {['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'].map((v) => (
                      <option key={v} value={v}>
                        {v}
                      </option>
                    ))}
                  </select>
                </label>
              ) : null}
            </section>
          )}
            </>
          ) : null}

          {activeSection === 'media' ? (
            <MediaProvidersSection
              cfg={cfg}
              setCfg={setCfg}
              mediaProvidersNotice={mediaProvidersNotice}
              onReloadMediaProviders={onReloadMediaProviders}
              pendingLocalProviderIds={pendingMediaProviderEditIds}
              onChange={(providerId) => {
                mediaProvidersChangeVersionRef.current += 1;
                setPendingMediaProviderEditIds((current) => {
                  if (current.has(providerId)) return current;
                  const next = new Set(current);
                  next.add(providerId);
                  return next;
                });
              }}
            />
          ) : null}
          {activeSection === 'integrations' ? <IntegrationsSection /> : null}

          {activeSection === 'mcpClient' ? <McpClientSection /> : null}

          {activeSection === 'composio' ? (
            <ConnectorSection
              cfg={cfg}
              setCfg={setCfg}
              composioConfigLoading={composioConfigLoading}
              onPersistComposioKey={onPersistComposioKey}
              onConnectorAuthResult={({ connectorId, action, result, errorCode }) =>
                trackSettingsConnectorAuthResult(analytics.track, {
                  page_name: 'settings',
                  area: 'connectors',
                  connector_id: connectorId,
                  action,
                  result,
                  ...(errorCode ? { error_code: errorCode } : {}),
                })
              }
            />
          ) : null}

          {activeSection === 'routines' ? <RoutinesSection onClose={onClose} /> : null}

          {activeSection === 'orbit' ? (
            <OrbitSection
              cfg={cfg}
              setCfg={setCfg}
              composioApiKeyConfigured={Boolean(cfg.composio?.apiKeyConfigured)}
              daemonMediaProviders={daemonMediaProviders}
              daemonMediaProvidersFetchState={daemonMediaProvidersFetchState}
              onOpenComposioSection={() => setActiveSection('composio')}
              onLeaveForOrbitProject={(runConfig) => {
                // Persist any in-flight Orbit edits (toggle / time) before
                // navigating away so they aren't silently lost. The autosave
                // loop is best-effort; this synchronous flush guarantees the
                // run-config landed on the daemon before we tear the dialog
                // down. Closing the dialog drops the user on the
                // /projects/orbit view where the agent run streams in.
                void onPersist(runConfig);
                onClose();
              }}
            />
          ) : null}

          {activeSection === 'language' ? (
          <section className="settings-section">
            <div className="settings-language-grid" role="radiogroup" aria-label={t('settings.language')}>
              {LOCALES.map((code) => {
                const active = locale === code;
                return (
                  <button
                    key={code}
                    type="button"
                    role="radio"
                    aria-checked={active}
                    className={`settings-language-tile${active ? ' active' : ''}`}
                    onClick={() => {
                      // P1 ui_click area=language — record the locale id
                      // that was picked, regardless of whether it differs
                      // from the current one (user clicked = signal).
                      trackSettingsLanguageClick(analytics.track, {
                        page_name: 'settings',
                        area: 'language',
                        element: code,
                      });
                      setLocale(code as Locale);
                    }}
                  >
                    <span className="settings-language-tile-text">
                      <span className="settings-language-tile-title">
                        {LOCALE_LABEL[code]}
                      </span>
                      <span className="settings-language-tile-code">
                        {code}
                      </span>
                    </span>
                    {active ? <Icon name="check" size={16} /> : null}
                  </button>
                );
              })}
            </div>
          </section>
          ) : null}

          {activeSection === 'appearance' ? (
            <AppearanceSection cfg={cfg} setCfg={setCfg} />
          ) : null}

          {activeSection === 'critiqueTheater' ? (
            <CritiqueTheaterSection />
          ) : null}

          {activeSection === 'notifications' ? (
            <NotificationsSection cfg={cfg} setCfg={setCfg} />
          ) : null}

          {activeSection === 'pet' ? (
            <PetSettings cfg={cfg} setCfg={setCfg} />
          ) : null}

          {activeSection === 'skills' ? (
            <SkillsSection
              cfg={cfg}
              setCfg={setCfg}
              onSkillsRefresh={onSkillsRefresh}
              onSkillsChanged={onSkillsChanged}
            />
          ) : null}

          {activeSection === 'designSystems' ? (
            <DesignSystemsSection
              cfg={cfg}
              setCfg={setCfg}
              onDesignSystemsChanged={onDesignSystemsChanged}
            />
          ) : null}

          {activeSection === 'projectLocations' ? (
            <ProjectLocationsSection cfg={cfg} setCfg={setCfg} onProjectsRefresh={onProjectsRefresh} />
          ) : null}

          {activeSection === 'instructions' ? (
            <section className="settings-section settings-section-card instructions-rules-section">
              <div className="memory-field-block instructions-rules-card">
                <div className="memory-block-head">
                  <div>
                    <h4>{t('settings.customInstructionsTitle')}</h4>
                    <p className="hint">
                      Fixed instructions OpenDesign follows in every chat. These are
                      not saved memories; use Memory for facts, preferences, and
                      project context.
                    </p>
                  </div>
                </div>
                <textarea
                  className="custom-instructions-input memory-global-rules-input instructions-rules-input"
                  rows={5}
                  maxLength={5000}
                  placeholder={t('settings.customInstructionsPlaceholder')}
                  value={cfg.customInstructions ?? ''}
                  onChange={(event) =>
                    setCfg({
                      ...cfg,
                      customInstructions: event.target.value || undefined,
                    })
                  }
                />
              </div>
            </section>
          ) : null}

          {activeSection === 'memory' ? (
            <MemorySection
              onOpenConnectors={() => setActiveSection('composio')}
              chatAgentId={cfg.mode === 'daemon' ? cfg.agentId ?? null : null}
              chatModel={selectedMemoryChatModel}
            />
          ) : null}

          {activeSection === 'privacy' ? (
            <PrivacySection cfg={cfg} setCfg={setCfg} />
          ) : null}

          {activeSection === 'about' ? (
            <section className="settings-section">
              {appVersionInfo ? (
                <dl className="settings-about-list">
                  <div className="settings-about-version-row">
                    <div className="settings-about-version-left">
                      <dt>{t('settings.appVersion')}</dt>
                      <span className="settings-about-version-num">{appVersionInfo.version}</span>
                    </div>
                    <button
                      type="button"
                      className="settings-about-download-link"
                      disabled={versionChecking}
                      onClick={handleInstallLatest}
                    >
                      {versionChecking ? t('common.loading') : t('settings.installLatest')}
                    </button>
                  </div>
                  <div>
                    <dt>{t('settings.appChannel')}</dt>
                    <dd>{appVersionInfo.channel}</dd>
                  </div>
                  <div>
                    <dt>{t('settings.appRuntime')}</dt>
                    <dd>
                      {appVersionInfo.packaged
                        ? t('settings.runtimePackaged')
                        : t('settings.runtimeDevelopment')}
                    </dd>
                  </div>
                  <div>
                    <dt>{t('settings.appPlatform')}</dt>
                    <dd>{appVersionInfo.platform}</dd>
                  </div>
                  <div>
                    <dt>{t('settings.appArchitecture')}</dt>
                    <dd>{appVersionInfo.arch}</dd>
                  </div>
                </dl>
              ) : (
                <div className="empty-card">{t('settings.versionUnavailable')}</div>
              )}
              <div className="settings-about-diagnostics">
                <div className="settings-about-diagnostics-text">
                  <h4>{t('diagnostics.exportTitle')}</h4>
                  <p className="hint">{t('diagnostics.exportHint')}</p>
                </div>
                <ExportDiagnosticsRow />
              </div>
            </section>
          ) : null}
          {aboutToast ? (
            <Toast
              message={aboutToast}
              onDismiss={() => setAboutToast(null)}
            />
          ) : null}
          </div>
        </div>
      </div>
    </div>
  );
}

/**
 * The four UI states the Composio API key field can be in.
 *
 * `saved-pending` exists so the saved-key indicator stays visible while
 * the user types a draft replacement. Previously the badge was tied to
 * `!hasPendingEdit`, which made it vanish on the first keystroke and
 * trained users to think the original key had already been overwritten
 * (issue #741). Treating "saved key plus draft" as its own state lets
 * the badge stay anchored while the hint text differentiates the
 * unsaved replacement from a fully-saved value.
 */
export type ComposioCredentialState =
  | 'empty'
  | 'pending-new'
  | 'saved'
  | 'saved-pending';

export function deriveComposioCredentialState(
  composio: { apiKey?: string; apiKeyConfigured?: boolean } | null | undefined,
): ComposioCredentialState {
  const hasPendingEdit = Boolean(composio?.apiKey?.trim());
  const hasSavedKey = Boolean(composio?.apiKeyConfigured);
  if (hasSavedKey && hasPendingEdit) return 'saved-pending';
  if (hasSavedKey) return 'saved';
  if (hasPendingEdit) return 'pending-new';
  return 'empty';
}

export function ConnectorSection({
  cfg,
  setCfg,
  composioConfigLoading = false,
  onPersistComposioKey,
  onConnectorsTabClick,
  onConnectorAuthResult,
}: {
  cfg: AppConfig;
  setCfg: Dispatch<SetStateAction<AppConfig>>;
  /** True while the daemon-backed Composio config is still hydrating on
   *  first paint. The credentials surface renders a skeleton over the
   *  input + buttons so the user does not mistake the temporarily empty
   *  input for "no saved key", and so accidental Save/Clear clicks
   *  cannot overwrite the saved state with `''` before hydration lands. */
  composioConfigLoading?: boolean;
  /** Persist the freshly typed Composio API key to the daemon. Returns
   *  once both localStorage and the daemon have caught up so the
   *  section-local Save button can flip from "Saving…" back to idle. */
  onPersistComposioKey: (composio: AppConfig['composio']) => Promise<void> | void;
  /** Optional analytics hook for the integrations surface. The parent
   *  (IntegrationsView) wires this so connectors-tab clicks emit on
   *  `page_name: 'integrations'`; when omitted (SettingsDialog uses the
   *  settings page family instead), no event is fired. */
  onConnectorsTabClick?: (
    element:
      | 'api_key_input'
      | 'save_key'
      | 'clear'
      | 'get_api_key'
      | 'provider_chip'
      | 'search_connectors',
  ) => void;
  /** Analytics hook for the per-connector authorization result. Wired
   *  by the parent so settings_connector_auth_result events fire on
   *  the settings page family. */
  onConnectorAuthResult?: (params: {
    connectorId: string;
    action: 'connect' | 'disconnect' | 'refresh';
    result: 'success' | 'failed' | 'cancelled';
    errorCode?: string;
  }) => void;
}) {
  const { t } = useI18n();
  const composio = cfg.composio ?? {};

  const updateComposio = (patch: NonNullable<AppConfig['composio']>) => {
    setCfg((curr) => ({ ...curr, composio: { ...(curr.composio ?? {}), ...patch } }));
  };
  const credentialState = deriveComposioCredentialState(composio);
  const hasSavedKey = credentialState === 'saved' || credentialState === 'saved-pending';
  const hasPendingEdit = credentialState === 'pending-new' || credentialState === 'saved-pending';
  const apiKeyConfigured = credentialState !== 'empty';
  const savedApiKeyConfigured = Boolean(composio.apiKeyConfigured || hasSavedKey);
  const tail = composio.apiKeyTail?.trim();

  // Section-local save state. The Composio key bypasses the dialog's
  // global autosave loop because it is a secret — we don't want
  // partial-typed keys leaving the browser on every keystroke. The
  // user explicitly clicks "Save key" when they're ready, the request
  // completes, the daemon returns a tail-only echo, and we land in
  // the saved state with the same UI as a key loaded from disk.
  const [keySaveStatus, setKeySaveStatus] =
    useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
  const [catalogRefreshNonce, setCatalogRefreshNonce] = useState(0);
  const keySavedTimerRef = useRef<number | null>(null);
  // Clear the saved-state timer on unmount to avoid setState after unmount
  useEffect(() => {
    return () => {
      if (keySavedTimerRef.current != null) {
        window.clearTimeout(keySavedTimerRef.current);
      }
    };
  }, []);
  const handleSaveKey = async () => {
    if (keySaveStatus === 'saving') return;
    if (!hasPendingEdit) return;
    if (composioConfigLoading) return;
    // Clear any stale timer before transitioning to 'saving' to prevent
    // it from firing during the await and flipping the button back to idle.
    if (keySavedTimerRef.current != null) {
      window.clearTimeout(keySavedTimerRef.current);
      keySavedTimerRef.current = null;
    }
    const pendingKey = composio.apiKey ?? '';
    setKeySaveStatus('saving');
    try {
      await onPersistComposioKey(cfg.composio);
      // Mirror the parent's normalization so the local draft moves
      // into the saved state immediately: drop the secret from the
      // input, mark configured, and store the last-4 tail for the
      // status badge. The parent's setConfig won't propagate back to
      // the dialog because `initial` is read once at mount.
      updateComposio({
        apiKey: '',
        apiKeyConfigured: true,
        apiKeyTail: pendingKey.trim().slice(-4),
      });
      setCatalogRefreshNonce((nonce) => nonce + 1);
      // Clear any existing timer before starting a new one to avoid
      // a stale timeout flipping status back to 'idle' after a
      // subsequent save or clear.
      if (keySavedTimerRef.current != null) {
        window.clearTimeout(keySavedTimerRef.current);
      }
      setKeySaveStatus('saved');
      keySavedTimerRef.current = window.setTimeout(() => {
        setKeySaveStatus('idle');
      }, 2000);
    } catch {
      if (keySavedTimerRef.current != null) {
        window.clearTimeout(keySavedTimerRef.current);
      }
      setKeySaveStatus('error');
      keySavedTimerRef.current = null;
    }
  };

  // Action gating during hydration. Both Save and Clear are dangerous
  // before the daemon's response lands: Save would push whatever the
  // user typed (or didn't type) over the saved key, and Clear would
  // unconditionally wipe it. The skeleton state below makes this
  // visually obvious; the disabled flags here are the safety net.
  const actionsLocked = composioConfigLoading || keySaveStatus === 'saving';
  const saveDisabled = actionsLocked || !hasPendingEdit;
  const clearDisabled = actionsLocked || !apiKeyConfigured;

  // Two-stage destructive confirmation for "Clear". Clearing the saved
  // Composio API key cascades into disconnecting every connector that
  // depends on it, which is irreversible from the UI's standpoint —
  // accounts, OAuth grants, and tool access all unwind. To stop that
  // from happening on a stray click we gate the existing wipe behind
  //   1. an inline warning panel (must click "Continue"), then
  //   2. a final destructive confirmation panel with a brief arming
  //      window so the destructive button cannot be hit by reflex
  //      double-click, then
  //   3. the original clear behavior fires.
  // The panel collapses on Cancel, when the saved key disappears for
  // any other reason, or when the user navigates away from the section.
  const [clearStage, setClearStage] = useState<'idle' | 'confirm' | 'final'>('idle');
  const [clearArmed, setClearArmed] = useState(false);
  const finalConfirmButtonRef = useRef<HTMLButtonElement | null>(null);
  // Reset the flow if the underlying state stops being clearable
  // (e.g. the daemon reloaded and there's nothing saved anymore, or
  // hydration started). This avoids a stale confirmation panel sitting
  // open over a key that no longer exists.
  useEffect(() => {
    if (!apiKeyConfigured || composioConfigLoading) {
      setClearStage('idle');
      setClearArmed(false);
    }
  }, [apiKeyConfigured, composioConfigLoading]);
  // Arm the destructive button after a short delay once the user
  // reaches the final stage. Until then the button is visually hot
  // but inert — this is the "hold on a sec" moment that keeps a
  // reflex Enter / double-click from blowing through both stages.
  useEffect(() => {
    if (clearStage !== 'final') {
      setClearArmed(false);
      return;
    }
    setClearArmed(false);
    const timer = window.setTimeout(() => setClearArmed(true), 700);
    // Pull focus to the final confirm button so keyboard users can
    // see the arming animation finish and choose deliberately rather
    // than tabbing through stale focus state.
    const focusTimer = window.setTimeout(() => {
      finalConfirmButtonRef.current?.focus({ preventScroll: true });
    }, 720);
    return () => {
      window.clearTimeout(timer);
      window.clearTimeout(focusTimer);
    };
  }, [clearStage]);
  const handleClearRequest = () => {
    if (clearDisabled) return;
    setClearStage('confirm');
  };
  const handleClearAbort = () => {
    setClearStage('idle');
    setClearArmed(false);
  };
  const handleClearContinue = () => {
    setClearStage('final');
  };
  const handleClearCommit = async () => {
    if (keySaveStatus === 'saving') return;
    if (!clearArmed) return;
    // Clear any stale timer before transitioning to 'saving', matching
    // handleSaveKey's pattern for consistency.
    if (keySavedTimerRef.current != null) {
      window.clearTimeout(keySavedTimerRef.current);
      keySavedTimerRef.current = null;
    }
    setKeySaveStatus('saving');
    try {
      const cleared = {
        apiKey: '',
        apiKeyConfigured: false,
        apiKeyTail: '',
      };
      await onPersistComposioKey(cleared);
      updateComposio(cleared);
      setCatalogRefreshNonce((nonce) => nonce + 1);
      setClearStage('idle');
      setClearArmed(false);
      setKeySaveStatus('idle');
    } catch {
      if (keySavedTimerRef.current != null) {
        window.clearTimeout(keySavedTimerRef.current);
      }
      setKeySaveStatus('error');
      keySavedTimerRef.current = null;
    }
  };

  return (
    <section className="settings-section settings-section-connectors">

      <label
        className={`field settings-section-connectors-credentials${composioConfigLoading ? ' is-loading' : ''}`}
        aria-busy={composioConfigLoading || undefined}
      >
        <span className="field-label-row">
          <span className="field-label-group">
            <span className="field-label">{t('settings.connectorsComposioApiKey')}</span>
            {composioConfigLoading ? (
              // Skeleton chip stands in for the "Saved · ••••XXXX" badge
              // while we wait for the daemon. Same footprint as the real
              // chip so the row geometry doesn't jump on resolve.
              <span
                className="field-status-badge field-status-badge-skeleton"
                aria-hidden="true"
              />
            ) : hasSavedKey ? (
              <span
                className="field-status-badge"
                title={t('settings.connectorsSavedTitle')}
              >
                {tail
                  ? t('settings.connectorsSavedWithTail', { tail })
                  : t('settings.connectorsSaved')}
              </span>
            ) : null}
          </span>
          <a
            className="field-label-link"
            href="https://app.composio.dev"
            target="_blank"
            rel="noreferrer"
            onClick={() => onConnectorsTabClick?.('get_api_key')}
          >
            {t('settings.connectorsGetApiKey')}
            <Icon name="external-link" size={11} />
          </a>
        </span>
        <div className="field-row">
          {/* Wrap the password input so the shimmer overlay can sit on
              top of it without affecting layout. The input itself stays
              mounted (rather than swapped for a placeholder div) so the
              browser keeps any in-progress autofill, focus, and
              accessibility tree intact when hydration completes. */}
          <span className="field-input-skeleton-wrap">
            <input
              type="password"
              value={composio.apiKey ?? ''}
              placeholder={
                composioConfigLoading
                  ? t('settings.connectorsLoadingSavedKey')
                  : hasSavedKey
                    ? t('settings.connectorsReplaceKeyPlaceholder')
                    : t('settings.connectorsApiKeyPlaceholder')
              }
              onFocus={() => onConnectorsTabClick?.('api_key_input')}
              onChange={(e) => updateComposio({ apiKey: e.target.value })}
              onKeyDown={(e) => {
                // Enter from the password field commits the key — the
                // most common save gesture for credential fields, and
                // it removes the need to mouse over to the button.
                if (
                  e.key === 'Enter'
                  && hasPendingEdit
                  && keySaveStatus !== 'saving'
                  && !composioConfigLoading
                ) {
                  e.preventDefault();
                  void handleSaveKey();
                }
              }}
              disabled={composioConfigLoading}
              aria-describedby="composio-api-key-help"
            />
            {composioConfigLoading ? (
              <span className="field-input-skeleton-shimmer" aria-hidden="true" />
            ) : null}
          </span>
          <button
            type="button"
            className={'primary settings-connectors-save' + (keySaveStatus === 'saving' ? ' is-busy' : '')}
            disabled={saveDisabled}
            onClick={() => {
              onConnectorsTabClick?.('save_key');
              void handleSaveKey();
            }}
            title={
              composioConfigLoading
                ? t('settings.connectorsLoadingSavedKey')
                : t('settings.connectorsSaveKeyTitle')
            }
          >
            {keySaveStatus === 'saving' ? (
              <>
                <Icon name="spinner" size={12} className="icon-spin" />
                <span>{t('settings.connectorsKeySaving')}</span>
              </>
            ) : keySaveStatus === 'saved' ? (
              <>
                <Icon name="check" size={12} />
                <span>{t('settings.connectorsKeySaved')}</span>
              </>
            ) : (
              t('settings.connectorsSaveKey')
            )}
          </button>
          <button
            type="button"
            className={
              'ghost settings-connectors-clear'
              + (clearStage !== 'idle' ? ' is-arming' : '')
            }
            disabled={clearDisabled}
            title={
              composioConfigLoading
                ? t('settings.connectorsLoadingSavedKey')
                : undefined
            }
            aria-expanded={clearStage !== 'idle'}
            aria-controls="composio-clear-confirm"
            onClick={() => {
              onConnectorsTabClick?.('clear');
              handleClearRequest();
            }}
          >
            {t('settings.connectorsClear')}
          </button>
        </div>
        {/* Two-stage destructive confirmation panel. Lives inside the
            credentials field so it visually grows out of the row that
            owns the action, instead of floating disconnected at the
            bottom of the section. The panel is destructive-styled
            (red border + soft red bg) and uses an alertdialog role so
            screen readers treat it as a modal blocker for the field. */}
        {clearStage !== 'idle' ? (
          <div
            id="composio-clear-confirm"
            className={
              'settings-connectors-clear-confirm is-' + clearStage
              + (clearStage === 'final' && clearArmed ? ' is-armed' : '')
            }
            role="alertdialog"
            aria-modal="false"
            aria-labelledby="composio-clear-confirm-title"
            aria-describedby="composio-clear-confirm-body"
          >
            <div className="settings-connectors-clear-confirm-icon" aria-hidden="true">
              <span className="settings-connectors-clear-confirm-glyph">!</span>
            </div>
            <div className="settings-connectors-clear-confirm-copy">
              <strong id="composio-clear-confirm-title">
                {clearStage === 'final'
                  ? t('settings.connectorsClearFinalTitle')
                  : t('settings.connectorsClearConfirmTitle')}
              </strong>
              <span id="composio-clear-confirm-body">
                {clearStage === 'final'
                  ? t('settings.connectorsClearFinalBody')
                  : t('settings.connectorsClearConfirmBody')}
              </span>
            </div>
            <div className="settings-connectors-clear-confirm-actions">
              <button
                type="button"
                className="ghost"
                onClick={handleClearAbort}
              >
                {t('settings.connectorsClearCancel')}
              </button>
              {clearStage === 'confirm' ? (
                <button
                  type="button"
                  className="settings-connectors-clear-step"
                  onClick={handleClearContinue}
                >
                  {t('settings.connectorsClearConfirmContinue')}
                  <Icon name="chevron-right" size={12} />
                </button>
              ) : (
                <button
                  ref={finalConfirmButtonRef}
                  type="button"
                  className={
                    'settings-connectors-clear-commit'
                    + (clearArmed ? ' is-armed' : '')
                  }
                  onClick={handleClearCommit}
                  disabled={!clearArmed}
                  aria-disabled={!clearArmed}
                >
                  <span className="settings-connectors-clear-commit-arm" aria-hidden="true" />
                  <span className="settings-connectors-clear-commit-label">
                    {clearArmed ? (
                      t('settings.connectorsClearFinalConfirm')
                    ) : (
                      <>
                        <Icon name="spinner" size={12} className="icon-spin" />
                        {t('settings.connectorsClearArming')}
                      </>
                    )}
                  </span>
                </button>
              )}
            </div>
          </div>
        ) : null}
        <span
          id="composio-api-key-help"
          className={`hint${composioConfigLoading ? ' field-hint-loading' : ''}`}
          role={composioConfigLoading ? 'status' : undefined}
          aria-live={composioConfigLoading ? 'polite' : undefined}
        >
          {composioConfigLoading ? (
            <>
              <Icon name="spinner" size={11} className="icon-spin" />
              <span>{t('settings.connectorsLoadingSavedKey')}</span>
            </>
          ) : keySaveStatus === 'error'
            ? t('settings.connectorsKeyError')
            : hasSavedKey
              ? t('settings.connectorsHelpSaved')
              : apiKeyConfigured
                ? t('settings.connectorsHelpUnsaved')
                : t('settings.connectorsHelpEmpty')}
        </span>
      </label>

      <ConnectorsBrowser
        composioConfigured={savedApiKeyConfigured}
        catalogRefreshKey={`${savedApiKeyConfigured ? 'configured' : 'empty'}:${tail ?? ''}:${catalogRefreshNonce}`}
        {...(onConnectorsTabClick ? { onConnectorsTabClick } : {})}
        {...(onConnectorAuthResult ? { onConnectorAuthResult } : {})}
      />
    </section>
  );
}

interface OrbitRunStartResponse {
  projectId: string;
  agentRunId: string;
}

export async function persistConfigAndRunOrbit(
  config: AppConfig,
  options?: {
    daemonProviders?: AppConfig['mediaProviders'] | null;
    syncMediaProviders?: boolean;
    locale?: string | null;
  },
): Promise<OrbitRunStartResponse> {
  if (options?.syncMediaProviders !== false) {
    await syncMediaProvidersToDaemon(config.mediaProviders, {
      daemonProviders: options?.daemonProviders,
    });
  }
  await syncConfigToDaemon(config, { throwOnError: true });
  const response = await fetch('/api/orbit/run', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ locale: options?.locale ?? null }),
  });
  if (!response.ok) throw new Error('Orbit run failed');
  return await response.json() as OrbitRunStartResponse;
}

export function configForManualOrbitRun(config: AppConfig): AppConfig {
  const effectiveTemplateSkillId = config.orbit?.templateSkillId || DEFAULT_ORBIT.templateSkillId || '';
  if (!effectiveTemplateSkillId) return config;
  return {
    ...config,
    orbit: {
      ...(config.orbit ?? DEFAULT_ORBIT),
      templateSkillId: effectiveTemplateSkillId,
    },
  };
}

export function isOrbitRunDisabled(isBusy: boolean, connectedCount: number | null): boolean {
  return isBusy || connectedCount === null || connectedCount === 0;
}

function formatRelative(
  iso: string | undefined | null,
  t: (key: keyof Dict, vars?: Record<string, string | number>) => string,
): string | null {
  if (!iso) return null;
  const then = new Date(iso).getTime();
  if (Number.isNaN(then)) return null;
  const diffMs = Date.now() - then;
  const absMin = Math.round(Math.abs(diffMs) / 60_000);
  if (absMin < 1) return t('common.justNow');
  if (absMin < 60) return t('common.minutesAgo', { n: absMin });
  const absHr = Math.round(absMin / 60);
  if (absHr < 24) return t('common.hoursAgo', { n: absHr });
  const absDay = Math.round(absHr / 24);
  return t('common.daysAgo', { n: absDay });
}

function OrbitSection({
  cfg,
  setCfg,
  composioApiKeyConfigured,
  daemonMediaProviders,
  daemonMediaProvidersFetchState,
  onOpenComposioSection,
  onLeaveForOrbitProject,
}: {
  cfg: AppConfig;
  setCfg: Dispatch<SetStateAction<AppConfig>>;
  /** Whether the user has already saved a Composio API key. Drives the
   *  Orbit configuration gate's copy/CTA. When false the gate explains
   *  that Orbit needs Composio first; when true (key present, just no
   *  connectors yet) it nudges the user toward the connector catalog. */
  composioApiKeyConfigured: boolean;
  daemonMediaProviders?: AppConfig['mediaProviders'] | null;
  daemonMediaProvidersFetchState?: 'idle' | 'ok' | 'error';
  /** Switch the parent settings dialog to the Connectors (Composio) tab.
   *  Used by the Orbit gate's primary CTA so the user can fix the
   *  prerequisite without leaving the dialog. */
  onOpenComposioSection: () => void;
  /** Called right before navigating to the generated Orbit project so the
   *  parent dialog can persist any unsaved Orbit edits and close itself. */
  onLeaveForOrbitProject: (runConfig: AppConfig) => void;
}) {
  const { locale, t } = useI18n();
  const orbit = cfg.orbit ?? DEFAULT_ORBIT;
  const [status, setStatus] = useState<OrbitStatusResponse | null>(null);
  const [running, setRunning] = useState(false);
  const [notice, setNotice] = useState<{ kind: 'success' | 'error'; message: string } | null>(null);
  const [copied, setCopied] = useState(false);
  const [legacyLastRunTemplateSkillId, setLegacyLastRunTemplateSkillId] = useState<string | null>(null);
  const legacyLastRunIdentity = status?.lastRun?.id
    ?? `${status?.lastRun?.completedAt ?? ''}:${status?.lastRun?.agentRunId ?? ''}:${status?.lastRun?.markdown ?? ''}`;
  // Orbit templates ship under the renderable design-templates registry after
  // the skills/design-templates split. We fetch on mount and keep three states
  // for graceful UX: `null` = still loading, `[]` = loaded with no Orbit
  // templates available, `SkillSummary[]` = ready. If the daemon is offline
  // the call resolves with [] (see fetchDesignTemplates) so the section never
  // throws — the rest of the Orbit controls keep working.
  const [orbitTemplates, setOrbitTemplates] = useState<SkillSummary[] | null>(null);
  // Connector presence drives the configuration gate at the top of the Orbit
  // tab. We track three states: `null` = still loading (skip rendering the
  // gate so it doesn't flash before data arrives), `0` = no connectors
  // present (gate is shown), `>0` = at least one connected integration
  // (gate is hidden). We only count connectors with `status === 'connected'`
  // because the catalog itself ships hundreds of available rows — what
  // matters for Orbit is whether anything has actually been wired up.
  const [connectedCount, setConnectedCount] = useState<number | null>(null);
  // Once the user clicks Generate we close Settings and navigate away. The ref
  // lets late-arriving handlers no-op without React warnings.
  const isMountedRef = useRef(true);
  useEffect(() => {
    // React Strict Mode replays mount effects in development. Reset the ref on
    // each setup so the synthetic cleanup from the first pass does not leave
    // async Orbit status / connector refreshes permanently thinking the panel
    // has unmounted.
    isMountedRef.current = true;
    return () => {
      isMountedRef.current = false;
    };
  }, []);

  const updateOrbit = (patch: Partial<NonNullable<AppConfig['orbit']>>) => {
    setCfg((curr) => ({
      ...curr,
      orbit: { ...(curr.orbit ?? DEFAULT_ORBIT), ...patch },
    }));
  };

  const refreshStatus = async () => {
    try {
      const response = await fetch('/api/orbit/status');
      if (!response.ok) return;
      if (!isMountedRef.current) return;
      setStatus(await response.json() as OrbitStatusResponse);
    } catch {
      // Daemon may be offline in API-only development; keep local controls usable.
    }
  };

  useEffect(() => {
    void refreshStatus();
  }, []);

  useEffect(() => {
    if (!status?.running) return undefined;
    const interval = window.setInterval(() => {
      void refreshStatus();
    }, 3000);
    return () => window.clearInterval(interval);
  }, [status?.running]);

  // Fetch the design-template registry once on mount and filter to
  // scenario === 'orbit'. We tolerate fetch failure:
  // fetchDesignTemplates already swallows errors and returns []. The
  // component then transitions from "loading" → "empty" and the rest of the
  // Orbit panel stays fully functional.
  useEffect(() => {
    let alive = true;
    void (async () => {
      const all = await fetchDesignTemplates();
      if (!alive) return;
      const filtered = all.filter((s) => s.scenario === 'orbit');
      // Stable order: featured first (higher number = more featured), then by name.
      filtered.sort((a, b) => {
        const af = a.featured ?? 0;
        const bf = b.featured ?? 0;
        if (af !== bf) return bf - af;
        return a.name.localeCompare(b.name);
      });
      setOrbitTemplates(filtered);
    })();
    return () => {
      alive = false;
    };
  }, []);

  const refreshConnectedCount = useCallback(async () => {
    const list = await fetchConnectors();
    if (!isMountedRef.current) return;
    const connected = list.filter((c) => c.status === 'connected').length;
    setConnectedCount(connected);
  }, []);

  // Fetch the connector catalog on mount to determine whether the Orbit
  // configuration gate should render. fetchConnectors swallows errors and
  // returns []; if the daemon is offline we treat that as "0 connected" and
  // surface the gate so the user has a clear path forward instead of being
  // dropped into a broken Orbit configuration.
  useEffect(() => {
    void refreshConnectedCount();
  }, [refreshConnectedCount]);

  // Connector auth often completes in another window. Re-check when focus
  // returns so the Orbit gate reflects newly connected accounts without
  // requiring the user to close and reopen Settings.
  useEffect(() => {
    const onFocus = () => {
      void refreshConnectedCount();
    };
    window.addEventListener('focus', onFocus);
    return () => window.removeEventListener('focus', onFocus);
  }, [refreshConnectedCount]);

  // The id used to drive the prompt template — coalesces a null/empty
  // saved value to the built-in default (DEFAULT_ORBIT.templateSkillId,
  // currently 'orbit-general'). The select no longer offers a "no template"
  // option, so legacy configs that stored null are presented as if they
  // were on the default. Manual runs persist this effective value before
  // launching so the daemon uses the same template the UI displays.
  const effectiveTemplateSkillId = orbit.templateSkillId || DEFAULT_ORBIT.templateSkillId || '';
  const supportsTemplateScopedHistory = status?.lastRunsByTemplate !== undefined;

  useEffect(() => {
    const hasTemplateScopedHistory = Object.keys(status?.lastRunsByTemplate ?? {}).length > 0;
    const hasLegacyUnscopedLastRun = Boolean(status?.lastRun && !status.lastRun.templateSkillId);
    if (!hasLegacyUnscopedLastRun || hasTemplateScopedHistory) {
      setLegacyLastRunTemplateSkillId(null);
      return;
    }
    setLegacyLastRunTemplateSkillId((current) => current ?? (effectiveTemplateSkillId || null));
  }, [effectiveTemplateSkillId, legacyLastRunIdentity, status]);

  const selectedTemplate = useMemo(() => {
    if (!effectiveTemplateSkillId || !orbitTemplates) return null;
    return orbitTemplates.find((s) => s.id === effectiveTemplateSkillId) ?? null;
  }, [effectiveTemplateSkillId, orbitTemplates]);

  const triggerNow = () => {
    if (running) return;
    setRunning(true);
    setNotice(null);

    void (async () => {
      try {
        const runConfig = configForManualOrbitRun(cfg);
        const payload = await persistConfigAndRunOrbit(runConfig, {
          daemonProviders: daemonMediaProviders,
          syncMediaProviders: daemonMediaProvidersFetchState === 'ok',
          locale,
        });
        if (!payload.projectId) throw new Error('Orbit run did not return a project');

        onLeaveForOrbitProject(runConfig);
        navigateRoute({
          kind: 'project',
          projectId: payload.projectId,
          conversationId: null,
          fileName: null,
        });
      } catch {
        if (!isMountedRef.current) return;
        setNotice({
          kind: 'error',
          message: t('settings.orbit.runError'),
        });
      } finally {
        if (!isMountedRef.current) return;
        setRunning(false);
        void refreshStatus();
      }
    })();
  };

  const templateScopedLastRun = effectiveTemplateSkillId
    ? status?.lastRunsByTemplate?.[effectiveTemplateSkillId] ?? null
    : null;
  const hasLegacyUnscopedLastRun = Boolean(
    status?.lastRun
    && !status.lastRun.templateSkillId
    && legacyLastRunTemplateSkillId
    && legacyLastRunTemplateSkillId === effectiveTemplateSkillId,
  );
  const lastRun = supportsTemplateScopedHistory
    ? (templateScopedLastRun ?? (hasLegacyUnscopedLastRun ? status?.lastRun ?? null : null))
    : status?.lastRun ?? null;
  const nextRunLabel = status?.nextRunAt ? new Date(status.nextRunAt).toLocaleString() : null;
  const lastRunAbs = lastRun ? new Date(lastRun.completedAt).toLocaleString() : null;
  const lastRunRel = formatRelative(lastRun?.completedAt, t);
  const liveArtifactHref = lastRun?.artifactId && lastRun?.artifactProjectId
    ? `/api/live-artifacts/${encodeURIComponent(lastRun.artifactId)}/preview?projectId=${encodeURIComponent(lastRun.artifactProjectId)}`
    : null;
  const isBusy = running || Boolean(status?.running);

  const copyMarkdown = async () => {
    if (!lastRun?.markdown) return;
    try {
      await navigator.clipboard.writeText(lastRun.markdown);
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1600);
    } catch {
      // Clipboard access may be denied in some browsing contexts; silently skip.
    }
  };

  // Proportional widths for the run-result meter. We avoid showing 0-width
  // segments by falling back to a tiny sliver when a category has hits but
  // rounds to 0% — the visual "something happened here" cue matters more
  // than exact proportion at low counts.
  const total = lastRun
    ? Math.max(
        lastRun.connectorsSucceeded + lastRun.connectorsSkipped + lastRun.connectorsFailed,
        1,
      )
    : 1;
  const segPct = (n: number) => {
    if (!lastRun || n <= 0) return 0;
    const pct = (n / total) * 100;
    return pct < 3 ? 3 : pct;
  };
  const meterSucceeded = lastRun ? segPct(lastRun.connectorsSucceeded) : 0;
  const meterSkipped = lastRun ? segPct(lastRun.connectorsSkipped) : 0;
  const meterFailed = lastRun ? segPct(lastRun.connectorsFailed) : 0;

  const automationState = orbit.enabled ? 'active' : 'off';
  const triggerLabel = lastRun?.trigger === 'manual'
    ? t('settings.orbit.triggerManual')
    : t('settings.orbit.triggerScheduled');

  // Surface the configuration gate when we know for sure that the user has
  // no connected integrations. While `connectedCount === null` we are still
  // loading and intentionally hide the gate so the panel doesn't flash an
  // empty-state warning before data arrives. Once resolved, `0` triggers
  // the gate. The gate's copy + CTA branch on whether a Composio API key
  // has been saved: missing key → push toward configuring Composio first;
  // key present, no connections → push toward picking an integration.
  const showConfigGate = connectedCount === 0;
  const gateBodyKey = composioApiKeyConfigured
    ? 'settings.orbit.gateBody'
    : 'settings.orbit.gateBodyNoKey';
  const gateActionKey = composioApiKeyConfigured
    ? 'settings.orbit.gateAction'
    : 'settings.orbit.gateActionNoKey';
  // Disable the hero's "Run it now" CTA while the gate is visible: running
  // without any connector wired up surfaces a cryptic backend error. We
  // keep the button mounted so layout stays stable; a tooltip and the
  // adjacent gate make the disabled reason obvious.
  const runDisabled = isOrbitRunDisabled(isBusy, connectedCount);
  const runDisabledTitle = showConfigGate
    ? t('settings.orbit.gateTitle')
    : t('settings.orbit.runTitle');

  // When the configuration gate is visible (no connector available) we
  // also lock down every secondary control on the panel — schedule
  // toggle, time input, prompt template select, and the missing-template
  // Reset button. Touching any of them before a connector exists either
  // produces a no-op or persists state the user can't actually exercise.
  // Locking them keeps the panel honest, prevents "ghost configuration",
  // and reinforces the gate's CTA as the only meaningful next step.
  const controlsLocked = showConfigGate;
  const controlsLockedHint = controlsLocked
    ? t('settings.orbit.controlsLockedHint')
    : undefined;

  return (
    <section className="settings-section orbit-section">
      {/* ---------- 1. HEADER ZONE ---------- */}
      <header className="orbit-hero">
        <div className="orbit-hero-mark" aria-hidden="true">
          <Icon name="refresh" size={20} />
        </div>
        <div className="orbit-hero-copy">
          <span className="orbit-hero-eyebrow">{t('settings.orbit.eyebrow')}</span>
          <h3 className="orbit-hero-title">{t('settings.orbit.title')}</h3>
          <p className="orbit-hero-lede">
            {t('settings.orbit.lede')}
          </p>
        </div>
        <div className="orbit-hero-actions">
          <span
            className={`orbit-state-pill orbit-state-${automationState}`}
            title={
              orbit.enabled
                ? t('settings.orbit.statusOnTitle')
                : t('settings.orbit.statusOffTitle')
            }
          >
            <span className="orbit-state-dot" aria-hidden="true" />
            {orbit.enabled
              ? t('settings.orbit.statusActive')
              : t('settings.orbit.statusOff')}
          </span>
          <button
            type="button"
            className={'orbit-run-cta' + (isBusy ? ' is-busy' : '')}
            onClick={() => void triggerNow()}
            disabled={runDisabled}
            title={runDisabledTitle}
          >
            {isBusy ? (
              <>
                <Icon name="spinner" size={14} className="icon-spin" />
                <span>{t('settings.orbit.running')}</span>
              </>
            ) : (
              <>
                <Icon name="play" size={14} />
                <span>{t('settings.orbit.runOpen')}</span>
              </>
            )}
          </button>
        </div>
      </header>

      {/* ---------- 1b. CONFIGURATION GATE ----------
          Renders when no connected integrations are present. Orbit's job is
          to summarize connector activity, so without any wired-up
          connector there is literally nothing for it to report on.
          The gate uses the same orbit-themed accent surface as the
          automation card to feel like a first-class part of the panel
          rather than an inline error, and routes the user back to the
          Connectors tab inside the same settings dialog (no navigation
          off the page). The copy/CTA branch on whether a Composio API
          key has been saved already, because the prerequisite chain is:
          API key → connector connected → Orbit can run. */}
      {showConfigGate ? (
        <div
          className="orbit-config-gate"
          role="region"
          aria-label={t('settings.orbit.gateAriaLabel')}
          data-testid="orbit-config-gate"
        >
          <div className="orbit-config-gate-glyph" aria-hidden="true">
            <span className="orbit-config-gate-ring orbit-config-gate-ring-outer" />
            <span className="orbit-config-gate-ring orbit-config-gate-ring-inner" />
            <span className="orbit-config-gate-icon">
              <Icon name="link" size={16} />
            </span>
          </div>
          <div className="orbit-config-gate-copy">
            <span className="orbit-config-gate-eyebrow">
              {t('settings.orbit.gateEyebrow')}
            </span>
            <h4 className="orbit-config-gate-title">
              {t('settings.orbit.gateTitle')}
            </h4>
            <p className="orbit-config-gate-body">
              {t(gateBodyKey)}
            </p>
          </div>
          <div className="orbit-config-gate-actions">
            <button
              type="button"
              className="orbit-config-gate-action"
              onClick={onOpenComposioSection}
              data-testid="orbit-config-gate-action"
            >
              <span>{t(gateActionKey)}</span>
              <Icon name="chevron-right" size={13} />
            </button>
          </div>
        </div>
      ) : null}

      {/* ---------- 2. AUTOMATION CARD ----------
          Single unified configuration surface for Orbit: the daily-summary
          switch, the run-time schedule, and the prompt-template selection
          all live inside one card, separated by hairline dividers. The
          template row was previously a parallel card; folding it in here
          collapses the "two paired panels" pattern into one cohesive
          stack so users configure Orbit in one place. */}
      <div
        className={`orbit-automation${orbit.enabled ? ' is-on' : ''}${selectedTemplate ? ' has-template' : ''}${controlsLocked ? ' is-locked' : ''}`}
        aria-busy={orbitTemplates === null || undefined}
        aria-disabled={controlsLocked || undefined}
        data-testid="orbit-automation-card"
      >
        {controlsLocked ? (
          <div
            className="orbit-automation-lock-banner"
            role="note"
            aria-label={t('settings.orbit.controlsLockedHint')}
          >
            <Icon name="link" size={12} />
            <span className="orbit-automation-lock-badge">
              {t('settings.orbit.controlsLockedBadge')}
            </span>
            <span className="orbit-automation-lock-text">
              {t('settings.orbit.controlsLockedHint')}
            </span>
          </div>
        ) : null}
        <div className="orbit-automation-row orbit-automation-switch-row">
          <div className="orbit-automation-label">
            <span className="orbit-automation-title">{t('settings.orbit.dailySummaryTitle')}</span>
            <span className="orbit-automation-sub">
              {t('settings.orbit.dailySummarySub')}
            </span>
          </div>
          <button
            type="button"
            role="switch"
            aria-checked={orbit.enabled}
            aria-disabled={controlsLocked || undefined}
            className={`orbit-switch${orbit.enabled ? ' is-on' : ''}${controlsLocked ? ' is-locked' : ''}`}
            disabled={controlsLocked}
            title={controlsLockedHint}
            onClick={() => updateOrbit({ enabled: !orbit.enabled })}
          >
            <span className="orbit-switch-track" aria-hidden="true">
              <span className="orbit-switch-thumb" />
            </span>
            <span className="orbit-switch-text">
              {orbit.enabled ? t('settings.orbit.on') : t('settings.orbit.off')}
            </span>
          </button>
        </div>

        <div className="orbit-automation-divider" aria-hidden="true" />

        <div className="orbit-automation-row orbit-automation-schedule-row">
          <div className="orbit-automation-label">
            <span className="orbit-automation-title">{t('settings.orbit.runTimeTitle')}</span>
            <span className="orbit-automation-sub">
              {t('settings.orbit.runTimeSub')}
            </span>
          </div>
          <div className="orbit-automation-schedule-controls">
            <input
              type="time"
              className="orbit-time-input"
              value={orbit.time}
              onChange={(e) => updateOrbit({ time: e.target.value || DEFAULT_ORBIT.time })}
              aria-label={t('settings.orbit.runTimeAria')}
              aria-disabled={controlsLocked || undefined}
              disabled={controlsLocked}
              title={controlsLockedHint}
            />
            <div className="orbit-next-run" aria-live="polite">
              {orbit.enabled ? (
                nextRunLabel ? (
                  <>
                    <span className="orbit-next-run-label">{t('settings.orbit.nextRun')}</span>
                    <span className="orbit-next-run-value">{nextRunLabel}</span>
                  </>
                ) : (
                  <>
                    <span className="orbit-next-run-label">{t('settings.orbit.nextRun')}</span>
                    <span className="orbit-next-run-value muted">{t('settings.orbit.nextRunScheduledAfterSave')}</span>
                  </>
                )
              ) : (
                <>
                  <span className="orbit-next-run-label">{t('settings.orbit.schedule')}</span>
                  <span className="orbit-next-run-value muted">{t('settings.orbit.pausedManualOnly')}</span>
                </>
              )}
            </div>
          </div>
        </div>

        <div className="orbit-automation-divider" aria-hidden="true" />

        {/* Prompt template row — folded into the automation card so users
            configure schedule and prompt steering in one place. The select
            picks which scenario === 'orbit' skill template gets injected
            into the Orbit prompt. There is no separate preview slab below
            the select: the dropdown's option label is the source of
            truth for the active template, and each option carries the
            skill description as a `title` tooltip. The only state that
            still needs explicit surfacing is "saved id no longer in the
            registry" — that warning replaces the row's normal sub-copy
            and inlines a Reset action when the missing id differs from
            the default. */}
        <div className="orbit-automation-row orbit-automation-template-row">
          <div className="orbit-automation-label">
            {/* Title aligns with the other automation rows ("Daily summary",
                "Run time") — a single short label. */}
            <span className="orbit-automation-title">{t('settings.orbit.templateTitle')}</span>
            {orbitTemplates &&
            effectiveTemplateSkillId &&
            !orbitTemplates.some((s) => s.id === effectiveTemplateSkillId) ? (
              // The saved skill id is no longer installed — surface a
              // soft warning right under the title, with an inline Reset
              // action that pushes back to DEFAULT_ORBIT (currently
              // `orbit-general`). Reset is hidden when the missing id
              // already equals the default, so the control never loops
              // on itself.
              <span
                className="orbit-automation-sub orbit-automation-sub-warning"
                role="status"
              >
                <Icon name="history" size={11} />
                <span>
                  {t('settings.orbit.templateMissing', { id: effectiveTemplateSkillId })}{' '}
                  {orbitTemplates.length === 0
                    ? t('settings.orbit.templateMissingInstall')
                    : t('settings.orbit.templateMissingPickAnother')}
                </span>
                {DEFAULT_ORBIT.templateSkillId &&
                effectiveTemplateSkillId !== DEFAULT_ORBIT.templateSkillId ? (
                  <button
                    type="button"
                    className="orbit-automation-sub-action"
                    disabled={controlsLocked}
                    aria-disabled={controlsLocked || undefined}
                    onClick={() =>
                      updateOrbit({ templateSkillId: DEFAULT_ORBIT.templateSkillId })
                    }
                    title={
                      controlsLocked
                        ? t('settings.orbit.controlsLockedHint')
                        : t('settings.orbit.templateResetTitle', {
                            id: DEFAULT_ORBIT.templateSkillId,
                          })
                    }
                  >
                    {t('settings.orbit.templateReset')}
                  </button>
                ) : null}
              </span>
            ) : (
              <span className="orbit-automation-sub">
                {t('settings.orbit.templateHelp')}
              </span>
            )}
          </div>
          <div className="orbit-automation-template-controls">
            <div className="orbit-template-select">
              <div className="orbit-template-select-wrap">
                <select
                  id="orbit-template-select"
                  className="orbit-template-select-input"
                  aria-label={t('settings.orbit.templateAria')}
                  aria-disabled={controlsLocked || undefined}
                  value={effectiveTemplateSkillId}
                  disabled={orbitTemplates === null || controlsLocked}
                  title={controlsLockedHint}
                  onChange={(e) => {
                    const next = e.target.value;
                    // Guard against the loading placeholder making it
                    // through onChange — only persist real skill ids.
                    if (!next) return;
                    updateOrbit({ templateSkillId: next });
                  }}
                >
                  {/* While the skill registry is still loading we render a
                      single non-interactive placeholder so the select has
                      a value to display. Once `orbitTemplates` resolves we
                      drop the placeholder entirely — the dropdown lists
                      only real Orbit skill templates, so there is no
                      "no template" / "use built-in" option to pick. */}
                  {orbitTemplates === null ? (
                    <option value="">{t('settings.orbit.templatesLoading')}</option>
                  ) : null}
                  {/* If the saved id no longer exists in the registry,
                      surface it as a hidden placeholder so the controlled
                      <select> doesn't fall back to the first real option
                      and silently mutate the user's stored choice. The
                      inline warning above offers the explicit Reset
                      action. */}
                  {orbitTemplates &&
                  effectiveTemplateSkillId &&
                  !orbitTemplates.some((s) => s.id === effectiveTemplateSkillId) ? (
                    <option value={effectiveTemplateSkillId} hidden>
                      {t('settings.orbit.templateMissingOption', {
                        id: effectiveTemplateSkillId,
                      })}
                    </option>
                  ) : null}
                  {orbitTemplates && orbitTemplates.length > 0 ? (
                    <optgroup label={t('settings.orbit.templatesOptgroup')}>
                      {orbitTemplates.map((s) => (
                        <option
                          key={s.id}
                          value={s.id}
                          // Browser-native tooltip — surfaces the skill
                          // description on hover without needing a
                          // dedicated preview panel.
                          title={s.description ?? undefined}
                        >
                          {s.name}
                        </option>
                      ))}
                    </optgroup>
                  ) : null}
                </select>
                <Icon
                  name="chevron-down"
                  size={12}
                  className="orbit-template-select-chevron"
                />
              </div>
            </div>
          </div>
        </div>
      </div>

      {/* ---------- 4. RUN RESULT / RECEIPT ---------- */}
      {/* When there is no last run yet, the "receipt" metaphor doesn't fit —
          there's nothing to report. We swap to a first-run prompt with its
          own composed layout (orbit-glyph · copy · inline CTA) so the empty
          state feels intentional and rhythmically balanced with the hero,
          automation card, and (eventual) artifact strip. */}
      {lastRun ? (
        <div className="orbit-receipt">
          <div className="orbit-receipt-head">
            <div className="orbit-receipt-head-left">
              <span className="orbit-receipt-eyebrow">
                <Icon name="history" size={12} />
                {t('settings.orbit.lastRun')}
              </span>
              <span
                className="orbit-receipt-timestamp"
                title={lastRunAbs ?? undefined}
              >
                {lastRunRel ?? lastRunAbs}
              </span>
            </div>
            <span
              className={`orbit-trigger-pill orbit-trigger-${lastRun.trigger ?? 'scheduled'}`}
            >
              {triggerLabel}
            </span>
          </div>

          {notice ? (
            <div
              className={`orbit-inline-notice is-${notice.kind}`}
              role={notice.kind === 'error' ? 'alert' : 'status'}
            >
              <Icon name={notice.kind === 'error' ? 'close' : 'check'} size={12} />
              <span>{notice.message}</span>
            </div>
          ) : null}

          <div
            className="orbit-meter"
            role="img"
            aria-label={t('settings.orbit.meterAria', {
              succeeded: lastRun.connectorsSucceeded,
              skipped: lastRun.connectorsSkipped,
              failed: lastRun.connectorsFailed,
              checked: lastRun.connectorsChecked,
            })}
          >
            {meterSucceeded > 0 ? (
              <span
                className="orbit-meter-seg is-succeeded"
                style={{ width: `${meterSucceeded}%` }}
              />
            ) : null}
            {meterSkipped > 0 ? (
              <span
                className="orbit-meter-seg is-skipped"
                style={{ width: `${meterSkipped}%` }}
              />
            ) : null}
            {meterFailed > 0 ? (
              <span
                className="orbit-meter-seg is-failed"
                style={{ width: `${meterFailed}%` }}
              />
            ) : null}
            {meterSucceeded + meterSkipped + meterFailed === 0 ? (
              <span className="orbit-meter-seg is-empty" />
            ) : null}
          </div>
          <dl className="orbit-counts">
            <div className="orbit-count">
              <dt>{t('settings.orbit.countChecked')}</dt>
              <dd>{lastRun.connectorsChecked}</dd>
            </div>
            <div className="orbit-count is-succeeded">
              <dt>{t('settings.orbit.countSucceeded')}</dt>
              <dd>{lastRun.connectorsSucceeded}</dd>
            </div>
            <div className="orbit-count is-skipped">
              <dt>{t('settings.orbit.countSkipped')}</dt>
              <dd>{lastRun.connectorsSkipped}</dd>
            </div>
            <div className="orbit-count is-failed">
              <dt>{t('settings.orbit.countFailed')}</dt>
              <dd>{lastRun.connectorsFailed}</dd>
            </div>
          </dl>
        </div>
      ) : notice ? (
        <div
          className={`orbit-inline-notice is-${notice.kind}`}
          role={notice.kind === 'error' ? 'alert' : 'status'}
        >
          <Icon name={notice.kind === 'error' ? 'close' : 'check'} size={12} />
          <span>{notice.message}</span>
        </div>
      ) : null}

      {/* ---------- 5. LIVE ARTIFACT STRIP ---------- */}
      {lastRun ? (
        <div
          className={`orbit-artifact-strip${liveArtifactHref ? '' : ' is-legacy'}`}
        >
          <div className="orbit-artifact-strip-icon" aria-hidden="true">
            <Icon name="file-code" size={18} />
          </div>
          <div className="orbit-artifact-strip-copy">
            <span className="orbit-artifact-strip-kicker">
              {liveArtifactHref
                ? t('settings.orbit.artifactKickerLive')
                : t('settings.orbit.artifactKickerLegacy')}
            </span>
            <span className="orbit-artifact-strip-title">
              {t('settings.orbit.artifactTitle')}
            </span>
            <span className="orbit-artifact-strip-meta">
              {liveArtifactHref
                ? t('settings.orbit.artifactMetaLive')
                : t('settings.orbit.artifactMetaLegacy')}
            </span>
          </div>
          <div className="orbit-artifact-strip-actions">
            {lastRun.markdown ? (
              <button
                type="button"
                className="orbit-artifact-ghost"
                onClick={() => void copyMarkdown()}
                title={t('settings.orbit.copyMarkdownTitle')}
              >
                {copied ? (
                  <>
                    <Icon name="check" size={13} />
                    <span>{t('settings.orbit.copied')}</span>
                  </>
                ) : (
                  <>
                    <Icon name="copy" size={13} />
                    <span>{t('settings.orbit.copy')}</span>
                  </>
                )}
              </button>
            ) : null}
            {liveArtifactHref ? (
              <a
                className="orbit-artifact-open"
                href={liveArtifactHref}
                target="_blank"
                rel="noreferrer"
              >
                <span>{t('settings.orbit.openArtifact')}</span>
                <Icon name="external-link" size={13} />
              </a>
            ) : null}
          </div>
          {lastRun.markdown ? (
            <details className="orbit-artifact-peek">
              <summary>
                <Icon name="chevron-right" size={12} />
                <span>{t('settings.orbit.sourceMarkdown')}</span>
              </summary>
              <pre>{lastRun.markdown}</pre>
            </details>
          ) : null}
        </div>
      ) : null}
    </section>
  );
}

function MediaProvidersSection({
  cfg,
  setCfg,
  mediaProvidersNotice,
  onReloadMediaProviders,
  providerModelsCache: sharedProviderModelsCache,
  onProviderModelsCacheChange,
  pendingLocalProviderIds,
  onChange,
}: {
  cfg: AppConfig;
  setCfg: Dispatch<SetStateAction<AppConfig>>;
  mediaProvidersNotice?: string | null;
  onReloadMediaProviders?: () => Promise<AppConfig['mediaProviders'] | null>;
  providerModelsCache?: Record<string, ProviderModelOption[]>;
  onProviderModelsCacheChange?: Dispatch<SetStateAction<Record<string, ProviderModelOption[]>>>;
  pendingLocalProviderIds: ReadonlySet<string>;
  onChange: (providerId: string) => void;
}) {
  const { t } = useI18n();
  const analytics = useAnalytics();
  const [reloadRunning, setReloadRunning] = useState(false);
  const [reloadNotice, setReloadNotice] = useState<{ kind: 'error' | 'success'; message: string } | null>(null);
  const [visibleApiKeys, setVisibleApiKeys] = useState<ReadonlySet<string>>(
    () => new Set(),
  );
  useEffect(() => {
    setVisibleApiKeys((current) => {
      const next = new Set<string>();
      for (const providerId of current) {
        const apiKey = cfg.mediaProviders?.[providerId]?.apiKey ?? '';
        if (apiKey.trim()) next.add(providerId);
      }
      return next.size === current.size ? current : next;
    });
  }, [cfg.mediaProviders]);
  const visibleProviders = MEDIA_PROVIDERS.filter(
    (p) => p.settingsVisible !== false,
  );
  // Split the catalog into two surfaces:
  //   - "Available" — daemon ships a real client, user can paste a key
  //     and it works. Rendered as full editable cards.
  //   - "Coming soon" — listed for transparency / roadmap signaling but
  //     the daemon has no client yet, so the form fields would be
  //     disabled placeholders. Hiding them behind a <details> keeps the
  //     primary list focused (was 16 cards, now 8) without dropping the
  //     informational value.
  const availableProviders = visibleProviders
    .filter((p) => p.integrated)
    .slice()
    .sort((a, b) => {
      const aEntry = cfg.mediaProviders?.[a.id];
      const bEntry = cfg.mediaProviders?.[b.id];
      const aConfigured = isStoredMediaProviderEntryPresent(aEntry);
      const bConfigured = isStoredMediaProviderEntryPresent(bEntry);
      if (aConfigured !== bConfigured) return aConfigured ? -1 : 1;
      return a.label.localeCompare(b.label);
    });
  const comingSoonProviders = visibleProviders
    .filter((p) => !p.integrated)
    .slice()
    .sort((a, b) => a.label.localeCompare(b.label));
  const updateProvider = (
    provider: MediaProvider,
    patch: {
      apiKey?: string;
      baseUrl?: string;
      model?: string;
      apiKeyConfigured?: boolean;
      apiKeyTail?: string;
    },
  ) => {
    onChange(provider.id);
    setCfg((curr) => {
      const prev = curr.mediaProviders?.[provider.id] ?? { apiKey: '', baseUrl: '', model: '' };
      const next = { ...prev, ...patch };
      const map = { ...(curr.mediaProviders ?? {}) };
      if (isStoredMediaProviderEntryEmpty(next)) {
        delete map[provider.id];
      } else {
        map[provider.id] = next;
      }
      return { ...curr, mediaProviders: map };
    });
  };
  const handleReload = async () => {
    if (!onReloadMediaProviders || reloadRunning) return;
    setReloadRunning(true);
    setReloadNotice(null);
    try {
      const next = await onReloadMediaProviders();
      if (!next) {
        setReloadNotice({ kind: 'error', message: t('settings.mediaProviderReloadError') });
        return;
      }
      setCfg((curr) => mergeDaemonMediaProviders(curr, next, {
        preserveLocalProviderIds: pendingLocalProviderIds,
      }));
      setReloadNotice({ kind: 'success', message: t('settings.mediaProviderReloadSuccess') });
    } finally {
      setReloadRunning(false);
    }
  };
  // Successful reload acknowledgement lives on the button (✓ Reloaded)
  // for ~2s then disappears. Keeping it as a permanent paragraph under
  // the section header was noise — the user just clicked a button and
  // got a visible state change, an extra "we did the thing" line is
  // redundant. Errors stay sticky because they actually require user
  // attention.
  useEffect(() => {
    if (reloadNotice?.kind !== 'success') return;
    const handle = window.setTimeout(() => setReloadNotice(null), 2000);
    return () => window.clearTimeout(handle);
  }, [reloadNotice]);

  const toggleApiKeyVisibility = (providerId: string) => {
    setVisibleApiKeys((current) => {
      const next = new Set(current);
      if (next.has(providerId)) {
        next.delete(providerId);
      } else {
        next.add(providerId);
      }
      return next;
    });
  };

  return (
    <section className="settings-section">
      {mediaProvidersNotice ? (
        <p className="hint" role="alert">{mediaProvidersNotice}</p>
      ) : null}
      {reloadNotice && reloadNotice.kind === 'error' ? (
        // Errors only — successful reload feedback now rides on the
        // button (see is-success-flash above) and clears itself after
        // 2s, so the section header doesn't get colonised by a
        // permanent "yes I did the thing" paragraph.
        <p className="hint" role="alert">{reloadNotice.message}</p>
      ) : null}
      {reloadNotice && reloadNotice.kind === 'success' ? (
        // Off-screen announcement so assistive tech still hears the
        // success state even though the visible feedback collapses
        // into a transient button label change.
        <VisuallyHidden role="status">
          {reloadNotice.message}
        </VisuallyHidden>
      ) : null}
      {onReloadMediaProviders ? (
        <div className="media-provider-reload-row">
          <button
            type="button"
            className={`ghost media-provider-reload-btn${
              reloadNotice?.kind === 'success' ? ' is-success-flash' : ''
            }`}
            onClick={() => {
              trackSettingsMediaProvidersClick(analytics.track, {
                page_name: 'settings',
                area: 'media_providers',
                element: 'reload',
              });
              void handleReload();
            }}
            disabled={reloadRunning}
            aria-live="polite"
          >
            {reloadRunning ? (
              t('common.loading')
            ) : reloadNotice?.kind === 'success' ? (
              <>
                <Icon name="check" size={13} />
                <span style={{ marginLeft: 4 }}>Reloaded</span>
              </>
            ) : (
              <>
                <Icon name="refresh" size={13} />
                <span style={{ marginLeft: 4 }}>{t('settings.mediaProviderReload')}</span>
              </>
            )}
          </button>
        </div>
      ) : null}
      <div className="media-provider-list">
        {availableProviders.map((provider) => {
          const entry = cfg.mediaProviders?.[provider.id] ?? { apiKey: '', baseUrl: '', model: '' };
          const hasPendingEdit = Boolean(entry.apiKey.trim());
          const isSavedState = Boolean((hasPendingEdit || entry.apiKeyConfigured) && !hasPendingEdit);
          const tail = entry.apiKeyTail?.trim();
          // Every provider rendered in the main list is integrated by
          // construction (see availableProviders filter), so the inputs
          // are always editable here. Non-integrated entries live in
          // the "Coming soon" <details> below.
          const disabled = false;
          const supportsCustomModel = provider.supportsCustomModel === true;
          const clearable = isStoredMediaProviderEntryPresent(entry);
          const apiKeyVisible = visibleApiKeys.has(provider.id);
          return (
            <div key={provider.id} className="media-provider-row">
              <div className="media-provider-head">
                <div className="media-provider-meta">
                  {/*
                    Provider name + "Saved" badge sit on a single row.
                    The badge used to render below the name with a green
                    success-pill treatment, which clashed with the green
                    "Integrated" badge on the right of the same row and
                    pushed the model hint two lines down. Inline + a
                    neutral muted treatment keeps the row scannable: green
                    means "we support this", blue means "you configured
                    it", gray means "your key is persisted" — three
                    distinct hues, three distinct meanings.
                  */}
                  <div className="media-provider-name-row">
                    <span className="media-provider-name">{provider.label}</span>
                    {isSavedState ? (
                      <span
                        className="field-status-badge field-status-badge--inline"
                        title={t('settings.connectorsSavedTitle')}
                      >
                        {tail
                          ? t('settings.connectorsSavedWithTail', { tail })
                          : t('settings.connectorsSaved')}
                      </span>
                    ) : null}
                  </div>
                  <span className="media-provider-hint">{provider.hint}</span>
                </div>
                {/*
                  Right-side badges deliberately omitted now: every row
                  in this list is "Integrated" by definition and the
                  "Configured" pill duplicated the inline "Saved" chip
                  next to the provider name. Three pills per row read
                  as warnings; one chip reads as status.
                */}
              </div>
              {provider.id === 'grok' ? <XaiOAuthControl /> : null}
              <div className="media-provider-body">
                <div className="media-provider-secret-field">
                  <input
                    type={apiKeyVisible ? 'text' : 'password'}
                    value={entry.apiKey}
                    placeholder={isSavedState ? t('settings.connectorsReplaceKeyPlaceholder') : t('settings.mediaProviderPlaceholder')}
                    aria-label={`${provider.label} ${t('settings.mediaProviderApiKey')}`}
                    disabled={disabled}
                    onFocus={() => {
                      trackSettingsMediaProvidersClick(analytics.track, {
                        page_name: 'settings',
                        area: 'media_providers',
                        element: 'key_input',
                        providers_id: provider.id,
                        is_configured: clearable,
                      });
                    }}
                    onChange={(e) => updateProvider(provider, { apiKey: e.target.value })}
                  />
                  <button
                    type="button"
                    className="secret-visibility-button"
                    disabled={disabled}
                    aria-label={
                      apiKeyVisible
                        ? `${provider.label} ${t('settings.hideKey')}`
                        : `${provider.label} ${t('settings.showKey')}`
                    }
                    aria-pressed={apiKeyVisible}
                    onClick={() => toggleApiKeyVisibility(provider.id)}
                  >
                      <Icon name={apiKeyVisible ? 'eye' : 'eye-off'} size={15} />
                    </button>
                  </div>
                <input
                  value={entry.baseUrl}
                  placeholder={provider.defaultBaseUrl || t('settings.mediaProviderBaseUrlPlaceholder')}
                  aria-label={`${provider.label} ${t('settings.mediaProviderBaseUrl')}`}
                  disabled={disabled}
                  onFocus={() => {
                    trackSettingsMediaProvidersClick(analytics.track, {
                      page_name: 'settings',
                      area: 'media_providers',
                      element: 'url_input',
                      providers_id: provider.id,
                      is_configured: clearable,
                    });
                  }}
                  onChange={(e) => updateProvider(provider, { baseUrl: e.target.value })}
                />
                {supportsCustomModel ? (
                  <input
                    value={entry.model ?? ''}
                    placeholder="gemini-3.1-flash-image-preview"
                    aria-label={`${provider.label} model`}
                    disabled={disabled}
                    onChange={(e) => updateProvider(provider, { model: e.target.value })}
                  />
                ) : null}
                <button
                  type="button"
                  className="ghost"
                  disabled={!clearable}
                  onClick={() => {
                    trackSettingsMediaProvidersClick(analytics.track, {
                      page_name: 'settings',
                      area: 'media_providers',
                      element: 'clear',
                      providers_id: provider.id,
                      // The click reports the state at the moment the
                      // user pressed Clear; the actual clear only lands
                      // after they confirm the dialog below, but the
                      // dashboard cares about the intent signal.
                      is_configured: clearable,
                    });
                    // Match the existing window.confirm guard the rest of
                    // the app uses for destructive actions (conversation
                    // delete, design delete, file delete in FileWorkspace).
                    // Without this a stray click on the row's Clear button
                    // wipes the saved key with no recovery. Issue #737.
                    if (
                      !confirm(
                        t('settings.mediaProviderClearConfirm', {
                          name: provider.label,
                        }),
                      )
                    ) {
                      return;
                    }
                    updateProvider(provider, {
                      apiKey: '',
                      baseUrl: '',
                      model: '',
                      apiKeyConfigured: false,
                      apiKeyTail: '',
                    });
                  }}
                >
                  {t('settings.mediaProviderClear')}
                </button>
              </div>
            </div>
          );
        })}
      </div>
      {comingSoonProviders.length > 0 ? (
        // Roadmap drawer. We still want to advertise that we know
        // these providers exist (so users don't ask "where is Fal?"),
        // but disabled placeholder cards in the main list were noise.
        // Closed by default — opens to a compact name + hint + docs
        // link list, no inputs because there's nothing to wire up yet.
        // TODO(i18n): inline English placeholders; promote to locale
        // keys when we touch this section again.
        <details className="library-group media-provider-coming-soon">
          <summary className="memory-details-summary">
            <span className="memory-details-title">
              {t('tasks.comingSoon')}
            </span>
            <span className="filter-pill-count">
              {comingSoonProviders.length}
            </span>
          </summary>
          <p className="hint" style={{ marginTop: 4, marginBottom: 8 }}>
            {t('settings.mediaProviderComingSoonHint')}
          </p>
          <ul className="media-provider-coming-soon-list">
            {comingSoonProviders.map((provider) => {
              const docsHref = sanitizeHttpsUrl(provider.docsUrl);
              return (
                <li
                  key={provider.id}
                  className="media-provider-coming-soon-item"
                >
                  <div className="media-provider-coming-soon-meta">
                    <span className="media-provider-name">
                      {provider.label}
                    </span>
                    <span className="media-provider-hint">
                      {provider.hint}
                    </span>
                  </div>
                  {docsHref ? (
                    <a
                      href={docsHref}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="ghost-link"
                    >
                      {t('settings.agentInstall.docs')}
                      <Icon name="external-link" size={11} />
                    </a>
                  ) : null}
                </li>
              );
            })}
          </ul>
        </details>
      ) : null}
    </section>
  );
}

// Per-client install paths. Each entry's `snippet` is what the user
// copies; some clients also support a richer `deeplink` flow that
// triggers a one-click install with an in-client approval dialog.
//
// Schemas drift between clients in deliberate ways. VS Code keys
// servers under "servers" with a required "type" field; Zed uses
// "context_servers"; Cursor, Windsurf, and Antigravity share
// "mcpServers"; Claude Code is best served by its CLI which writes
// to the local config for you. Verified against each tool's official
// docs in May 2026.
//
// Important: every snippet uses absolute paths to the daemon's current
// Node-compatible runtime and built cli.js, fetched at runtime. macOS
// and Linux ship a system /usr/bin/od (octal-dump) that shadows any
// `od` we might add to PATH, and most Open Design users run from
// source where `od` is not installed globally. The installer panel
// must NOT reference bare `od`.
type McpClientId =
  | 'claude'
  | 'codex'
  | 'cursor'
  | 'vscode'
  | 'zed'
  | 'windsurf'
  | 'antigravity';

interface McpInstallInfo {
  command: string;
  args: string[];
  env?: Record<string, string>;
  daemonUrl: string;
  platform: 'darwin' | 'linux' | 'win32' | string;
  cliExists: boolean;
  nodeExists: boolean;
  buildHint: string | null;
}

interface McpStdioServerConfig {
  command: string;
  args: string[];
  env?: Record<string, string>;
}

interface McpClient {
  id: McpClientId;
  label: string;
  // Function so the dropdown can show different methods per OS
  // (Claude Code uses CLI on POSIX but JSON edit on Windows because
  // the bash/PowerShell/cmd.exe quoting is too fragile to reliably
  // emit a single command that works in every shell).
  buildMethod: (info: McpInstallInfo) => string;
  // Function so per-OS path hints (~/.cursor on POSIX vs
  // %USERPROFILE%\.cursor on Windows) and shortcut differences
  // (⌘⇧P vs Ctrl+Shift+P) can be rendered correctly.
  buildInstruction: (info: McpInstallInfo) => string;
  buildSnippet: (info: McpInstallInfo) => string;
  buildSnippetLang: (info: McpInstallInfo) => 'bash' | 'json' | 'toml';
  // Optional one-click install action. Currently only Cursor
  // supports deeplinks of this shape.
  buildDeeplink?: (info: McpInstallInfo) => string;
  deeplinkLabel?: () => string;
}

// Path hint per OS. Localizes the "where to paste" copy so a
// Windows user does not see ~/.cursor/mcp.json (which their shell
// will not expand) or a Linux user does not see %APPDATA% paths.
function homeConfigPath(
  platform: McpInstallInfo['platform'],
  posix: string,
  windows: string,
): string {
  return platform === 'win32' ? windows : posix;
}

function commandPaletteShortcut(platform: McpInstallInfo['platform']): string {
  return platform === 'darwin' ? '⌘⇧P' : 'Ctrl+Shift+P';
}

function settingsShortcut(platform: McpInstallInfo['platform']): string {
  return platform === 'darwin' ? '⌘,' : 'Ctrl+,';
}

// btoa() requires every input character be representable in Latin-1
// (codepoints 0-255). A Mac/Linux home directory like
// "/Users/Émile/.fnm/.../node" trips that and throws
// InvalidCharacterError. UTF-8-encode the string into bytes first,
// then map each byte back to a Latin-1 char before base64'ing.
function utf8Btoa(s: string): string {
  const bytes = new TextEncoder().encode(s);
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]!);
  return btoa(bin);
}

function buildMcpStdioServerConfig(info: McpInstallInfo): McpStdioServerConfig {
  const env = info.env && Object.keys(info.env).length > 0 ? info.env : undefined;
  return {
    command: info.command,
    args: info.args,
    ...(env ? { env } : {}),
  };
}

function buildCodexEnvToml(info: McpInstallInfo): string {
  const entries = Object.entries(info.env ?? {});
  if (entries.length === 0) return '';
  return `

[mcp_servers.open-design.env]
${entries.map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join('\n')}`;
}

function buildSharedMcpJson(info: McpInstallInfo): string {
  const inner = buildMcpStdioServerConfig(info);
  const innerJson = JSON.stringify(inner, null, 2)
    .split('\n')
    .map((line, i) => (i === 0 ? line : `    ${line}`))
    .join('\n');
  return `{
  "mcpServers": {
    "open-design": ${innerJson}
  }
}`;
}

// One-click install toggle for Codex: queries the daemon for whether
// `codex mcp get open-design` succeeds, and POSTs/DELETEs the install
// endpoint to call `codex mcp add/remove` on the user's behalf. The
// copy-snippet path still works for users who prefer to paste manually
// or whose Codex CLI is not on PATH (button shows a disabled hint in
// that case).
function CodexInstallToggle(): JSX.Element | null {
  const { t } = useI18n();
  const [available, setAvailable] = useState<boolean | null>(null);
  const [installed, setInstalled] = useState<boolean | null>(null);
  const [busy, setBusy] = useState(false);
  const [message, setMessage] = useState<{ kind: 'success' | 'error'; text: string } | null>(null);

  const refresh = useCallback(async () => {
    try {
      const res = await fetch('/api/mcp/install/codex/status');
      if (!res.ok) throw new Error(`status ${res.status}`);
      const data = (await res.json()) as { available: boolean; installed: boolean };
      setAvailable(Boolean(data.available));
      setInstalled(Boolean(data.installed));
    } catch {
      // Daemon unreachable or endpoint missing — hide the toggle
      // entirely rather than spook the user with a permanent error.
      setAvailable(false);
      setInstalled(false);
    }
  }, []);

  useEffect(() => {
    refresh();
  }, [refresh]);

  const run = useCallback(
    async (method: 'POST' | 'DELETE', successKey: 'settings.mcpCodexInstallSuccess' | 'settings.mcpCodexUninstallSuccess') => {
      setBusy(true);
      setMessage(null);
      try {
        const res = await fetch('/api/mcp/install/codex', { method });
        if (!res.ok) {
          const body = (await res.json().catch(() => ({}))) as { error?: { message?: string } };
          throw new Error(body?.error?.message || `HTTP ${res.status}`);
        }
        setMessage({ kind: 'success', text: t(successKey) });
        await refresh();
      } catch (err) {
        setMessage({
          kind: 'error',
          text: t('settings.mcpCodexInstallError', { error: err instanceof Error ? err.message : String(err) }),
        });
      } finally {
        setBusy(false);
      }
    },
    [refresh, t],
  );

  if (available === null) return null;

  if (!available) {
    return (
      <div style={{ marginBottom: 12 }}>
        <button
          type="button"
          disabled
          style={{ padding: '6px 14px', fontSize: 13, opacity: 0.6 }}
        >
          {t('settings.mcpCodexOneClickInstall')}
        </button>
        <span style={{ marginLeft: 10, fontSize: 12, color: 'var(--fg-2, #9aa0a6)' }}>
          {t('settings.mcpCodexOneClickUnavailable')}
        </span>
      </div>
    );
  }

  const label = installed
    ? t('settings.mcpCodexOneClickUninstall')
    : t('settings.mcpCodexOneClickInstall');
  const onClick = () => {
    if (installed) {
      void run('DELETE', 'settings.mcpCodexUninstallSuccess');
    } else {
      void run('POST', 'settings.mcpCodexInstallSuccess');
    }
  };

  return (
    <div style={{ marginBottom: 12 }}>
      <button
        type="button"
        className={installed ? '' : 'primary'}
        disabled={busy}
        onClick={onClick}
        style={{ padding: '6px 14px', fontSize: 13 }}
      >
        {busy ? t('settings.mcpCodexBusy') : label}
      </button>
      {message ? (
        <span
          style={{
            marginLeft: 10,
            fontSize: 12,
            color: message.kind === 'error' ? 'var(--danger, #ff6b6b)' : 'var(--fg-2, #9aa0a6)',
          }}
        >
          {message.text}
        </span>
      ) : null}
    </div>
  );
}

function IntegrationsSection() {
  const { t } = useI18n();

  const MCP_CLIENTS: McpClient[] = [
    {
      id: 'claude',
      label: 'Claude Code',
      buildMethod: () => t('settings.mcpMethodCli'),
      buildInstruction: () => t('settings.mcpInstructionCli'),
      buildSnippet: (info) => {
        const inner = JSON.stringify(buildMcpStdioServerConfig(info));
        return `claude mcp add-json --scope user open-design '${inner}'`;
      },
      buildSnippetLang: () => 'bash',
    },
    {
      id: 'codex',
      label: 'Codex',
      buildMethod: () => t('settings.mcpMethodToml'),
      buildInstruction: (info) => {
        const path = homeConfigPath(
          info.platform,
          '~/.codex/config.toml',
          '%USERPROFILE%\\.codex\\config.toml',
        );
        return t('settings.mcpInstructionCodex', { path });
      },
      buildSnippet: (info) => `[mcp_servers.open-design]\ncommand = ${JSON.stringify(info.command)}\nargs = ${JSON.stringify(info.args)}${buildCodexEnvToml(info)}`,
      buildSnippetLang: () => 'toml',
    },
    {
      id: 'cursor',
      label: 'Cursor',
      buildMethod: () => t('settings.mcpMethodOneClick'),
      buildInstruction: (info) =>
        t('settings.mcpInstructionCursor', {
          path: homeConfigPath(info.platform, '~/.cursor/mcp.json', '%USERPROFILE%\\.cursor\\mcp.json'),
        }),
      buildSnippet: buildSharedMcpJson,
      buildSnippetLang: () => 'json',
      buildDeeplink: (info) => {
        const inner = buildMcpStdioServerConfig(info);
        const encoded = utf8Btoa(JSON.stringify(inner));
        return `cursor://anysphere.cursor-deeplink/mcp/install?name=open-design&config=${encoded}`;
      },
      deeplinkLabel: () => t('settings.mcpDeeplinkInstallCursor'),
    },
    {
      id: 'vscode',
      label: 'VS Code',
      buildMethod: () => t('settings.mcpMethodJson'),
      buildInstruction: (info) =>
        t('settings.mcpInstructionCopilot', {
          shortcut: commandPaletteShortcut(info.platform),
        }),
      buildSnippet: (info) => `{\n  "servers": {\n    "open-design": {\n      "type": "stdio",\n      "command": ${JSON.stringify(info.command)},\n      "args": ${JSON.stringify(info.args)}${info.env && Object.keys(info.env).length > 0 ? `,\n      "env": ${JSON.stringify(info.env)}` : ''}\n    }\n  }\n}`,
      buildSnippetLang: () => 'json',
    },
    {
      id: 'antigravity',
      label: 'Antigravity',
      buildMethod: () => t('settings.mcpMethodJson'),
      buildInstruction: () => t('settings.mcpInstructionAntigravity'),
      buildSnippet: buildSharedMcpJson,
      buildSnippetLang: () => 'json',
    },
    {
      id: 'zed',
      label: 'Zed',
      buildMethod: () => t('settings.mcpMethodJson'),
      buildInstruction: (info) =>
        t('settings.mcpInstructionZed', {
          shortcut: settingsShortcut(info.platform),
        }),
      buildSnippet: (info) => `{\n  "context_servers": {\n    "open-design": {\n      "source": "custom",\n      "command": ${JSON.stringify(info.command)},\n      "args": ${JSON.stringify(info.args)}${info.env && Object.keys(info.env).length > 0 ? `,\n      "env": ${JSON.stringify(info.env)}` : ''}\n    }\n  }\n}`,
      buildSnippetLang: () => 'json',
    },
    {
      id: 'windsurf',
      label: 'Windsurf',
      buildMethod: () => t('settings.mcpMethodJson'),
      buildInstruction: (info) =>
        t('settings.mcpInstructionWindsurf', {
          path: homeConfigPath(info.platform, '~/.codeium/windsurf/mcp_config.json', '%USERPROFILE%\\.codeium\\windsurf\\mcp_config.json'),
        }),
      buildSnippet: buildSharedMcpJson,
      buildSnippetLang: () => 'json',
    },
  ];

  const [clientId, setClientId] = useState<McpClientId>('claude');
  const [pickerOpen, setPickerOpen] = useState(false);
  const [copied, setCopied] = useState(false);
  const [info, setInfo] = useState<McpInstallInfo | null>(null);
  const [infoError, setInfoError] = useState<string | null>(null);
  const pickerRef = useRef<HTMLDivElement | null>(null);
  // The reset is wired through a ref-driven timer rather than effect
  // cleanup so re-clicks during the 2s window restart the countdown.
  const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(() => {
    return () => {
      if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
    };
  }, []);

  // Close the dropdown on outside click or Escape.
  useEffect(() => {
    if (!pickerOpen) return;
    const onDoc = (e: MouseEvent) => {
      if (!pickerRef.current) return;
      if (!pickerRef.current.contains(e.target as Node)) setPickerOpen(false);
    };
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setPickerOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [pickerOpen]);

  // Pull the absolute paths to node + cli.js from the running daemon
  // so snippets work even when `od` isn't on PATH (the realistic
  // case for source clones, plus macOS/Linux ship a /usr/bin/od that
  // shadows any global install). Fetched on mount; if the daemon is
  // unreachable we surface a clear error instead of a half-built
  // snippet that would silently fail when pasted.
  useEffect(() => {
    let cancelled = false;
    fetch('/api/mcp/install-info')
      .then(async (res) => {
        if (!res.ok) throw new Error(`daemon ${res.status}`);
        return (await res.json()) as McpInstallInfo;
      })
      .then((data) => {
        if (cancelled) return;
        setInfo(data);
        setInfoError(null);
      })
      .catch((err) => {
        if (cancelled) return;
        setInfoError(String(err && err.message ? err.message : err));
      });
    return () => {
      cancelled = true;
    };
  }, []);

  const client = MCP_CLIENTS.find((c) => c.id === clientId) ?? MCP_CLIENTS[0]!;
  const snippet = info ? client.buildSnippet(info) : '';
  const snippetLang: 'bash' | 'json' | 'toml' = info
    ? client.buildSnippetLang(info)
    : 'json';

  // Reset the "Copied" badge when the user flips to a different
  // client; otherwise the green check sits there next to a snippet
  // they haven't actually copied.
  useEffect(() => {
    setCopied(false);
    if (copyTimerRef.current) {
      clearTimeout(copyTimerRef.current);
      copyTimerRef.current = null;
    }
  }, [clientId]);

  const onCopy = async () => {
    if (!snippet) return;
    try {
      await navigator.clipboard.writeText(snippet);
      setCopied(true);
      if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
      copyTimerRef.current = setTimeout(() => setCopied(false), 2000);
    } catch {
      // Clipboard API can fail under non-secure contexts; the snippet
      // is selectable so the user can still copy manually.
      setCopied(false);
    }
  };

  return (
    <section className="settings-section">
      <div className="mcp-client-body">
        {infoError ? (
          <div
            className="empty-card"
            style={{ marginBottom: 14, color: 'var(--danger-fg, #f88)' }}
          >
            {t('settings.mcpDaemonError', { error: infoError! })}
          </div>
        ) : null}

        {/* Group 1: what the MCP server does */}
        <div className="mcp-capabilities-card">
          <p className="mcp-capabilities-label">
            {t('settings.mcpCapabilitiesTitle')}
          </p>
          <ul className="mcp-capabilities-list">
            <li>{t('settings.mcpCapabilityRead')}</li>
            <li>{t('settings.mcpCapabilityPull')}</li>
            <li>{t('settings.mcpCapabilityDefault')}</li>
          </ul>
        </div>

        {/* Group 2: setup flow */}
        <div className="mcp-setup-card">
          <div
            className="ds-picker"
            ref={pickerRef}
          >
          <button
            type="button"
            className={`ds-picker-trigger${pickerOpen ? ' open' : ''}`}
            onClick={() => setPickerOpen((v) => !v)}
            aria-haspopup="listbox"
            aria-expanded={pickerOpen}
          >
            <span className="ds-picker-meta">
              <span className="ds-picker-title">{client.label}</span>
              <span className="ds-picker-sub">
                {info ? client.buildMethod(info) : ''}
              </span>
            </span>
            <Icon
              name="chevron-down"
              size={14}
              className="ds-picker-chevron"
              style={{ transform: pickerOpen ? 'rotate(180deg)' : undefined }}
            />
          </button>
          {pickerOpen ? (
            <div className="ds-picker-popover" role="listbox">
              <div className="ds-picker-list">
                {MCP_CLIENTS.map((c) => {
                  const active = c.id === clientId;
                  return (
                    <button
                      key={c.id}
                      type="button"
                      role="option"
                      aria-selected={active}
                      className={`ds-picker-item${active ? ' active' : ''}`}
                      onClick={() => {
                        setClientId(c.id);
                        setPickerOpen(false);
                      }}
                    >
                      <span className="ds-picker-item-text">
                        <span className="ds-picker-item-title">{c.label}</span>
                        <span
                          style={{
                            fontSize: 11,
                            color: 'var(--text-muted)',
                          }}
                        >
                          {info ? c.buildMethod(info) : ''}
                        </span>
                      </span>
                    </button>
                  );
                })}
              </div>
            </div>
          ) : null}
        </div>

        {info ? (
          <p style={{ margin: 0 }}>{client.buildInstruction(info)}</p>
        ) : null}

        {client.id === 'codex' ? <CodexInstallToggle /> : null}

        {client.buildDeeplink && info ? (
          <div style={{ marginBottom: 12 }}>
            <button
              type="button"
              className="primary"
              onClick={() => {
                // Use a hidden anchor so the cursor:// scheme is
                // handled the same way as a normal link click; some
                // browsers block window.location assignments to
                // unknown schemes from button handlers.
                const url = client.buildDeeplink!(info);
                const a = document.createElement('a');
                a.href = url;
                a.rel = 'noopener noreferrer';
                a.click();
              }}
              disabled={!info.cliExists || !info.nodeExists}
              style={{ padding: '6px 14px', fontSize: 13 }}
            >
              <Icon name="link" size={14} />
              <span style={{ marginLeft: 6 }}>{client.deeplinkLabel ? client.deeplinkLabel() : ''}</span>
            </button>
            <span
              style={{
                marginLeft: 10,
                fontSize: 12,
                color: 'var(--fg-2, #9aa0a6)',
              }}
            >
              {t('settings.mcpCursorApproval')}
            </span>
          </div>
        ) : null}

        <div style={{ position: 'relative' }}>
          <pre
            style={{
              background: 'var(--surface-2, #11141a)',
              color: 'var(--fg-1, #e6e6e6)',
              // Reserve top clearance for the absolutely-positioned
              // Copy button so the first line of the snippet does not
              // sit underneath it, and reserve right clearance so a
              // wrapped bash one-liner stops short of the button rather
              // than scrolling behind it. The right padding is sized
              // for the wider "Copied" post-click state (icon + text +
              // button padding + the 8px right offset) with a few px
              // of buffer for elevated font sizes / zoom. Issue #632.
              padding: '40px 104px 12px 14px',
              borderRadius: 8,
              overflowX: 'auto',
              fontFamily:
                'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
              fontSize: 12,
              lineHeight: 1.55,
              margin: 0,
              userSelect: 'text',
              whiteSpace: snippetLang === 'bash' ? 'pre-wrap' : 'pre',
              wordBreak: snippetLang === 'bash' ? 'break-all' : 'normal',
              minHeight: 60,
            }}
            data-lang={snippetLang}
          >
            <code>
              {snippet ||
                (infoError
                  ? t('settings.mcpResolvingFailed')
                  : t('settings.mcpLoadingPaths'))}
            </code>
          </pre>
          <button
            type="button"
            className="ghost mcp-copy-btn"
            onClick={onCopy}
            disabled={!snippet}
            style={{
              position: 'absolute',
              top: 8,
              right: 8,
              padding: '4px 10px',
              fontSize: 12,
            }}
            aria-label={t('settings.mcpCopyAria')}
          >
            <Icon name={copied ? 'check' : 'copy'} size={14} />
            <span style={{ marginLeft: 6 }}>{copied ? t('settings.mcpCopied') : t('settings.mcpCopy')}</span>
          </button>
        </div>

        {/* "Build the daemon first" lives here — next to the code
            block it explains — rather than at the top of the section
            before the user has seen anything. A dev-mode pre-condition
            warning at the very top reads as "something is broken"
            before the user has even picked their client. */}
        {info && (!info.cliExists || !info.nodeExists) ? (
          <div
            className="empty-card"
            style={{ borderLeft: '3px solid var(--warning-fg, #fbbf24)' }}
          >
            <strong>
              {!info.cliExists
                ? t('settings.mcpBuildDaemon')
                : t('settings.mcpNodeMissing')}
            </strong>{' '}
            {info.buildHint ?? t('settings.mcpBuildHint')}
          </div>
        ) : null}

        {/* Restart note is a "next step" after running the command,
            not an error — keep it right after the code block. */}
        <div
          style={{
            padding: '10px 12px',
            background: 'var(--bg-subtle)',
            border: '1px solid var(--border)',
            borderLeft: '3px solid var(--border-strong)',
            borderRadius: 6,
            fontSize: 13,
            lineHeight: 1.5,
          }}
        >
          <strong>{t('settings.mcpRestartNote')}</strong>{' '}
          <span style={{ color: 'var(--text-muted)' }}>
            {t('settings.mcpRestartDetail')}
          </span>
        </div>

          <p className="mcp-running-note">
            {t('settings.mcpRunningNote')}
          </p>
        </div>{/* end mcp-setup-card */}
      </div>
    </section>
  );
}

const THEMES: Array<{ value: AppTheme; labelKey: 'settings.themeSystem' | 'settings.themeLight' | 'settings.themeDark'; icon?: 'sun' | 'moon' }> = [
  { value: 'system', labelKey: 'settings.themeSystem' },
  { value: 'light', labelKey: 'settings.themeLight', icon: 'sun' },
  { value: 'dark', labelKey: 'settings.themeDark', icon: 'moon' },
];

function AppearanceSection({
  cfg,
  setCfg,
}: {
  cfg: AppConfig;
  setCfg: Dispatch<SetStateAction<AppConfig>>;
}) {
  const { t } = useI18n();
  const analytics = useAnalytics();
  const current = cfg.theme ?? 'system';
  const currentAccent = normalizeAccentColor(cfg.accentColor) ?? DEFAULT_ACCENT_COLOR;
  const accentLabel = t('pet.fieldAccent');
  const defaultAccentLabel = t('pet.fieldAccentDefault');
  const customAccentLabel = t('pet.fieldAccentCustom');

  // Apply the draft theme immediately so the user sees a live preview
  // before hitting Save. SettingsDialog's cleanup reverts this on cancel.
  useLayoutEffect(() => {
    applyAppearanceToDocument({
      theme: current,
      accentColor: currentAccent,
    });
  }, [current, currentAccent]);

  const setAccentColor = (color: string) => {
    setCfg((c) => ({ ...c, accentColor: normalizeAccentColor(color) ?? c.accentColor ?? DEFAULT_ACCENT_COLOR }));
  };

  return (
    <section className="settings-section">
      <div className="seg-control" role="group" aria-label={t('settings.appearance')} style={{ '--seg-cols': THEMES.length } as React.CSSProperties}>
        {THEMES.map(({ value, labelKey, icon }) => (
          <button
            key={value}
            type="button"
            className={'seg-btn' + (current === value ? ' active' : '')}
            aria-pressed={current === value}
            onClick={() => {
              // P1 ui_click area=appearance — `system|light|dark` only
              // emits from the segmented control; accent swatch picks
              // use `accent_color` with the swatch hex below.
              if (value === 'system' || value === 'light' || value === 'dark') {
                trackSettingsAppearanceClick(analytics.track, {
                  page_name: 'settings',
                  area: 'appearance',
                  element: value,
                });
              }
              setCfg((c) => ({ ...c, theme: value }));
            }}
          >
            {icon ? <Icon name={icon} size={14} aria-hidden="true" /> : null}
            <span className="seg-title">{t(labelKey)}</span>
          </button>
        ))}
      </div>
      <div className="field">
        <span className="field-label">{accentLabel}</span>
        <div className="pet-swatches" role="radiogroup" aria-label={accentLabel}>
          {ACCENT_SWATCHES.map((color) => {
            const active = currentAccent === color;
            return (
              <button
                key={color}
                type="button"
                className={`pet-swatch${active ? ' active' : ''}`}
                style={{ background: color }}
                aria-label={color === DEFAULT_ACCENT_COLOR ? defaultAccentLabel : color}
                aria-checked={active}
                role="radio"
                onClick={() => {
                  trackSettingsAppearanceClick(analytics.track, {
                    page_name: 'settings',
                    area: 'appearance',
                    element: 'accent_color',
                    color,
                  });
                  setAccentColor(color);
                }}
              />
            );
          })}
          <input
            type="color"
            aria-label={customAccentLabel}
            className="pet-swatch-picker"
            value={currentAccent}
            onChange={(e) => setAccentColor(e.target.value)}
          />
        </div>
      </div>
    </section>
  );
}

/**
 * Settings surface for the M1 Critique Theater rollout toggle.
 *
 * The toggle has two halves on opposite sides of the HTTP boundary:
 *
 *   * Browser-side: `useCritiqueTheaterEnabled` reads / writes the
 *     `open-design:config` localStorage blob; this is what gates
 *     whether `<CritiqueTheaterMount>` actually renders.
 *   * Daemon-side: the rollout resolver in `server.ts` reads
 *     `project.metadata.critiqueTheaterEnabled`, so the daemon only
 *     routes runs through the critique pipeline when the active
 *     project's metadata row says yes (or env / phase / skill policy
 *     overrides it).
 *
 * If we only wrote localStorage, the user would see the mount but
 * every generation would still skip the critique pipeline server-side
 * (Codex + lefarcen P1 on PR #1484). To keep the two halves in
 * lockstep, the setter takes an optional `{ projectId }` and, when
 * provided, does the read-merge-write PATCH on the project's metadata
 * (already shipped by Phase 15 and exercised by the wireup PR).
 *
 * This section threads the currently-open project id when the dialog
 * is opened from `/projects/:id`. When opened from the entry gallery
 * (`/`), the toggle is localStorage-only, and a contextual hint tells
 * the user that per-project persistence requires opening a project
 * first. That matches the actual scope of the wire-up.
 */
function CritiqueTheaterSection() {
  const { t } = useI18n();
  const enabled = useCritiqueTheaterEnabled();
  const route = useRoute();
  const activeProjectId = route.kind === 'project' ? route.projectId : null;
  return (
    <section className="settings-section">
      <div className="section-head">
        <div>
          <h3>{t('critiqueTheater.settingsNav')}</h3>
          <p className="hint">{t('critiqueTheater.settingsNavHint')}</p>
        </div>
      </div>
      <label className="field">
        <span className="field-label">
          <input
            type="checkbox"
            checked={enabled}
            onChange={(e) => {
              const next = e.target.checked;
              if (activeProjectId !== null) {
                void setCritiqueTheaterEnabled(next, { projectId: activeProjectId });
              } else {
                void setCritiqueTheaterEnabled(next);
              }
            }}
          />
          {' '}
          {t('critiqueTheater.settingsEnabledLabel')}
        </span>
        <small className="hint">
          {t('critiqueTheater.settingsEnabledDescription')}
        </small>
        {activeProjectId !== null ? (
          <small className="hint">
            {t('critiqueTheater.settingsEnabledProjectHint')}
          </small>
        ) : (
          <small className="hint">
            {t('critiqueTheater.settingsEnabledNoProjectHint')}
          </small>
        )}
      </label>
    </section>
  );
}

// Map the runtime SoundId (hyphenated, used by utils/notifications.ts) onto
// the contract's underscored enum. Sounds that don't have a tracking entry
// drop to undefined so we never emit an off-enum value.
function soundIdToTracking(
  id: string,
):
  | 'ding'
  | 'chime'
  | 'two_tone_up'
  | 'pluck'
  | 'buzz'
  | 'two_tone_down'
  | 'thud'
  | undefined {
  switch (id) {
    case 'ding':
      return 'ding';
    case 'chime':
      return 'chime';
    case 'two-tone-up':
      return 'two_tone_up';
    case 'pluck':
      return 'pluck';
    case 'buzz':
      return 'buzz';
    case 'two-tone-down':
      return 'two_tone_down';
    case 'thud':
      return 'thud';
    default:
      return undefined;
  }
}

function NotificationsSection({
  cfg,
  setCfg,
}: {
  cfg: AppConfig;
  setCfg: Dispatch<SetStateAction<AppConfig>>;
}) {
  const { t } = useI18n();
  const analytics = useAnalytics();
  const notif = cfg.notifications ?? DEFAULT_NOTIFICATIONS;
  const [permission, setPermission] = useState<NotificationPermission | 'unsupported'>(
    () => notificationPermission(),
  );
  const [testStatus, setTestStatus] = useState<ReturnType<typeof testNotificationStatusText> | null>(null);

  const updateNotif = (
    patch: Partial<NonNullable<AppConfig['notifications']>>,
  ) => {
    setCfg((c) => ({
      ...c,
      notifications: { ...DEFAULT_NOTIFICATIONS, ...(c.notifications ?? {}), ...patch },
    }));
  };

  const toggleSound = () => {
    const next = !notif.soundEnabled;
    // P1 ui_click area=notifications element=completion_sound — the toggle
    // emits the post-click state on `completion_sound_status` so a single
    // event captures intent + outcome.
    trackSettingsNotificationsClick(analytics.track, {
      page_name: 'settings',
      area: 'notifications',
      element: 'completion_sound',
      completion_sound_status: next ? 'on' : 'off',
    });
    updateNotif({ soundEnabled: next });
    // Give the user immediate audible feedback when turning the master
    // switch on so they know which sound they're signing up for. Resuming
    // the AudioContext also bakes in their gesture for later auto-plays.
    if (next) playSound(notif.successSoundId);
  };

  const toggleDesktop = async () => {
    if (notif.desktopEnabled) {
      trackSettingsNotificationsClick(analytics.track, {
        page_name: 'settings',
        area: 'notifications',
        element: 'desktop_notification',
        desktop_notification_status: 'off',
      });
      updateNotif({ desktopEnabled: false });
      return;
    }
    const result = await requestNotificationPermission();
    setPermission(result);
    if (result === 'granted') {
      trackSettingsNotificationsClick(analytics.track, {
        page_name: 'settings',
        area: 'notifications',
        element: 'desktop_notification',
        desktop_notification_status: 'on',
      });
      updateNotif({ desktopEnabled: true });
    } else {
      trackSettingsNotificationsClick(analytics.track, {
        page_name: 'settings',
        area: 'notifications',
        element: 'desktop_notification',
        desktop_notification_status: 'off',
      });
      updateNotif({ desktopEnabled: false });
    }
  };

  const sendTestNotification = async () => {
    const result = await showCompletionNotification({
      status: 'succeeded',
      title: t('notify.successTitle'),
      body: t('notify.successBody'),
    });
    setPermission(notificationPermission());
    setTestStatus(testNotificationStatusText(result));
  };

  return (
    <section className="settings-section">
      <div className="settings-subsection">
        <div className="settings-notify-card">
          <div className="settings-notify-card-header">
            <h4>{t('settings.notifyCompletionSound')}</h4>
            <div className="section-head-actions">
              <div className="seg-control" role="group" aria-label={t('settings.notifyCompletionSound')} style={{ '--seg-cols': 1 } as React.CSSProperties}>
                <button
                  type="button"
                  className={'seg-btn' + (notif.soundEnabled ? ' active' : '')}
                  aria-pressed={notif.soundEnabled}
                  onClick={toggleSound}
                >
                  <span className="seg-title">{notif.soundEnabled ? t('common.active') : t('common.offline')}</span>
                </button>
              </div>
            </div>
          </div>
          <p className="hint settings-notify-card-hint">{t('settings.notifyCompletionSoundHint')}</p>
        </div>

        {notif.soundEnabled ? (
          <>
            <div className="settings-field">
              <label>{t('settings.notifySuccessSound')}</label>
              <div className="seg-control" role="group" aria-label={t('settings.notifySuccessSound')} style={{ '--seg-cols': SUCCESS_SOUNDS.length } as React.CSSProperties}>
                {SUCCESS_SOUNDS.map((sound) => (
                  <button
                    key={sound.id}
                    type="button"
                    className={'seg-btn' + (notif.successSoundId === sound.id ? ' active' : '')}
                    aria-pressed={notif.successSoundId === sound.id}
                    onClick={() => {
                      const trackingSoundId = soundIdToTracking(sound.id);
                      trackSettingsNotificationsClick(analytics.track, {
                        page_name: 'settings',
                        area: 'notifications',
                        element: 'success_sound',
                        ...(trackingSoundId ? { sound_id: trackingSoundId } : {}),
                      });
                      updateNotif({ successSoundId: sound.id });
                      playSound(sound.id);
                    }}
                  >
                    <span className="seg-title">{t(sound.labelKey)}</span>
                  </button>
                ))}
              </div>
            </div>

            <div className="settings-field">
              <label>{t('settings.notifyFailureSound')}</label>
              <div className="seg-control" role="group" aria-label={t('settings.notifyFailureSound')} style={{ '--seg-cols': FAILURE_SOUNDS.length } as React.CSSProperties}>
                {FAILURE_SOUNDS.map((sound) => (
                  <button
                    key={sound.id}
                    type="button"
                    className={'seg-btn' + (notif.failureSoundId === sound.id ? ' active' : '')}
                    aria-pressed={notif.failureSoundId === sound.id}
                    onClick={() => {
                      const trackingSoundId = soundIdToTracking(sound.id);
                      trackSettingsNotificationsClick(analytics.track, {
                        page_name: 'settings',
                        area: 'notifications',
                        element: 'failure_sound',
                        ...(trackingSoundId ? { sound_id: trackingSoundId } : {}),
                      });
                      updateNotif({ failureSoundId: sound.id });
                      playSound(sound.id);
                    }}
                  >
                    <span className="seg-title">{t(sound.labelKey)}</span>
                  </button>
                ))}
              </div>
            </div>
          </>
        ) : null}
      </div>

      <div className="settings-subsection">
        <div className="settings-notify-card">
          <div className="settings-notify-card-header">
            <h4>{t('settings.notifyDesktop')}</h4>
            <div className="section-head-actions">
              <div className="seg-control" role="group" aria-label={t('settings.notifyDesktop')} style={{ '--seg-cols': 1 } as React.CSSProperties}>
                <button
                  type="button"
                  className={'seg-btn' + (notif.desktopEnabled ? ' active' : '')}
                  aria-pressed={notif.desktopEnabled}
                  disabled={permission === 'unsupported'}
                  onClick={() => { void toggleDesktop(); }}
                >
                  <span className="seg-title">{notif.desktopEnabled ? t('common.active') : t('common.offline')}</span>
                </button>
              </div>
            </div>
          </div>
          <p className="hint settings-notify-card-hint">{t('settings.notifyDesktopHint')}</p>
        </div>
        {permission === 'unsupported' ? (
          <p className="hint">{t('settings.notifyDesktopUnsupported')}</p>
        ) : null}
        {permission === 'denied' ? (
          <p className="hint">{t('settings.notifyDesktopBlocked')}</p>
        ) : null}
        {notif.desktopEnabled && permission === 'granted' ? (
          <>
            <Button variant="ghost" onClick={() => {
              trackSettingsNotificationsClick(analytics.track, {
                page_name: 'settings',
                area: 'notifications',
                element: 'send_test',
              });
              void sendTestNotification();
            }}>
              {t('settings.notifyTest')}
            </Button>
            {testStatus ? <p className="hint" role="status">{t(testStatus)}</p> : null}
          </>
        ) : null}
      </div>
    </section>
  );
}

function testNotificationStatusText(
  result: Awaited<ReturnType<typeof showCompletionNotification>>,
):
  | 'settings.notifyTestSent'
  | 'settings.notifyDesktopBlocked'
  | 'settings.notifyDesktopUnsupported'
  | 'settings.notifyTestFailed' {
  if (result === 'shown') return 'settings.notifyTestSent';
  if (result === 'permission-denied') return 'settings.notifyDesktopBlocked';
  if (result === 'unsupported') return 'settings.notifyDesktopUnsupported';
  return 'settings.notifyTestFailed';
}
