/**
 * @file CcConfig.tsx
 * @description Claude Code configuration explorer. Surfaces every plugin,
 * skill, subagent, slash command, MCP server, hook, settings file, memory
 * file, marketplace, keybinding, and statusline script Claude Code knows
 * about. Read access for all surfaces; create / edit / delete for the
 * low-risk text-file surfaces (skills, agents, commands, output styles,
 * CLAUDE.md memory, and per-project file-based memory files). Plugins, MCP,
 * hooks-in-settings, and settings.json files stay read-only - those have
 * concurrent-write races with the live CLI.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */
/* =============================================================================
 * MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
 * =============================================================================
 * **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
 *
 * ## Design constraints
 * - Local-first: no telemetry leaves the machine unless the user configures webhooks.
 * - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
 *   philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
 * - Destructive flows stay behind explicit confirmation modals and server-side gates.
 * - Internationalization: user-visible strings belong in i18n JSON, not literals here.
 *
 * ## Remote data & SSH
 * Remote Data Sources let operators aggregate multiple machines. SSH entries describe
 * how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
 * scoped GET via `?sources=`. Health checks and import history surface in Settings.
 *
 * ## Observability
 * Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
 * provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
 * Docker Compose profiles are documented in `monitoring/README.md`.
 *
 * ## Internal dependencies
 * - `../lib/eventBus`
 * - `../lib/api`
 *
 * ## Public surface
 * - `CcConfig` — exported API; see TSDoc on the symbol for behavior.
 *
 * ## Testing pointers
 * - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
 * - Server contract changes require `npm run test:server` and OpenAPI sync.
 * - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
 *
 * ## Related docs
 * - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline.
 * - `docs/API.md` — REST reference.
 * - `.claude/skills/file-headers/` — mandatory `@author` header policy.
 * ============================================================================= */
/* -----------------------------------------------------------------------------
 * EXPORT CATALOG — quick index of symbols defined below (documentation only).
 * -----------------------------------------------------------------------------
 * **CcConfig**
 *   Part of this module's public contract. Downstream imports should treat
 *   the signature and return type as stable unless release notes say otherwise.
 *   When behavior changes, update the `@file` overview and relevant tests.
 *
 * ----------------------------------------------------------------------------- */

import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import { eventBus } from "../lib/eventBus";
import {
  Boxes,
  RefreshCw,
  Search,
  Sparkles,
  UserRound,
  FolderTree,
  Wrench,
  Slash,
  Palette,
  PlugZap,
  Server,
  Webhook,
  Settings as SettingsIcon,
  BookOpen,
  FileText,
  Copy,
  Check,
  AlertCircle,
  ExternalLink,
  X,
  Info,
  Pencil,
  Trash2,
  Plus,
  Save,
  ShieldAlert,
  Lock,
  History,
  Terminal,
  Store,
  Keyboard,
  CircleDot,
  CircleSlash,
  ChevronLeft,
  ChevronRight,
  ChevronDown,
} from "lucide-react";
import { api } from "../lib/api";
import type {
  CcArtifactType,
  CcBackup,
  CcFileResponse,
  CcHookScripts,
  CcHookSource,
  CcKeybindings,
  CcKeybindingGroup,
  CcMarketplacesResponse,
  CcMcpResponse,
  CcMcpServer,
  CcMdItem,
  CcMemoryItem,
  CcMutationResult,
  CcOverview,
  CcPlugin,
  CcPluginsResponse,
  CcScope,
  CcSettingsSource,
  CcStatusline,
} from "../lib/api";

function isMutable(
  tab: TabKey
): tab is "skills" | "agents" | "commands" | "outputStyles" | "memory" {
  return (
    tab === "skills" ||
    tab === "agents" ||
    tab === "commands" ||
    tab === "outputStyles" ||
    tab === "memory"
  );
}

function tabToArtifactType(
  tab: "skills" | "agents" | "commands" | "outputStyles" | "memory"
): CcArtifactType {
  return tab === "outputStyles" ? "output-styles" : tab;
}

type TabKey =
  | "overview"
  | "skills"
  | "agents"
  | "commands"
  | "outputStyles"
  | "plugins"
  | "marketplaces"
  | "mcp"
  | "hooks"
  | "keybindings"
  | "settings"
  | "memory";

interface TabDef {
  key: TabKey;
  icon: typeof Sparkles;
  i18nKey: string;
}

const TABS: TabDef[] = [
  { key: "overview", icon: Boxes, i18nKey: "tabs.overview" },
  { key: "skills", icon: Sparkles, i18nKey: "tabs.skills" },
  { key: "agents", icon: UserRound, i18nKey: "tabs.agents" },
  { key: "commands", icon: Slash, i18nKey: "tabs.commands" },
  { key: "memory", icon: BookOpen, i18nKey: "tabs.memory" },
  { key: "plugins", icon: PlugZap, i18nKey: "tabs.plugins" },
  { key: "marketplaces", icon: Store, i18nKey: "tabs.marketplaces" },
  { key: "mcp", icon: Server, i18nKey: "tabs.mcp" },
  { key: "hooks", icon: Webhook, i18nKey: "tabs.hooks" },
  { key: "keybindings", icon: Keyboard, i18nKey: "tabs.keybindings" },
  { key: "settings", icon: SettingsIcon, i18nKey: "tabs.settings" },
  { key: "outputStyles", icon: Palette, i18nKey: "tabs.outputStyles" },
];

interface PageState {
  overview: CcOverview | null;
  skills: CcMdItem[] | null;
  agents: CcMdItem[] | null;
  commands: CcMdItem[] | null;
  outputStyles: CcMdItem[] | null;
  plugins: CcPluginsResponse | null;
  marketplaces: CcMarketplacesResponse | null;
  mcp: CcMcpResponse | null;
  hooks: CcHookSource[] | null;
  keybindings: CcKeybindings | null;
  settings: CcSettingsSource[] | null;
  memory: CcMemoryItem[] | null;
  statusline: CcStatusline | null;
  hookScripts: CcHookScripts | null;
}

const EMPTY_STATE: PageState = {
  overview: null,
  skills: null,
  agents: null,
  commands: null,
  outputStyles: null,
  plugins: null,
  marketplaces: null,
  mcp: null,
  hooks: null,
  keybindings: null,
  settings: null,
  memory: null,
  statusline: null,
  hookScripts: null,
};

type EditorState =
  | {
      mode: "create";
      type: CcArtifactType;
      defaultScope: "user" | "project";
      template: string;
      project?: string; // set for type === "auto-memory"
    }
  | {
      mode: "edit";
      type: CcArtifactType;
      scope: "user" | "project" | "auto-memory";
      name: string;
      filePath: string;
      project?: string; // set for type === "auto-memory"
    }
  | null;

type ConfirmDeleteState = {
  type: CcArtifactType;
  scope: "user" | "project" | "auto-memory";
  name?: string;
  path: string;
  project?: string; // set for type === "auto-memory"
} | null;

type Toast = { kind: "success" | "error"; message: string } | null;

export function CcConfig() {
  const { t } = useTranslation("ccConfig");
  const [tab, setTab] = useState<TabKey>("overview");
  const [scope, setScope] = useState<CcScope>("all");
  const [data, setData] = useState<PageState>(EMPTY_STATE);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
  const [search, setSearch] = useState("");
  const [viewer, setViewer] = useState<{
    path: string;
    data: CcFileResponse | null;
    error: string | null;
  } | null>(null);
  const [editor, setEditor] = useState<EditorState>(null);
  const [confirmDelete, setConfirmDelete] = useState<ConfirmDeleteState>(null);
  const [toast, setToast] = useState<Toast>(null);
  const [backupsOpen, setBackupsOpen] = useState(false);

  // Auto-dismiss toasts after 5s
  useEffect(() => {
    if (!toast) return;
    const id = setTimeout(() => setToast(null), 5000);
    return () => clearTimeout(id);
  }, [toast]);

  const fetchAll = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const [
        overview,
        skills,
        agents,
        commands,
        outputStyles,
        plugins,
        marketplaces,
        mcp,
        hooks,
        keybindings,
        settings,
        memory,
        statusline,
        hookScripts,
      ] = await Promise.all([
        api.ccConfig.overview(),
        api.ccConfig.skills(scope),
        api.ccConfig.agents(scope),
        api.ccConfig.commands(scope),
        api.ccConfig.outputStyles(scope),
        api.ccConfig.plugins(),
        api.ccConfig.marketplaces(),
        api.ccConfig.mcp(),
        api.ccConfig.hooks(),
        api.ccConfig.keybindings(),
        api.ccConfig.settings(),
        api.ccConfig.memory(),
        api.ccConfig.statusline(),
        api.ccConfig.hookScripts(),
      ]);
      setData({
        overview,
        skills: skills.items,
        agents: agents.items,
        commands: commands.items,
        outputStyles: outputStyles.items,
        plugins,
        marketplaces,
        mcp,
        hooks: hooks.items,
        keybindings,
        settings: settings.items,
        memory: memory.items,
        statusline,
        hookScripts,
      });
      setLastUpdated(new Date());
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : "unknown error";
      setError(msg);
    } finally {
      setLoading(false);
    }
  }, [scope]);

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

  // Live updates - refetch whenever the server broadcasts that a config
  // surface has changed (either via dashboard mutations or external file
  // edits picked up by the cc-watcher). Debounced because a single user
  // action can write multiple files (e.g. a skill backup + the skill itself
  // + the file-history snapshot all land within tens of ms).
  const refetchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(() => {
    return eventBus.subscribe((msg) => {
      if (msg.type !== "cc_config_changed") return;
      if (refetchTimerRef.current) clearTimeout(refetchTimerRef.current);
      refetchTimerRef.current = setTimeout(() => {
        refetchTimerRef.current = null;
        void fetchAll();
      }, 250);
    });
  }, [fetchAll]);
  useEffect(() => {
    return () => {
      if (refetchTimerRef.current) clearTimeout(refetchTimerRef.current);
    };
  }, []);

  const wsConnected = useSyncExternalStore(eventBus.onConnection, () => eventBus.connected);

  const openViewer = useCallback(async (path: string) => {
    setViewer({ path, data: null, error: null });
    try {
      const file = await api.ccConfig.file(path);
      setViewer({ path, data: file, error: null });
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : "unknown error";
      setViewer({ path, data: null, error: msg });
    }
  }, []);

  const openCreate = useCallback(
    (type: CcArtifactType, overrideScope?: "user" | "project") => {
      const tplKey = `edit.templates.${type}`;
      const template = t(tplKey);
      const defaultScope: "user" | "project" =
        overrideScope ?? (scope === "project" ? "project" : "user");
      setEditor({ mode: "create", type, defaultScope, template });
    },
    [scope, t]
  );

  const openEdit = useCallback(
    (type: CcArtifactType, item: { scope: "user" | "project"; name: string; filePath: string }) => {
      setEditor({
        mode: "edit",
        type,
        scope: item.scope,
        name: item.name,
        filePath: item.filePath,
      });
    },
    []
  );

  const openDelete = useCallback(
    (
      type: CcArtifactType,
      scopeArg: "user" | "project",
      name: string | undefined,
      path: string
    ) => {
      setConfirmDelete({ type, scope: scopeArg, name, path });
    },
    []
  );

  // ── Auto-memory (per-project file-based memory) create / edit / delete ──
  const openCreateAuto = useCallback(
    (project: string) => {
      setEditor({
        mode: "create",
        type: "auto-memory",
        defaultScope: "user", // unused for auto-memory; scope is fixed
        template: t("edit.templates.auto-memory"),
        project,
      });
    },
    [t]
  );

  const openEditAuto = useCallback((item: CcMemoryItem) => {
    if (!item.project || !item.name) return;
    setEditor({
      mode: "edit",
      type: "auto-memory",
      scope: "auto-memory",
      name: item.name,
      filePath: item.file,
      project: item.project,
    });
  }, []);

  const openDeleteAuto = useCallback((item: CcMemoryItem) => {
    if (!item.project || !item.name) return;
    setConfirmDelete({
      type: "auto-memory",
      scope: "auto-memory",
      name: item.name,
      path: item.file,
      project: item.project,
    });
  }, []);

  const handleSave = useCallback(
    async (args: {
      type: CcArtifactType;
      targetScope: "user" | "project" | "auto-memory";
      name: string | undefined;
      content: string;
      project?: string;
    }) => {
      const result: CcMutationResult = await api.ccConfig.write({
        scope: args.targetScope,
        type: args.type,
        name: args.name,
        content: args.content,
        project: args.project,
      });
      setEditor(null);
      setToast({
        kind: "success",
        message: result.created
          ? t("edit.saveSuccessNew")
          : t("edit.saveSuccess", { path: result.backupPath || "-" }),
      });
      void fetchAll();
    },
    [fetchAll, t]
  );

  const handleDelete = useCallback(async () => {
    if (!confirmDelete) return;
    try {
      const result = await api.ccConfig.delete({
        scope: confirmDelete.scope,
        type: confirmDelete.type,
        name: confirmDelete.name,
        project: confirmDelete.project,
      });
      setConfirmDelete(null);
      setToast({
        kind: "success",
        message: t("edit.deleteSuccess", { path: result.backupPath || "-" }),
      });
      void fetchAll();
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : "unknown error";
      setConfirmDelete(null);
      setToast({ kind: "error", message: t("edit.deleteError", { message: msg }) });
    }
  }, [confirmDelete, fetchAll, t]);

  return (
    <div className="space-y-5">
      <Header
        loading={loading}
        lastUpdated={lastUpdated}
        scope={scope}
        onScopeChange={setScope}
        onRefresh={fetchAll}
        onOpenBackups={() => setBackupsOpen(true)}
        wsConnected={wsConnected}
      />

      {error && (
        <div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-4 py-3 text-sm text-status-danger flex items-center gap-2">
          <AlertCircle className="w-4 h-4 flex-shrink-0" />
          <span>{t("loadError", { message: error })}</span>
        </div>
      )}

      <Tabs current={tab} onSelect={setTab} counts={data.overview?.counts} />

      <div className="rounded-xl border border-border bg-surface-1">
        {tab !== "overview" && (
          <div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
            <Search className="w-4 h-4 text-fg-muted" />
            <input
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              placeholder={t("common.search")}
              className="h-7 bg-transparent text-sm text-fg-primary placeholder:text-fg-muted focus:outline-none flex-1"
            />
            {search && (
              <button
                type="button"
                onClick={() => setSearch("")}
                title={t("common.clearSearch")}
                aria-label={t("common.clearSearch")}
                className="h-7 w-7 flex-shrink-0 inline-flex items-center justify-center rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 focus:outline-none focus:ring-1 focus:ring-accent/40"
              >
                <X className="w-3.5 h-3.5" />
              </button>
            )}
            {isMutable(tab) && tab !== "memory" && (
              <button
                onClick={() => openCreate(tabToArtifactType(tab))}
                className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
              >
                <Plus className="w-3 h-3" />
                {t("edit.newButton")}
              </button>
            )}
          </div>
        )}
        <div className="p-4">
          <TabPanel
            tab={tab}
            data={data}
            search={search}
            onOpenFile={openViewer}
            onEdit={openEdit}
            onDelete={openDelete}
            onCreateMemory={(s) => openCreate("memory", s)}
            onEditAuto={openEditAuto}
            onDeleteAuto={openDeleteAuto}
            onCreateAuto={openCreateAuto}
            onKeybindingsSaved={fetchAll}
            onToast={setToast}
          />
        </div>
      </div>

      {viewer && <FileViewer state={viewer} onClose={() => setViewer(null)} />}
      {editor && <EditorModal state={editor} onClose={() => setEditor(null)} onSave={handleSave} />}
      {confirmDelete && (
        <ConfirmDeleteModal
          state={confirmDelete}
          onCancel={() => setConfirmDelete(null)}
          onConfirm={handleDelete}
        />
      )}
      {toast && <ToastNotice toast={toast} onDismiss={() => setToast(null)} />}
      {backupsOpen && <BackupsModal onClose={() => setBackupsOpen(false)} />}
    </div>
  );
}

// ── Header ────────────────────────────────────────────────────────────

interface HeaderProps {
  loading: boolean;
  lastUpdated: Date | null;
  scope: CcScope;
  onScopeChange: (s: CcScope) => void;
  onRefresh: () => void;
  onOpenBackups: () => void;
  wsConnected: boolean;
}

function Header({
  loading,
  lastUpdated,
  scope,
  onScopeChange,
  onRefresh,
  onOpenBackups,
  wsConnected,
}: HeaderProps) {
  const { t } = useTranslation("ccConfig");
  const { t: tCommon } = useTranslation("common");
  const formatted = lastUpdated
    ? lastUpdated.toLocaleTimeString(undefined, {
        hour: "2-digit",
        minute: "2-digit",
        second: "2-digit",
      })
    : "-";
  return (
    <header className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
      <div className="flex items-start gap-3 min-w-0 flex-1">
        <div className="w-9 h-9 rounded-xl bg-accent/15 flex items-center justify-center flex-shrink-0">
          <Boxes className="w-4.5 h-4.5 text-accent" />
        </div>
        <div className="min-w-0">
          <div className="flex items-center gap-2">
            <h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
            {wsConnected ? (
              <span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
                <span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
                {tCommon("live")}
              </span>
            ) : (
              <span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
                <span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
                {tCommon("offline")}
              </span>
            )}
          </div>
          <p className="text-xs text-fg-muted max-w-2xl">{t("subtitle")}</p>
        </div>
      </div>
      <div className="flex flex-col items-stretch lg:items-end gap-2 flex-shrink-0">
        <div className="flex items-center gap-2 justify-end flex-wrap">
          <ScopeToggle value={scope} onChange={onScopeChange} />
          <button
            onClick={onOpenBackups}
            className="inline-flex items-center gap-2 rounded-lg border border-border bg-surface-2 px-3 py-1.5 text-xs font-medium text-fg-secondary hover:bg-surface-3 transition-colors"
          >
            <History className="w-3.5 h-3.5" />
            {t("backups.openButton")}
          </button>
          <button
            onClick={onRefresh}
            disabled={loading}
            className="inline-flex items-center gap-2 rounded-lg border border-border bg-surface-2 px-3 py-1.5 text-xs font-medium text-fg-secondary hover:bg-surface-3 disabled:opacity-60 transition-colors"
          >
            <RefreshCw className={`w-3.5 h-3.5 ${loading ? "animate-spin" : ""}`} />
            {loading ? t("refreshing") : t("refresh")}
          </button>
        </div>
        {lastUpdated && (
          <span className="text-[11px] text-fg-muted self-end">
            {t("lastUpdated", { time: formatted })}
          </span>
        )}
      </div>
    </header>
  );
}

function ScopeToggle({ value, onChange }: { value: CcScope; onChange: (s: CcScope) => void }) {
  const { t } = useTranslation("ccConfig");
  const opts: { v: CcScope; label: string }[] = [
    { v: "all", label: t("scope.all") },
    { v: "user", label: t("scope.user") },
    { v: "project", label: t("scope.project") },
  ];
  return (
    <div className="inline-flex rounded-lg border border-border bg-surface-2 p-0.5">
      {opts.map((o) => (
        <button
          key={o.v}
          onClick={() => onChange(o.v)}
          className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors ${
            value === o.v
              ? "bg-accent/20 text-accent border border-accent/30"
              : "text-fg-secondary hover:text-fg-primary"
          }`}
        >
          {o.label}
        </button>
      ))}
    </div>
  );
}

// ── Tabs ──────────────────────────────────────────────────────────────

interface TabsProps {
  current: TabKey;
  onSelect: (k: TabKey) => void;
  counts?: CcOverview["counts"];
}

function Tabs({ current, onSelect, counts }: TabsProps) {
  const { t } = useTranslation("ccConfig");
  const scrollRef = useRef<HTMLDivElement | null>(null);
  const [canScrollLeft, setCanScrollLeft] = useState(false);
  const [canScrollRight, setCanScrollRight] = useState(false);

  // Update scroll affordances when content size or scroll position changes.
  const updateAffordances = useCallback(() => {
    const el = scrollRef.current;
    if (!el) return;
    setCanScrollLeft(el.scrollLeft > 1);
    setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
  }, []);

  useEffect(() => {
    updateAffordances();
    const el = scrollRef.current;
    if (!el) return;
    el.addEventListener("scroll", updateAffordances, { passive: true });
    const ro = new ResizeObserver(updateAffordances);
    ro.observe(el);
    return () => {
      el.removeEventListener("scroll", updateAffordances);
      ro.disconnect();
    };
  }, [updateAffordances]);

  // Scroll the active tab into view when it changes (e.g. user picks a tab
  // that's offscreen, or window resize hides the active one).
  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    const active = el.querySelector<HTMLElement>('[data-tab-active="true"]');
    if (!active) return;
    const elRect = el.getBoundingClientRect();
    const activeRect = active.getBoundingClientRect();
    if (activeRect.left < elRect.left + 8) {
      el.scrollBy({ left: activeRect.left - elRect.left - 16, behavior: "smooth" });
    } else if (activeRect.right > elRect.right - 8) {
      el.scrollBy({ left: activeRect.right - elRect.right + 16, behavior: "smooth" });
    }
  }, [current]);

  const scrollByButton = (dir: 1 | -1) => {
    const el = scrollRef.current;
    if (!el) return;
    el.scrollBy({ left: dir * Math.max(200, el.clientWidth * 0.6), behavior: "smooth" });
  };

  const countFor = (key: TabKey): number | null => {
    if (!counts) return null;
    switch (key) {
      case "skills":
        return counts.skills.user + counts.skills.project;
      case "agents":
        return counts.agents.user + counts.agents.project;
      case "commands":
        return counts.commands.user + counts.commands.project;
      case "outputStyles":
        return counts.outputStyles.user + counts.outputStyles.project;
      case "plugins":
        return counts.plugins;
      case "marketplaces":
        return counts.marketplaces;
      case "keybindings":
        return counts.keybindings;
      case "mcp":
        return counts.mcpServers.user + counts.mcpServers.project;
      case "hooks":
        return Object.values(counts.hooks).reduce((a, b) => a + b, 0);
      case "settings":
        return counts.settingsFiles;
      case "memory":
        return counts.memory;
      default:
        return null;
    }
  };
  return (
    <div className="relative rounded-xl border border-border bg-surface-1">
      {/* Left edge gradient + chevron */}
      <div
        className={`pointer-events-none absolute left-0 top-0 bottom-0 w-12 rounded-l-xl bg-gradient-to-r from-surface-1 to-transparent transition-opacity z-10 ${
          canScrollLeft ? "opacity-100" : "opacity-0"
        }`}
      />
      {canScrollLeft && (
        <button
          onClick={() => scrollByButton(-1)}
          aria-label="scroll tabs left"
          className="absolute left-1 top-1/2 -translate-y-1/2 z-20 rounded-md w-7 h-7 flex items-center justify-center bg-surface-2 border border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
        >
          <ChevronLeft className="w-4 h-4" />
        </button>
      )}

      <div
        ref={scrollRef}
        className="flex gap-1 p-1 overflow-x-auto scroll-smooth [&::-webkit-scrollbar]:hidden"
        style={{ scrollbarWidth: "none" }}
      >
        {TABS.map(({ key, icon: Icon, i18nKey }) => {
          const c = countFor(key);
          const active = current === key;
          return (
            <button
              key={key}
              data-tab-active={active ? "true" : undefined}
              onClick={() => onSelect(key)}
              className={`flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors flex-shrink-0 whitespace-nowrap ${
                active
                  ? "bg-accent/15 text-accent border border-accent/30"
                  : "text-fg-secondary hover:text-fg-primary hover:bg-surface-3 border border-transparent"
              }`}
            >
              <Icon className="w-3.5 h-3.5" />
              <span>{t(i18nKey)}</span>
              {c !== null && (
                <span
                  className={`text-[10px] px-1.5 py-0.5 rounded-full font-semibold ${
                    active ? "bg-accent/20 text-accent" : "bg-surface-3 text-fg-secondary"
                  }`}
                >
                  {c}
                </span>
              )}
            </button>
          );
        })}
      </div>

      {/* Right edge gradient + chevron */}
      <div
        className={`pointer-events-none absolute right-0 top-0 bottom-0 w-12 rounded-r-xl bg-gradient-to-l from-surface-1 to-transparent transition-opacity z-10 ${
          canScrollRight ? "opacity-100" : "opacity-0"
        }`}
      />
      {canScrollRight && (
        <button
          onClick={() => scrollByButton(1)}
          aria-label="scroll tabs right"
          className="absolute right-1 top-1/2 -translate-y-1/2 z-20 rounded-md w-7 h-7 flex items-center justify-center bg-surface-2 border border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
        >
          <ChevronRight className="w-4 h-4" />
        </button>
      )}
    </div>
  );
}

// ── Tab panel switch ──────────────────────────────────────────────────

interface TabPanelProps {
  tab: TabKey;
  data: PageState;
  search: string;
  onOpenFile: (path: string) => void;
  onEdit: (
    type: CcArtifactType,
    item: { scope: "user" | "project"; name: string; filePath: string }
  ) => void;
  onDelete: (
    type: CcArtifactType,
    scope: "user" | "project",
    name: string | undefined,
    path: string
  ) => void;
  onCreateMemory: (scope: "user" | "project") => void;
  onEditAuto: (item: CcMemoryItem) => void;
  onDeleteAuto: (item: CcMemoryItem) => void;
  onCreateAuto: (project: string) => void;
  onKeybindingsSaved: () => void;
  onToast: (toast: NonNullable<Toast>) => void;
}

function TabPanel({
  tab,
  data,
  search,
  onOpenFile,
  onEdit,
  onDelete,
  onCreateMemory,
  onEditAuto,
  onDeleteAuto,
  onCreateAuto,
  onKeybindingsSaved,
  onToast,
}: TabPanelProps) {
  switch (tab) {
    case "overview":
      return <OverviewPanel overview={data.overview} />;
    case "skills":
      return (
        <MdItemList
          items={data.skills}
          search={search}
          onOpen={onOpenFile}
          onEdit={onEdit}
          onDelete={onDelete}
          kind="skills"
        />
      );
    case "agents":
      return (
        <MdItemList
          items={data.agents}
          search={search}
          onOpen={onOpenFile}
          onEdit={onEdit}
          onDelete={onDelete}
          kind="agents"
        />
      );
    case "commands":
      return (
        <MdItemList
          items={data.commands}
          search={search}
          onOpen={onOpenFile}
          onEdit={onEdit}
          onDelete={onDelete}
          kind="commands"
        />
      );
    case "outputStyles":
      return (
        <MdItemList
          items={data.outputStyles}
          search={search}
          onOpen={onOpenFile}
          onEdit={onEdit}
          onDelete={onDelete}
          kind="outputStyles"
        />
      );
    case "plugins":
      return <PluginsPanel data={data.plugins} search={search} />;
    case "marketplaces":
      return <MarketplacesPanel data={data.marketplaces} search={search} />;
    case "mcp":
      return <McpPanel data={data.mcp} search={search} />;
    case "hooks":
      return (
        <HooksPanel
          sources={data.hooks}
          scripts={data.hookScripts}
          search={search}
          onOpen={onOpenFile}
        />
      );
    case "keybindings":
      return (
        <KeybindingsPanel
          data={data.keybindings}
          search={search}
          onSaved={onKeybindingsSaved}
          onToast={onToast}
        />
      );
    case "settings":
      return (
        <SettingsPanel sources={data.settings} statusline={data.statusline} onOpen={onOpenFile} />
      );
    case "memory":
      return (
        <MemoryPanel
          items={data.memory}
          search={search}
          onOpen={onOpenFile}
          onEdit={onEdit}
          onDelete={onDelete}
          onCreate={onCreateMemory}
          onEditAuto={onEditAuto}
          onDeleteAuto={onDeleteAuto}
          onCreateAuto={onCreateAuto}
        />
      );
    default:
      return null;
  }
}

// ── Overview ──────────────────────────────────────────────────────────

// Tone palette - each tone is { iconBg, iconText, border, accentBar }.
// Used by both root rows and summary stat tiles for a consistent color story.
type Tone =
  | "sky"
  | "emerald"
  | "violet"
  | "amber"
  | "fuchsia"
  | "cyan"
  | "pink"
  | "indigo"
  | "orange"
  | "teal"
  | "slate"
  | "rose";
const TONES: Record<Tone, { iconBg: string; iconText: string; bar: string; ring: string }> = {
  sky: {
    iconBg: "bg-sky-500/10",
    iconText: "text-sky-300",
    bar: "bg-sky-500/40",
    ring: "ring-sky-500/20",
  },
  emerald: {
    iconBg: "bg-status-success/10",
    iconText: "text-status-success",
    bar: "bg-status-success/40",
    ring: "ring-status-success/20",
  },
  violet: {
    iconBg: "bg-violet-500/10",
    iconText: "text-violet-300",
    bar: "bg-violet-500/40",
    ring: "ring-violet-500/20",
  },
  amber: {
    iconBg: "bg-status-warning/10",
    iconText: "text-status-warning",
    bar: "bg-status-warning/40",
    ring: "ring-status-warning/20",
  },
  fuchsia: {
    iconBg: "bg-fuchsia-500/10",
    iconText: "text-fuchsia-300",
    bar: "bg-fuchsia-500/40",
    ring: "ring-fuchsia-500/20",
  },
  cyan: {
    iconBg: "bg-cyan-500/10",
    iconText: "text-cyan-300",
    bar: "bg-cyan-500/40",
    ring: "ring-cyan-500/20",
  },
  pink: {
    iconBg: "bg-pink-500/10",
    iconText: "text-pink-300",
    bar: "bg-pink-500/40",
    ring: "ring-pink-500/20",
  },
  indigo: {
    iconBg: "bg-indigo-500/10",
    iconText: "text-indigo-300",
    bar: "bg-indigo-500/40",
    ring: "ring-indigo-500/20",
  },
  orange: {
    iconBg: "bg-orange-500/10",
    iconText: "text-orange-300",
    bar: "bg-orange-500/40",
    ring: "ring-orange-500/20",
  },
  teal: {
    iconBg: "bg-teal-500/10",
    iconText: "text-teal-300",
    bar: "bg-teal-500/40",
    ring: "ring-teal-500/20",
  },
  slate: {
    iconBg: "bg-surface-4/10",
    iconText: "text-fg-secondary",
    bar: "bg-surface-4/40",
    ring: "ring-border-light/20",
  },
  rose: {
    iconBg: "bg-rose-500/10",
    iconText: "text-rose-300",
    bar: "bg-rose-500/40",
    ring: "ring-rose-500/20",
  },
};

function OverviewPanel({ overview }: { overview: CcOverview | null }) {
  const { t } = useTranslation("ccConfig");
  if (!overview) return <SkeletonRows n={4} />;
  const { roots, counts } = overview;
  return (
    <div className="space-y-5">
      <section>
        <h2 className="text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-2">
          {t("overview.rootsTitle")}
        </h2>
        <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
          <RootRow
            icon={FolderTree}
            tone="sky"
            label={t("overview.claudeHome")}
            value={roots.claudeHome}
          />
          <RootRow
            icon={FolderTree}
            tone="emerald"
            label={t("overview.projectClaudeDir")}
            value={roots.projectClaudeDir}
          />
          <RootRow
            icon={FolderTree}
            tone="violet"
            label={t("overview.projectRoot")}
            value={roots.projectRoot}
          />
          <RootRow
            icon={FileText}
            tone="amber"
            label={t("overview.claudeJson")}
            value={roots.claudeJson}
          />
        </div>
      </section>

      <section>
        <h2 className="text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-2">
          {t("overview.summary")}
        </h2>
        <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2">
          <SummaryStat
            tone="fuchsia"
            icon={Sparkles}
            label={t("tabs.skills")}
            user={counts.skills.user}
            project={counts.skills.project}
          />
          <SummaryStat
            tone="sky"
            icon={UserRound}
            label={t("tabs.agents")}
            user={counts.agents.user}
            project={counts.agents.project}
          />
          <SummaryStat
            tone="cyan"
            icon={Slash}
            label={t("tabs.commands")}
            user={counts.commands.user}
            project={counts.commands.project}
          />
          <SummaryStat
            tone="pink"
            icon={Palette}
            label={t("tabs.outputStyles")}
            user={counts.outputStyles.user}
            project={counts.outputStyles.project}
          />
          <SummaryStat
            tone="indigo"
            icon={Server}
            label={t("tabs.mcp")}
            user={counts.mcpServers.user}
            project={counts.mcpServers.project}
          />
          <SummaryStat
            tone="emerald"
            icon={PlugZap}
            label={t("tabs.plugins")}
            value={counts.plugins}
          />
          <SummaryStat
            tone="amber"
            icon={Store}
            label={t("tabs.marketplaces")}
            value={counts.marketplaces}
          />
          <SummaryStat
            tone="orange"
            icon={Webhook}
            label={t("tabs.hooks")}
            value={Object.values(counts.hooks).reduce((a, b) => a + b, 0)}
          />
          <SummaryStat
            tone="rose"
            icon={Keyboard}
            label={t("tabs.keybindings")}
            value={counts.keybindings}
          />
          <SummaryStat
            tone="slate"
            icon={SettingsIcon}
            label={t("tabs.settings")}
            value={counts.settingsFiles}
          />
          <SummaryStat tone="teal" icon={BookOpen} label={t("tabs.memory")} value={counts.memory} />
        </div>
      </section>
    </div>
  );
}

interface SummaryStatProps {
  tone: Tone;
  icon: typeof Sparkles;
  label: string;
  // Either a single value, OR a user/project pair (which is summed for the headline number).
  value?: number;
  user?: number;
  project?: number;
}

function SummaryStat({ tone, icon: Icon, label, value, user, project }: SummaryStatProps) {
  const { t } = useTranslation("ccConfig");
  const T = TONES[tone];
  const total = value !== undefined ? value : (user ?? 0) + (project ?? 0);
  const showBreakdown = user !== undefined && project !== undefined;
  return (
    <div className={`relative rounded-lg border border-border bg-surface-2 overflow-hidden`}>
      {/* Left accent bar */}
      <div className={`absolute left-0 top-0 bottom-0 w-1 ${T.bar}`} aria-hidden />
      <div className="pl-3.5 pr-3 py-2.5">
        <div className="flex items-center gap-2">
          <span
            className={`w-6 h-6 rounded-md ${T.iconBg} flex items-center justify-center flex-shrink-0`}
          >
            <Icon className={`w-3.5 h-3.5 ${T.iconText}`} />
          </span>
          <span className="text-[10px] font-semibold uppercase tracking-wider text-fg-secondary truncate">
            {label}
          </span>
        </div>
        <div className="mt-1.5 flex items-baseline gap-2">
          <span className="text-xl font-semibold text-fg-primary tabular-nums">{total}</span>
          {showBreakdown && (
            <span className="text-[10px] text-fg-muted truncate">
              {user} {t("overview.user")} · {project} {t("overview.project")}
            </span>
          )}
        </div>
      </div>
    </div>
  );
}

function RootRow({
  icon: Icon,
  tone,
  label,
  value,
}: {
  icon: typeof FolderTree;
  tone: Tone;
  label: string;
  value: string;
}) {
  const T = TONES[tone];
  return (
    <div className="relative flex items-center gap-2.5 rounded-lg border border-border bg-surface-2 px-3 py-2 min-w-0 overflow-hidden">
      <div className={`absolute left-0 top-0 bottom-0 w-1 ${T.bar}`} aria-hidden />
      <span
        className={`w-7 h-7 rounded-md ${T.iconBg} flex items-center justify-center flex-shrink-0 ml-1.5`}
      >
        <Icon className={`w-3.5 h-3.5 ${T.iconText}`} />
      </span>
      <div className="min-w-0 flex-1">
        <div className="text-[10px] font-semibold uppercase tracking-wider text-fg-secondary">
          {label}
        </div>
        <div className="font-mono text-[11px] text-fg-secondary truncate">{value}</div>
      </div>
      <CopyButton value={value} />
    </div>
  );
}

// ── MD-item generic list (skills/agents/commands/output-styles) ───────

interface MdItemListProps {
  items: CcMdItem[] | null;
  search: string;
  onOpen: (path: string) => void;
  onEdit: (
    type: CcArtifactType,
    item: { scope: "user" | "project"; name: string; filePath: string }
  ) => void;
  onDelete: (
    type: CcArtifactType,
    scope: "user" | "project",
    name: string | undefined,
    path: string
  ) => void;
  kind: "skills" | "agents" | "commands" | "outputStyles";
}

function MdItemList({ items, search, onOpen, onEdit, onDelete, kind }: MdItemListProps) {
  const filtered = useMemo(() => {
    if (!items) return null;
    const q = search.toLowerCase();
    return items.filter((it) => {
      if (!q) return true;
      const blob = [it.name, it.frontmatter.description, it.frontmatter.name]
        .filter(Boolean)
        .join(" ")
        .toLowerCase();
      return blob.includes(q);
    });
  }, [items, search]);

  if (!filtered) return <SkeletonRows n={6} />;
  if (filtered.length === 0) return <Empty />;

  return (
    <div className="space-y-2">
      {filtered.map((it) => (
        <MdItemCard
          key={`${it.scope}:${it.name}`}
          item={it}
          onOpen={onOpen}
          onEdit={onEdit}
          onDelete={onDelete}
          kind={kind}
        />
      ))}
    </div>
  );
}

interface MdItemCardProps {
  item: CcMdItem;
  onOpen: (p: string) => void;
  onEdit: (
    type: CcArtifactType,
    item: { scope: "user" | "project"; name: string; filePath: string }
  ) => void;
  onDelete: (
    type: CcArtifactType,
    scope: "user" | "project",
    name: string | undefined,
    path: string
  ) => void;
  kind: "skills" | "agents" | "commands" | "outputStyles";
}

function MdItemCard({ item, onOpen, onEdit, onDelete, kind }: MdItemCardProps) {
  const { t } = useTranslation("ccConfig");
  const artifactType: CcArtifactType = kind === "outputStyles" ? "output-styles" : kind;

  const filePath = item.file || `${item.path}/SKILL.md`;
  const description =
    item.frontmatter.description ||
    item.preview
      .replace(/^#+\s.*\n/, "")
      .trim()
      .slice(0, 200);
  return (
    <div className="rounded-lg border border-border bg-surface-2 px-4 py-3 hover:border-border/80 transition-colors">
      <div className="flex items-start justify-between gap-3">
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-2">
            <span className="font-mono text-sm text-fg-primary truncate">{item.name}</span>
            <ScopeBadge scope={item.scope} />
            {item.frontmatter.model && (
              <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20">
                {item.frontmatter.model}
              </span>
            )}
          </div>
          {description && (
            <p className="mt-1.5 text-xs text-fg-secondary leading-relaxed line-clamp-2">
              {description}
            </p>
          )}
          {kind === "agents" && item.frontmatter.tools && (
            <div className="mt-2 text-[11px] text-fg-muted">
              <span className="text-fg-muted">{t("agents.tools")}:</span>{" "}
              <span className="font-mono text-fg-secondary">{item.frontmatter.tools}</span>
            </div>
          )}
          <div className="mt-2 font-mono text-[10px] text-fg-muted truncate">{filePath}</div>
        </div>
        <div className="flex flex-col gap-1.5 flex-shrink-0">
          <button
            onClick={() => onOpen(filePath)}
            className="text-[11px] font-medium px-2 py-1 rounded-md border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1.5"
          >
            <ExternalLink className="w-3 h-3" />
            {t("common.viewSource")}
          </button>
          <button
            onClick={() => onEdit(artifactType, { scope: item.scope, name: item.name, filePath })}
            className="text-[11px] font-medium px-2 py-1 rounded-md border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1.5"
          >
            <Pencil className="w-3 h-3" />
            {t("edit.editButton")}
          </button>
          <button
            onClick={() => onDelete(artifactType, item.scope, item.name, filePath)}
            className="text-[11px] font-medium px-2 py-1 rounded-md border border-status-danger/30 bg-status-danger/5 hover:bg-status-danger/15 text-status-danger inline-flex items-center gap-1.5"
          >
            <Trash2 className="w-3 h-3" />
            {t("edit.deleteButton")}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Plugins ───────────────────────────────────────────────────────────

function PluginsPanel({ data, search }: { data: CcPluginsResponse | null; search: string }) {
  const { t } = useTranslation("ccConfig");
  if (!data) return <SkeletonRows n={4} />;
  const filtered = data.plugins.filter(
    (p) => !search || p.key.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <div className="space-y-3">
      <ExplainerBanner
        title={t("explain.plugins.title")}
        body={t("explain.plugins.body")}
        howTo={t("explain.plugins.install")}
        commands={[{ cmd: t("explain.plugins.installCmd"), note: "" }]}
      />
      <div className="rounded-lg border border-border bg-surface-2 px-3 py-2 flex items-center gap-2 text-[11px] text-fg-muted">
        <FileText className="w-3.5 h-3.5" />
        <span className="font-mono truncate">{data.manifestPath}</span>
        {!data.manifestExists && (
          <span className="ml-auto text-status-warning">
            {t("plugins.manifestMissing", { path: "" })}
          </span>
        )}
      </div>
      {filtered.length === 0 ? (
        <Empty />
      ) : (
        filtered.map((p) => <PluginCard key={p.key} plugin={p} />)
      )}
    </div>
  );
}

function PluginCard({ plugin: p }: { plugin: CcPlugin }) {
  const { t } = useTranslation("ccConfig");
  const meta = p.contributes?.pluginJson;
  const description = meta?.description;
  const contribCounts: { key: string; count: number; label: string }[] = [];
  if (p.contributes) {
    if (p.contributes.skills > 0)
      contribCounts.push({
        key: "skills",
        count: p.contributes.skills,
        label: t("plugins.skills", { count: p.contributes.skills }),
      });
    if (p.contributes.agents > 0)
      contribCounts.push({
        key: "agents",
        count: p.contributes.agents,
        label: t("plugins.agents", { count: p.contributes.agents }),
      });
    if (p.contributes.commands > 0)
      contribCounts.push({
        key: "commands",
        count: p.contributes.commands,
        label: t("plugins.commands", { count: p.contributes.commands }),
      });
    if (p.contributes.outputStyles > 0)
      contribCounts.push({
        key: "outputStyles",
        count: p.contributes.outputStyles,
        label: t("plugins.outputStyles", { count: p.contributes.outputStyles }),
      });
    if (p.contributes.hooks > 0)
      contribCounts.push({
        key: "hooks",
        count: p.contributes.hooks,
        label: t("plugins.hooks", { count: p.contributes.hooks }),
      });
  }
  return (
    <div className="rounded-lg border border-border bg-surface-2 px-4 py-3">
      <div className="flex items-start justify-between gap-3">
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-2 flex-wrap">
            <span className="font-mono text-sm text-fg-primary">{p.name}</span>
            {p.marketplace && (
              <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface-3 text-fg-secondary border border-border">
                {p.marketplace}
              </span>
            )}
            {p.version && (
              <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20">
                v{p.version}
              </span>
            )}
            <ScopeBadge scope={p.scope} />
            {p.enabled === true && (
              <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-status-success/10 text-status-success border border-status-success/30 inline-flex items-center gap-1">
                <CircleDot className="w-3 h-3" />
                {t("plugins.enabled")}
              </span>
            )}
            {p.enabled === false && (
              <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface-4/10 text-fg-secondary border border-border-light/30 inline-flex items-center gap-1">
                <CircleSlash className="w-3 h-3" />
                {t("plugins.disabled")}
              </span>
            )}
            {!p.installPathExists && (
              <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-status-warning/10 text-status-warning border border-status-warning/30 inline-flex items-center gap-1">
                <AlertCircle className="w-3 h-3" />
                {t("plugins.missing")}
              </span>
            )}
          </div>
          {description && (
            <p className="mt-1.5 text-xs text-fg-secondary leading-relaxed">{description}</p>
          )}
          {contribCounts.length > 0 && (
            <div className="mt-2.5">
              <div className="text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1">
                {t("plugins.contributes")}
              </div>
              <div className="flex flex-wrap gap-1.5">
                {contribCounts.map((c) => (
                  <span
                    key={c.key}
                    className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-surface-3 text-fg-secondary border border-border"
                  >
                    {c.label}
                  </span>
                ))}
              </div>
            </div>
          )}
          <div className="mt-2 grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 text-[11px] text-fg-muted">
            {meta?.author?.name && (
              <div>
                <span className="text-fg-muted">{t("plugins.author")}:</span> {meta.author.name}
              </div>
            )}
            {meta?.license && (
              <div>
                <span className="text-fg-muted">{t("plugins.license")}:</span> {meta.license}
              </div>
            )}
            {p.installedAt && (
              <div>
                <span className="text-fg-muted">{t("plugins.installedAt")}:</span>{" "}
                {new Date(p.installedAt).toLocaleString()}
              </div>
            )}
            {p.lastUpdated && (
              <div>
                <span className="text-fg-muted">{t("plugins.lastUpdated")}:</span>{" "}
                {new Date(p.lastUpdated).toLocaleString()}
              </div>
            )}
            {p.gitCommitSha && (
              <div className="col-span-2">
                <span className="text-fg-muted">SHA:</span>{" "}
                <span className="font-mono">{p.gitCommitSha.slice(0, 12)}</span>
              </div>
            )}
            {meta?.homepage && (
              <div className="col-span-2 truncate">
                <span className="text-fg-muted">{t("plugins.homepage")}:</span>{" "}
                <a
                  href={meta.homepage}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-accent hover:underline"
                >
                  {meta.homepage}
                </a>
              </div>
            )}
          </div>
          {p.installPath && (
            <div className="mt-2 flex items-center gap-2">
              <span className="font-mono text-[10px] text-fg-muted truncate flex-1">
                {p.installPath}
              </span>
              <CopyButton value={p.installPath} />
            </div>
          )}
          <div className="mt-3">
            <CommandSnippet
              command={`claude plugin uninstall ${p.key}`}
              label={t("explain.plugins.uninstall")}
            />
          </div>
        </div>
      </div>
    </div>
  );
}

// ── MCP servers ───────────────────────────────────────────────────────

function McpPanel({ data, search }: { data: CcMcpResponse | null; search: string }) {
  const { t } = useTranslation("ccConfig");
  if (!data) return <SkeletonRows n={3} />;
  const all = [...data.user, ...data.projectScoped];
  const filter = (arr: CcMcpServer[]) =>
    arr.filter((s) => !search || s.name.toLowerCase().includes(search.toLowerCase()));
  return (
    <div className="space-y-4">
      <ExplainerBanner
        title={t("explain.mcp.title")}
        body={t("explain.mcp.body")}
        howTo={t("explain.mcp.add")}
        commands={[
          { cmd: t("explain.mcp.listCmd"), note: t("explain.mcp.list") },
          { cmd: t("explain.mcp.addCmd"), note: t("explain.mcp.add") },
        ]}
      />
      {all.length === 0 && (
        <div className="rounded-lg border border-border bg-surface-2 px-4 py-6 text-center text-sm text-fg-muted">
          {t("mcp.noServers")}
        </div>
      )}
      {data.user.length > 0 && (
        <div>
          <h3 className="text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-2">
            {t("mcp.userScope")}
          </h3>
          <div className="space-y-2">
            {filter(data.user).map((s) => (
              <McpCard key={`u:${s.name}:${s.source}`} server={s} />
            ))}
          </div>
        </div>
      )}
      {data.projectScoped.length > 0 && (
        <div>
          <h3 className="text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-2">
            {t("mcp.projectScope")}
          </h3>
          <div className="space-y-2">
            {filter(data.projectScoped).map((s) => (
              <McpCard key={`p:${s.name}:${s.source}`} server={s} />
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function McpCard({ server }: { server: CcMcpServer }) {
  const { t } = useTranslation("ccConfig");
  return (
    <div className="rounded-lg border border-border bg-surface-2 px-4 py-3">
      <div className="flex items-center gap-2 flex-wrap">
        <span className="font-mono text-sm text-fg-primary">{server.name}</span>
        <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface-3 text-fg-secondary border border-border">
          {server.kind}
        </span>
        <span className="text-[10px] text-fg-muted ml-auto truncate max-w-xs">{server.source}</span>
      </div>
      <div className="mt-2 space-y-1 text-[11px]">
        {server.kind === "stdio" && (
          <>
            <Field label={t("mcp.command")}>
              <span className="font-mono text-fg-secondary">{server.command}</span>
            </Field>
            {server.args && server.args.length > 0 && (
              <Field label={t("mcp.args")}>
                <span className="font-mono text-fg-secondary">{server.args.join(" ")}</span>
              </Field>
            )}
            {server.envNames && server.envNames.length > 0 && (
              <Field label={t("mcp.env")}>
                <span className="font-mono text-fg-secondary">{server.envNames.join(", ")}</span>
              </Field>
            )}
          </>
        )}
        {server.kind === "http" && (
          <>
            <Field label={t("mcp.url")}>
              <span className="font-mono text-fg-secondary">{server.url}</span>
            </Field>
            {server.headers && server.headers.length > 0 && (
              <Field label={t("mcp.headers")}>
                <span className="font-mono text-fg-secondary">{server.headers.join(", ")}</span>
              </Field>
            )}
          </>
        )}
      </div>
      <div className="mt-3">
        <CommandSnippet
          command={`claude mcp remove ${server.name}`}
          label={t("explain.mcp.remove")}
        />
      </div>
    </div>
  );
}

function Field({ label, children }: { label: string; children: React.ReactNode }) {
  return (
    <div className="flex gap-2">
      <span className="text-fg-muted min-w-20">{label}:</span>
      <span className="min-w-0 flex-1 truncate">{children}</span>
    </div>
  );
}

// ── Hooks ─────────────────────────────────────────────────────────────

function HooksPanel({
  sources,
  scripts,
  search,
  onOpen,
}: {
  sources: CcHookSource[] | null;
  scripts: CcHookScripts | null;
  search: string;
  onOpen: (p: string) => void;
}) {
  const { t } = useTranslation("ccConfig");
  if (!sources) return <SkeletonRows n={3} />;
  return (
    <div className="space-y-4">
      <ExplainerBanner
        title={t("explain.hooks.title")}
        body={t("explain.hooks.body")}
        howTo={t("explain.hooks.howTo")}
        commands={[
          { cmd: t("explain.hooks.cmd1"), note: t("explain.hooks.cmd1Note") },
          { cmd: t("explain.hooks.cmd2"), note: t("explain.hooks.cmd2Note") },
          { cmd: t("explain.hooks.cmd3"), note: t("explain.hooks.cmd3Note") },
        ]}
      />
      {sources.map((src) => {
        const events = Object.entries(src.hooks);
        const filteredEvents = search
          ? events.filter(([event]) => event.toLowerCase().includes(search.toLowerCase()))
          : events;
        return (
          <div key={src.scope} className="rounded-lg border border-border bg-surface-2">
            <div className="border-b border-border px-4 py-2.5 flex items-center gap-2">
              <ScopeBadge scope={src.scope} />
              <span className="font-mono text-[11px] text-fg-muted truncate flex-1">
                {src.file}
              </span>
              {src.exists ? (
                <button
                  onClick={() => onOpen(src.file)}
                  className="text-[11px] font-medium px-2 py-1 rounded-md border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1.5"
                >
                  <ExternalLink className="w-3 h-3" />
                  {t("common.viewSource")}
                </button>
              ) : (
                <span className="text-[11px] text-fg-muted">{t("hooks.fileMissing")}</span>
              )}
            </div>
            <div className="p-3">
              {filteredEvents.length === 0 ? (
                <div className="text-xs text-fg-muted px-1 py-2">{t("hooks.noHooks")}</div>
              ) : (
                <div className="space-y-3">
                  {filteredEvents.map(([event, entries]) => (
                    <div key={event}>
                      <div className="text-[11px] font-semibold text-fg-secondary mb-1.5 inline-flex items-center gap-2">
                        <Wrench className="w-3 h-3 text-fg-muted" />
                        {event}
                        <span className="text-[10px] text-fg-muted">({entries.length})</span>
                      </div>
                      <div className="space-y-1.5 pl-5">
                        {entries.map((h, idx) => (
                          <div
                            key={`${event}-${idx}`}
                            className="rounded-md border border-border bg-surface-1 px-2.5 py-1.5 text-[11px]"
                          >
                            <div className="flex items-center gap-2">
                              <span className="font-mono text-[10px] text-fg-muted">
                                {t("hooks.matcher")}={h.matcher}
                              </span>
                              <span className="text-[10px] text-fg-muted">·</span>
                              <span className="font-mono text-[10px] text-fg-muted">{h.type}</span>
                              {h.timeout != null && (
                                <span className="text-[10px] text-fg-muted">{h.timeout}ms</span>
                              )}
                            </div>
                            {h.command && (
                              <div className="mt-1 font-mono text-[11px] text-fg-secondary break-all">
                                {h.command}
                              </div>
                            )}
                          </div>
                        ))}
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>
          </div>
        );
      })}

      {scripts && scripts.items.length > 0 && (
        <div className="rounded-lg border border-border bg-surface-2">
          <div className="border-b border-border px-4 py-2.5">
            <div className="text-sm font-medium text-fg-primary">{t("hookScripts.title")}</div>
            <p className="mt-1 text-[11px] text-fg-muted leading-relaxed">
              {t("hookScripts.subtitle")}
            </p>
            <div className="mt-1 font-mono text-[10px] text-fg-muted">{scripts.dir}</div>
          </div>
          <div className="p-3 space-y-1.5">
            {scripts.items.map((s) => (
              <button
                key={s.file}
                onClick={() => onOpen(s.file)}
                className="w-full text-left rounded-md border border-border bg-surface-1 hover:bg-surface-3 px-3 py-1.5 inline-flex items-center gap-2"
              >
                <FileText className="w-3 h-3 text-fg-muted flex-shrink-0" />
                <span className="font-mono text-[11px] text-fg-secondary flex-1 truncate">
                  {s.name}
                </span>
                <span className="text-[10px] text-fg-muted">{formatBytes(s.size)}</span>
                <span className="text-[10px] text-fg-muted hidden md:inline">
                  {new Date(s.mtime).toLocaleDateString()}
                </span>
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// ── Settings ──────────────────────────────────────────────────────────

// The settings that the TUI's `/config` editor manages, in display order.
// Surfaced as a resolved at-a-glance summary so the user sees what `/config`
// set (model, verbose, theme, …) without hunting through the raw JSON files.
// Keys map 1:1 to settings.json keys per https://code.claude.com/docs/en/settings.
const CONFIG_OPTION_GROUPS: { title: string; keys: { key: string; label: string }[] }[] = [
  {
    title: "Model & reasoning",
    keys: [
      { key: "model", label: "Model" },
      { key: "effortLevel", label: "Effort level" },
      { key: "alwaysThinkingEnabled", label: "Always thinking" },
    ],
  },
  {
    title: "Output & display",
    keys: [
      { key: "outputStyle", label: "Output style" },
      { key: "verbose", label: "Verbose output" },
      { key: "theme", label: "Theme" },
      { key: "language", label: "Language" },
      { key: "spinnerTipsEnabled", label: "Spinner tips" },
      { key: "autoScrollEnabled", label: "Auto-scroll" },
    ],
  },
  {
    title: "Session & input",
    keys: [
      { key: "autoCompactEnabled", label: "Auto-compact" },
      { key: "fileCheckpointingEnabled", label: "File checkpointing" },
      { key: "editorMode", label: "Editor mode" },
      { key: "preferredNotifChannel", label: "Notifications" },
      { key: "awaySummaryEnabled", label: "Away summary" },
    ],
  },
];

/**
 * Resolve each /config option across the settings sources (project-local >
 * project > user precedence - later sources in the array win) and render a
 * compact summary. Unset options show as "default" so the view reflects the
 * effective configuration, not just whatever happens to be written to a file.
 */
function CurrentConfigPanel({ sources }: { sources: CcSettingsSource[] }) {
  // Build effective map: { key → { value, scope } }. Sources arrive ordered
  // user → project → project-local, so a later hit overrides an earlier one.
  const effective = new Map<string, { value: unknown; scope: CcSettingsSource["scope"] }>();
  for (const src of sources) {
    if (!src.exists || !src.data || typeof src.data !== "object") continue;
    const data = src.data as Record<string, unknown>;
    for (const group of CONFIG_OPTION_GROUPS) {
      for (const { key } of group.keys) {
        if (Object.prototype.hasOwnProperty.call(data, key)) {
          effective.set(key, { value: data[key], scope: src.scope });
        }
      }
    }
  }
  const setCount = effective.size;

  return (
    <div className="rounded-lg border border-border bg-surface-2">
      <div className="border-b border-border px-4 py-2.5 flex items-center gap-2">
        <SettingsIcon className="w-3.5 h-3.5 text-violet-300/80" />
        <span className="text-sm font-medium text-fg-primary">Current configuration</span>
        <span className="text-[11px] text-fg-muted ml-auto">
          {setCount} option{setCount !== 1 ? "s" : ""} set · the rest use defaults
        </span>
      </div>
      <div className="p-3 space-y-3">
        {CONFIG_OPTION_GROUPS.map((group) => (
          <div key={group.title}>
            <div className="text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5">
              {group.title}
            </div>
            <div className="rounded-md border border-border bg-surface-1 divide-y divide-border">
              {group.keys.map(({ key, label }) => {
                const hit = effective.get(key);
                return (
                  <div
                    key={key}
                    className="px-3 py-1.5 grid grid-cols-[150px_1fr_auto] gap-3 items-center"
                  >
                    <div className="text-[11px] text-fg-secondary truncate">{label}</div>
                    <div className="min-w-0">
                      {hit ? (
                        <SettingsValue value={hit.value} />
                      ) : (
                        <span className="text-[10px] text-fg-muted italic">default</span>
                      )}
                    </div>
                    {hit ? (
                      <ScopeBadge scope={hit.scope} />
                    ) : (
                      <span className="text-[10px] text-fg-muted">-</span>
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function SettingsPanel({
  sources,
  statusline,
  onOpen,
}: {
  sources: CcSettingsSource[] | null;
  statusline: CcStatusline | null;
  onOpen: (p: string) => void;
}) {
  const { t } = useTranslation("ccConfig");
  if (!sources) return <SkeletonRows n={3} />;
  return (
    <div className="space-y-3">
      <ExplainerBanner
        title={t("explain.settings.title")}
        body={t("explain.settings.body")}
        howTo={t("explain.settings.howTo")}
        commands={[
          { cmd: t("explain.settings.cmd1"), note: t("explain.settings.cmd1Note") },
          { cmd: t("explain.settings.cmd2"), note: t("explain.settings.cmd2Note") },
          { cmd: t("explain.settings.cmd3"), note: t("explain.settings.cmd3Note") },
        ]}
      />
      <CurrentConfigPanel sources={sources} />
      <div className="rounded-md border border-status-warning/30 bg-status-warning/5 px-3 py-2 text-[11px] text-status-warning/90 flex items-center gap-2">
        <Info className="w-3.5 h-3.5 flex-shrink-0" />
        {t("common.redactedNotice")}
      </div>
      {statusline && (statusline.config || statusline.scripts.length > 0) && (
        <StatuslineBlock data={statusline} onOpen={onOpen} />
      )}
      {sources.map((src) => (
        <SettingsBlock key={src.scope} source={src} onOpen={onOpen} />
      ))}
    </div>
  );
}

function StatuslineBlock({ data, onOpen }: { data: CcStatusline; onOpen: (p: string) => void }) {
  const { t } = useTranslation("ccConfig");
  return (
    <div className="rounded-lg border border-border bg-surface-2">
      <div className="border-b border-border px-4 py-2.5">
        <div className="text-sm font-medium text-fg-primary">{t("statusline.title")}</div>
      </div>
      <div className="p-3 space-y-3">
        {data.config ? (
          <div>
            <div className="text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5">
              {t("statusline.configured")}
            </div>
            <div className="rounded-md border border-border bg-surface-1 px-3 py-2 text-[11px] font-mono text-fg-secondary">
              <span className="text-fg-muted">type:</span> {data.config.type ?? "-"}
              {data.config.command && (
                <>
                  <br />
                  <span className="text-fg-muted">command:</span> {data.config.command}
                </>
              )}
            </div>
          </div>
        ) : (
          <div className="text-xs text-fg-muted">{t("statusline.noStatusline")}</div>
        )}
        {data.scripts.length > 0 && (
          <div>
            <div className="text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5">
              {t("statusline.scripts")}
            </div>
            <div className="space-y-1.5">
              {data.scripts.map((s) => (
                <button
                  key={s.file}
                  onClick={() => onOpen(s.file)}
                  className="w-full text-left rounded-md border border-border bg-surface-1 hover:bg-surface-3 px-3 py-1.5 inline-flex items-center gap-2"
                >
                  <FileText className="w-3 h-3 text-fg-muted flex-shrink-0" />
                  <span className="font-mono text-[11px] text-fg-secondary flex-1 truncate">
                    {s.file}
                  </span>
                  <span className="text-[10px] text-fg-muted">{formatBytes(s.size)}</span>
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function SettingsBlock({
  source,
  onOpen,
}: {
  source: CcSettingsSource;
  onOpen: (p: string) => void;
}) {
  const { t } = useTranslation("ccConfig");
  const [showRaw, setShowRaw] = useState(false);
  return (
    <div className="rounded-lg border border-border bg-surface-2">
      <div className="border-b border-border px-4 py-2.5 flex items-center gap-2 flex-wrap">
        <ScopeBadge scope={source.scope} />
        <span className="font-mono text-[11px] text-fg-muted truncate flex-1 min-w-0">
          {source.file}
        </span>
        {source.exists ? (
          <>
            <button
              onClick={() => setShowRaw((v) => !v)}
              className="text-[11px] font-medium px-2 py-1 rounded-md border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary"
            >
              {showRaw ? "Structured" : "Raw JSON"}
            </button>
            <button
              onClick={() => onOpen(source.file)}
              className="text-[11px] font-medium px-2 py-1 rounded-md border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1.5"
            >
              <ExternalLink className="w-3 h-3" />
              {t("common.viewSource")}
            </button>
          </>
        ) : (
          <span className="text-[11px] text-fg-muted">{t("settings.fileMissing")}</span>
        )}
      </div>
      {source.exists &&
        (showRaw ? (
          <pre className="p-3 text-[11px] font-mono text-fg-secondary overflow-auto max-h-96">
            {JSON.stringify(source.data, null, 2)}
          </pre>
        ) : (
          <SettingsKeyValueList data={source.data as Record<string, unknown> | null | undefined} />
        ))}
    </div>
  );
}

function SettingsKeyValueList({ data }: { data: Record<string, unknown> | null | undefined }) {
  if (!data || typeof data !== "object") {
    return <div className="p-3 text-xs text-fg-muted">-</div>;
  }
  const entries = Object.entries(data);
  if (entries.length === 0) {
    return <div className="p-3 text-xs text-fg-muted">{}</div>;
  }
  return (
    <div className="divide-y divide-border">
      {entries.map(([k, v]) => (
        <div
          key={k}
          className="px-3 py-2 grid grid-cols-1 md:grid-cols-[180px_1fr] gap-1 md:gap-3 items-start"
        >
          <div className="font-mono text-[11px] text-fg-secondary truncate">{k}</div>
          <div className="min-w-0">
            <SettingsValue value={v} />
          </div>
        </div>
      ))}
    </div>
  );
}

function SettingsValue({ value }: { value: unknown }) {
  if (value === null || value === undefined)
    return <span className="text-[11px] text-fg-muted italic">null</span>;
  if (typeof value === "boolean") {
    return (
      <span
        className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${
          value
            ? "bg-status-success/10 text-status-success border-status-success/30"
            : "bg-surface-4/10 text-fg-secondary border-border-light/30"
        }`}
      >
        {value ? "true" : "false"}
      </span>
    );
  }
  if (typeof value === "number") {
    return <span className="font-mono text-[11px] text-fg-secondary">{value}</span>;
  }
  if (typeof value === "string") {
    return <span className="font-mono text-[11px] text-fg-secondary break-all">{value}</span>;
  }
  if (Array.isArray(value)) {
    if (value.length === 0) return <span className="text-[11px] text-fg-muted">[]</span>;
    return (
      <div className="flex flex-wrap gap-1">
        {value.map((item, i) => (
          <span
            key={i}
            className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface-3 text-fg-secondary border border-border break-all"
          >
            {typeof item === "object" ? JSON.stringify(item) : String(item)}
          </span>
        ))}
      </div>
    );
  }
  // object
  const obj = value as Record<string, unknown>;
  return (
    <div className="space-y-0.5">
      {Object.entries(obj).map(([k, v]) => (
        <div key={k} className="font-mono text-[11px]">
          <span className="text-fg-muted">{k}:</span>{" "}
          <span className="text-fg-secondary break-all">
            {typeof v === "object" ? JSON.stringify(v) : String(v)}
          </span>
        </div>
      ))}
    </div>
  );
}

// ── Memory ────────────────────────────────────────────────────────────

interface MemoryPanelProps {
  items: CcMemoryItem[] | null;
  search: string;
  onOpen: (p: string) => void;
  onEdit: (
    type: CcArtifactType,
    item: { scope: "user" | "project"; name: string; filePath: string }
  ) => void;
  onDelete: (
    type: CcArtifactType,
    scope: "user" | "project",
    name: string | undefined,
    path: string
  ) => void;
  onCreate: (scope: "user" | "project") => void;
  onEditAuto: (item: CcMemoryItem) => void;
  onDeleteAuto: (item: CcMemoryItem) => void;
  onCreateAuto: (project: string) => void;
}

// Strip a leading markdown heading then take a short snippet — used when a
// per-fact memory file has no frontmatter description.
function memoryDescription(m: CcMemoryItem): string {
  return (
    m.frontmatter?.description ||
    m.preview
      .replace(/^#+\s.*\n/, "")
      .trim()
      .slice(0, 200)
  );
}

// Reduce a markdown link target (as written inside MEMORY.md, e.g.
// `./feedback_x.md#section` or `feedback_x.md`) to the bare filename we can
// match against a fact file's `name`. Tolerant of URL-encoding and anchors.
function normalizeMemoryTarget(target: string): string {
  let v = target.trim();
  const hash = v.indexOf("#");
  if (hash >= 0) v = v.slice(0, hash);
  try {
    v = decodeURIComponent(v);
  } catch {
    /* leave as-is when not valid percent-encoding */
  }
  const slash = v.lastIndexOf("/");
  if (slash >= 0) v = v.slice(slash + 1);
  return v.trim();
}

// Render a MEMORY.md preview with its `[label](target.md)` markdown links
// turned into clickable buttons. Everything else is emitted verbatim so the
// surrounding <pre> keeps the original index layout. Clicking a link asks the
// parent to jump to (scroll + highlight) the matching fact file.
function renderMemoryIndex(
  preview: string,
  onJump: (target: string) => void,
  jumpTitle: string
): React.ReactNode {
  const linkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
  const lines = preview.split("\n");
  return lines.map((line, li) => {
    const parts: React.ReactNode[] = [];
    let last = 0;
    let m: RegExpExecArray | null;
    linkRe.lastIndex = 0;
    while ((m = linkRe.exec(line)) !== null) {
      const full = m[0];
      const label = m[1] ?? "";
      const capturedTarget = m[2] ?? "";
      if (m.index > last) parts.push(line.slice(last, m.index));
      parts.push(
        <button
          key={`${li}-${m.index}`}
          type="button"
          onClick={() => onJump(capturedTarget)}
          title={`${jumpTitle}: ${normalizeMemoryTarget(capturedTarget)}`}
          className="text-teal-300 hover:text-teal-200 underline decoration-dotted underline-offset-2 hover:decoration-solid focus:outline-none focus:ring-1 focus:ring-teal-400/60 rounded-sm"
        >
          {label}
        </button>
      );
      last = m.index + full.length;
    }
    if (last < line.length) parts.push(line.slice(last));
    return (
      <span key={li}>
        {parts.length ? parts : line}
        {li < lines.length - 1 ? "\n" : null}
      </span>
    );
  });
}

function MemoryPanel({
  items,
  search,
  onOpen,
  onEdit,
  onDelete,
  onCreate,
  onEditAuto,
  onDeleteAuto,
  onCreateAuto,
}: MemoryPanelProps) {
  const { t } = useTranslation("ccConfig");

  const q = search.trim().toLowerCase();

  const { primary, autoFiltered, groups, missingScopes } = useMemo(() => {
    const list = items ?? [];
    const primaryItems = list.filter(
      (m): m is CcMemoryItem & { scope: "user" | "project" } =>
        m.scope === "user" || m.scope === "project"
    );
    const autoItems = list.filter((m) => m.scope === "auto-memory");

    const matchesAuto = (m: CcMemoryItem) => {
      if (!q) return true;
      const blob = [m.name, m.project, m.frontmatter?.description, m.frontmatter?.name, m.preview]
        .filter(Boolean)
        .join(" ")
        .toLowerCase();
      return blob.includes(q);
    };

    // Search applies to the whole tab — match the CLAUDE.md cards on their
    // scope label, path, and body too so the filter is consistent.
    const primaryFilteredItems = primaryItems.filter((m) => {
      if (!q) return true;
      return [m.scope, m.file, m.preview].join(" ").toLowerCase().includes(q);
    });

    const filtered = autoItems.filter(matchesAuto);

    // Group surviving auto-memory files by their project dir.
    const byProject = new Map<string, CcMemoryItem[]>();
    for (const m of filtered) {
      const key = m.project || "(unknown)";
      if (!byProject.has(key)) byProject.set(key, []);
      byProject.get(key)!.push(m);
    }
    const grouped = [...byProject.entries()].sort((a, b) => a[0].localeCompare(b[0]));

    const present = new Set(primaryItems.map((m) => m.scope));
    const missing = (["user", "project"] as const).filter((s) => !present.has(s));

    return {
      primary: primaryFilteredItems,
      autoFiltered: filtered,
      groups: grouped,
      missingScopes: missing,
    };
  }, [items, q]);

  if (!items) return <SkeletonRows n={2} />;

  const totalAuto = items.filter((m) => m.scope === "auto-memory").length;
  // The "create missing CLAUDE.md" prompts only make sense when not filtering.
  const showMissing = !q;

  return (
    <div className="space-y-3">
      {/* Primary CLAUDE.md memory (user + project) — editable */}
      {primary.map((m) => (
        <div key={m.scope} className="rounded-lg border border-border bg-surface-2">
          <div className="border-b border-border px-4 py-2.5 flex items-center gap-2 flex-wrap">
            <ScopeBadge scope={m.scope} />
            <span className="font-mono text-[11px] text-fg-muted truncate flex-1 min-w-0">
              {m.file}
            </span>
            <span className="text-[10px] text-fg-muted">{formatBytes(m.size)}</span>
            <button
              onClick={() => onOpen(m.file)}
              className="text-[11px] font-medium px-2 py-1 rounded-md border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1.5"
            >
              <ExternalLink className="w-3 h-3" />
              {t("common.viewSource")}
            </button>
            <button
              onClick={() => onEdit("memory", { scope: m.scope, name: "", filePath: m.file })}
              className="text-[11px] font-medium px-2 py-1 rounded-md border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1.5"
            >
              <Pencil className="w-3 h-3" />
              {t("edit.editButton")}
            </button>
            <button
              onClick={() => onDelete("memory", m.scope, undefined, m.file)}
              className="text-[11px] font-medium px-2 py-1 rounded-md border border-status-danger/30 bg-status-danger/5 hover:bg-status-danger/15 text-status-danger inline-flex items-center gap-1.5"
            >
              <Trash2 className="w-3 h-3" />
              {t("edit.deleteButton")}
            </button>
          </div>
          <pre className="p-3 text-[11px] font-mono text-fg-secondary whitespace-pre-wrap break-words max-h-72 overflow-auto">
            {m.preview}
            {m.truncated && (
              <span className="text-fg-muted italic">
                {"\n\n"}
                {t("common.truncated")}
              </span>
            )}
          </pre>
        </div>
      ))}

      {showMissing &&
        missingScopes.map((s) => (
          <div
            key={`missing-${s}`}
            className="rounded-lg border border-dashed border-border bg-surface-2 px-4 py-4 flex items-center justify-between gap-3"
          >
            <div className="flex items-center gap-2">
              <ScopeBadge scope={s} />
              <span className="text-xs text-fg-muted">{t("memory.missing")}</span>
            </div>
            <button
              onClick={() => onCreate(s)}
              className="text-[11px] font-medium px-2.5 py-1 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
            >
              <Plus className="w-3 h-3" />
              {t("edit.newButton")}
            </button>
          </div>
        ))}

      {/* Per-project file-based memory (~/.claude/projects/<slug>/memory/) */}
      {totalAuto > 0 && (
        <section className="pt-1">
          <div className="flex items-center gap-2 mb-1">
            <BookOpen className="w-3.5 h-3.5 text-teal-300" />
            <h3 className="text-[11px] font-semibold uppercase tracking-wider text-fg-secondary">
              {t("memory.autoTitle")}
            </h3>
            <span className="text-[10px] px-1.5 py-0.5 rounded-full font-semibold bg-surface-3 text-fg-secondary">
              {q ? `${autoFiltered.length}/${totalAuto}` : totalAuto}
            </span>
          </div>
          <p className="text-[11px] text-fg-muted mb-2.5 leading-relaxed">
            {t("memory.autoSubtitle")}
          </p>

          {groups.length === 0 ? (
            <div className="rounded-lg border border-border bg-surface-2 px-4 py-6 text-center text-sm text-fg-muted">
              {t("memory.noMatches")}
            </div>
          ) : (
            <div className="space-y-2">
              {groups.map(([project, files]) => (
                <MemoryProjectGroup
                  key={project}
                  project={project}
                  files={files}
                  onOpen={onOpen}
                  onEditAuto={onEditAuto}
                  onDeleteAuto={onDeleteAuto}
                  onCreateAuto={onCreateAuto}
                  defaultOpen={!!q || groups.length === 1}
                />
              ))}
            </div>
          )}
        </section>
      )}
    </div>
  );
}

interface MemoryProjectGroupProps {
  project: string;
  files: CcMemoryItem[];
  onOpen: (p: string) => void;
  onEditAuto: (item: CcMemoryItem) => void;
  onDeleteAuto: (item: CcMemoryItem) => void;
  onCreateAuto: (project: string) => void;
  defaultOpen: boolean;
}

function MemoryProjectGroup({
  project,
  files,
  onOpen,
  onEditAuto,
  onDeleteAuto,
  onCreateAuto,
  defaultOpen,
}: MemoryProjectGroupProps) {
  const { t } = useTranslation("ccConfig");
  const [open, setOpen] = useState(defaultOpen);
  // Re-sync when the search-driven default flips (expand on search, collapse
  // when cleared). User toggles within a stable search state are preserved.
  useEffect(() => {
    setOpen(defaultOpen);
  }, [defaultOpen]);

  const indexFiles = files.filter((f) => f.isIndex);
  const factFiles = files.filter((f) => !f.isIndex);

  // Wiring for "click an index entry → jump to its fact file". Fact rows
  // register their DOM node keyed by filename; the index links look them up.
  const rowRefs = useRef<Map<string, HTMLDivElement>>(new Map());
  const [highlighted, setHighlighted] = useState<string | null>(null);
  const highlightTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(
    () => () => {
      if (highlightTimer.current) clearTimeout(highlightTimer.current);
    },
    []
  );

  const rowKey = useCallback((m: CcMemoryItem) => m.name || normalizeMemoryTarget(m.file), []);

  const handleJump = useCallback(
    (target: string) => {
      const name = normalizeMemoryTarget(target);
      const el = rowRefs.current.get(name);
      if (el) {
        el.scrollIntoView({ behavior: "smooth", block: "center" });
        setHighlighted(name);
        if (highlightTimer.current) clearTimeout(highlightTimer.current);
        highlightTimer.current = setTimeout(() => setHighlighted(null), 2200);
        return;
      }
      // Target isn't currently in view (e.g. filtered out by search) — open the
      // underlying file directly if we can resolve it within this project.
      const match = files.find((f) => (f.name || normalizeMemoryTarget(f.file)) === name);
      if (match) onOpen(match.file);
    },
    [files, onOpen]
  );

  return (
    <div className="rounded-lg border border-border bg-surface-2 overflow-hidden">
      <div className="flex items-center gap-1 pr-2 hover:bg-surface-3 transition-colors">
        <button
          onClick={() => setOpen((v) => !v)}
          className="flex items-center gap-2 px-3 py-2.5 text-left flex-1 min-w-0"
        >
          <ChevronDown
            className={`w-3.5 h-3.5 text-fg-muted flex-shrink-0 transition-transform ${
              open ? "" : "-rotate-90"
            }`}
          />
          <FolderTree className="w-3.5 h-3.5 text-teal-300 flex-shrink-0" />
          <span className="font-mono text-xs text-fg-secondary truncate flex-1 min-w-0">
            {project}
          </span>
          <span className="text-[10px] text-fg-muted flex-shrink-0">
            {t("memory.fileCount", { count: files.length })}
          </span>
        </button>
        <button
          onClick={() => onCreateAuto(project)}
          title={t("memory.newFile")}
          className="text-[10px] font-medium px-1.5 py-0.5 rounded border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1 flex-shrink-0"
        >
          <Plus className="w-2.5 h-2.5" />
          {t("memory.newFile")}
        </button>
      </div>

      {open && (
        <div className="border-t border-border p-2.5 space-y-2.5">
          {indexFiles.length > 0 && (
            <div>
              <div className="text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5 px-1">
                {t("memory.indexFiles")}
              </div>
              <div className="space-y-1.5">
                {indexFiles.map((m) => (
                  <MemoryIndexCard
                    key={m.file}
                    item={m}
                    onOpen={onOpen}
                    onEditAuto={onEditAuto}
                    onDeleteAuto={onDeleteAuto}
                    onJump={handleJump}
                  />
                ))}
              </div>
            </div>
          )}
          {factFiles.length > 0 && (
            <div>
              <div className="text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5 px-1">
                {t("memory.factFiles", { count: factFiles.length })}
              </div>
              <div className="space-y-1">
                {factFiles.map((m) => {
                  const key = rowKey(m);
                  return (
                    <MemoryFactRow
                      key={m.file}
                      item={m}
                      onOpen={onOpen}
                      onEditAuto={onEditAuto}
                      onDeleteAuto={onDeleteAuto}
                      highlighted={highlighted === key}
                      rowRef={(el) => {
                        if (el) rowRefs.current.set(key, el);
                        else rowRefs.current.delete(key);
                      }}
                    />
                  );
                })}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

interface MemoryAutoItemProps {
  item: CcMemoryItem;
  onOpen: (p: string) => void;
  onEditAuto: (item: CcMemoryItem) => void;
  onDeleteAuto: (item: CcMemoryItem) => void;
}

// Compact View / Edit / Delete button cluster shared by index + fact rows.
function MemoryAutoActions({ item, onOpen, onEditAuto, onDeleteAuto }: MemoryAutoItemProps) {
  const { t } = useTranslation("ccConfig");
  return (
    <div className="flex items-center gap-1 flex-shrink-0">
      <button
        onClick={() => onOpen(item.file)}
        title={t("common.viewSource")}
        className="text-[10px] font-medium px-1.5 py-0.5 rounded border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1"
      >
        <ExternalLink className="w-2.5 h-2.5" />
      </button>
      <button
        onClick={() => onEditAuto(item)}
        title={t("edit.editButton")}
        className="text-[10px] font-medium px-1.5 py-0.5 rounded border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1"
      >
        <Pencil className="w-2.5 h-2.5" />
      </button>
      <button
        onClick={() => onDeleteAuto(item)}
        title={t("edit.deleteButton")}
        className="text-[10px] font-medium px-1.5 py-0.5 rounded border border-status-danger/30 bg-status-danger/5 hover:bg-status-danger/15 text-status-danger inline-flex items-center gap-1"
      >
        <Trash2 className="w-2.5 h-2.5" />
      </button>
    </div>
  );
}

function MemoryIndexCard({
  item,
  onOpen,
  onEditAuto,
  onDeleteAuto,
  onJump,
}: MemoryAutoItemProps & { onJump: (target: string) => void }) {
  const { t } = useTranslation("ccConfig");
  return (
    <div className="rounded-md border border-teal-500/20 bg-teal-500/5">
      <div className="flex items-center gap-2 px-3 py-1.5 border-b border-teal-500/15">
        <BookOpen className="w-3 h-3 text-teal-300 flex-shrink-0" />
        <span className="font-mono text-[11px] text-fg-secondary truncate flex-1 min-w-0">
          {item.name}
        </span>
        <span className="text-[10px] text-fg-muted">{formatBytes(item.size)}</span>
        <MemoryAutoActions
          item={item}
          onOpen={onOpen}
          onEditAuto={onEditAuto}
          onDeleteAuto={onDeleteAuto}
        />
      </div>
      <pre className="px-3 py-2 text-[10.5px] font-mono text-fg-secondary whitespace-pre-wrap break-words max-h-40 overflow-auto">
        {renderMemoryIndex(item.preview, onJump, t("memory.jumpTo"))}
        {item.truncated && <span className="text-fg-muted italic">{"\n…"}</span>}
      </pre>
    </div>
  );
}

function MemoryFactRow({
  item,
  onOpen,
  onEditAuto,
  onDeleteAuto,
  highlighted,
  rowRef,
}: MemoryAutoItemProps & {
  highlighted?: boolean;
  rowRef?: (el: HTMLDivElement | null) => void;
}) {
  const desc = memoryDescription(item);
  return (
    <div
      ref={rowRef}
      className={`flex items-start gap-2.5 rounded-md border px-3 py-2 transition-colors ${
        highlighted
          ? "border-teal-400/70 bg-teal-500/10 ring-1 ring-teal-400/50"
          : "border-border bg-surface-1 hover:border-border/80"
      }`}
    >
      <FileText className="w-3 h-3 text-fg-muted flex-shrink-0 mt-0.5" />
      <div className="min-w-0 flex-1">
        <span className="font-mono text-[11px] text-fg-secondary truncate block">{item.name}</span>
        {desc && (
          <p className="mt-0.5 text-[11px] text-fg-muted leading-snug line-clamp-2">{desc}</p>
        )}
      </div>
      <span className="text-[10px] text-fg-muted flex-shrink-0 mt-0.5">
        {formatBytes(item.size)}
      </span>
      <div className="mt-0.5">
        <MemoryAutoActions
          item={item}
          onOpen={onOpen}
          onEditAuto={onEditAuto}
          onDeleteAuto={onDeleteAuto}
        />
      </div>
    </div>
  );
}

// ── Marketplaces ──────────────────────────────────────────────────────

function MarketplacesPanel({
  data,
  search,
}: {
  data: CcMarketplacesResponse | null;
  search: string;
}) {
  const { t } = useTranslation("ccConfig");
  if (!data) return <SkeletonRows n={3} />;
  const filtered = data.items.filter(
    (m) =>
      !search ||
      m.name.toLowerCase().includes(search.toLowerCase()) ||
      (m.marketplaceName || "").toLowerCase().includes(search.toLowerCase())
  );
  return (
    <div className="space-y-3">
      <ExplainerBanner
        title={t("explain.plugins.title")}
        body={t("explain.plugins.body")}
        howTo={t("marketplaces.manifest")}
        commands={[{ cmd: t("marketplaces.addCmd"), note: "" }]}
      />
      <div className="rounded-lg border border-border bg-surface-2 px-3 py-2 flex items-center gap-2 text-[11px] text-fg-muted">
        <FileText className="w-3.5 h-3.5" />
        <span className="font-mono truncate">{data.knownPath}</span>
      </div>
      {filtered.length === 0 ? (
        <div className="rounded-lg border border-border bg-surface-2 px-4 py-6 text-center text-sm text-fg-muted">
          {t("marketplaces.noMarketplaces")}
        </div>
      ) : (
        filtered.map((m) => (
          <div key={m.name} className="rounded-lg border border-border bg-surface-2 px-4 py-3">
            <div className="flex items-center gap-2 flex-wrap">
              <Store className="w-3.5 h-3.5 text-fg-muted" />
              <span className="font-mono text-sm text-fg-primary">{m.name}</span>
              {m.marketplaceName && m.marketplaceName !== m.name && (
                <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface-3 text-fg-secondary border border-border">
                  {m.marketplaceName}
                </span>
              )}
              {m.pluginCount != null && (
                <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20">
                  {t("marketplaces.pluginCount")}: {m.pluginCount}
                </span>
              )}
            </div>
            {m.marketplaceDescription && (
              <p className="mt-1.5 text-xs text-fg-secondary leading-relaxed line-clamp-2">
                {m.marketplaceDescription}
              </p>
            )}
            <div className="mt-2 grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 text-[11px] text-fg-muted">
              {m.source && (
                <div className="col-span-2 truncate">
                  <span className="text-fg-muted">{t("marketplaces.source")}:</span>{" "}
                  <span className="font-mono">
                    {m.source.source === "github" && m.source.repo
                      ? `github.com/${m.source.repo}`
                      : m.source.url || m.source.repo || JSON.stringify(m.source)}
                  </span>
                </div>
              )}
              {m.marketplaceOwner?.name && (
                <div>
                  <span className="text-fg-muted">{t("marketplaces.owner")}:</span>{" "}
                  {m.marketplaceOwner.name}
                </div>
              )}
              {m.lastUpdated && (
                <div>
                  <span className="text-fg-muted">{t("marketplaces.lastUpdated")}:</span>{" "}
                  {new Date(m.lastUpdated).toLocaleString()}
                </div>
              )}
            </div>
            {m.installLocation && (
              <div className="mt-2 flex items-center gap-2">
                <span className="font-mono text-[10px] text-fg-muted truncate flex-1">
                  {m.installLocation}
                </span>
                <CopyButton value={m.installLocation} />
              </div>
            )}
          </div>
        ))
      )}
    </div>
  );
}

// ── Keybindings ───────────────────────────────────────────────────────

function KeybindingsPanel({
  data,
  search,
  onSaved,
  onToast,
}: {
  data: CcKeybindings | null;
  search: string;
  onSaved: () => void;
  onToast: (toast: NonNullable<Toast>) => void;
}) {
  const { t } = useTranslation("ccConfig");
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState<CcKeybindingGroup[]>([]);
  const [saving, setSaving] = useState(false);
  const [err, setErr] = useState<string | null>(null);

  const startEdit = useCallback(() => {
    const groups = data?.groups ?? [];
    // Deep clone so edits never mutate the fetched data.
    setDraft(
      groups.map((g) => ({ context: g.context, bindings: g.bindings.map((b) => ({ ...b })) }))
    );
    setErr(null);
    setEditing(true);
  }, [data]);

  const cancelEdit = useCallback(() => {
    setEditing(false);
    setDraft([]);
    setErr(null);
  }, []);

  const updateContext = (gi: number, value: string) =>
    setDraft((d) => d.map((g, i) => (i === gi ? { ...g, context: value } : g)));
  const removeContext = (gi: number) => setDraft((d) => d.filter((_, i) => i !== gi));
  const addContext = () =>
    setDraft((d) => [...d, { context: "", bindings: [{ key: "", action: "" }] }]);
  const updateBinding = (gi: number, bi: number, field: "key" | "action", value: string) =>
    setDraft((d) =>
      d.map((g, i) =>
        i === gi
          ? { ...g, bindings: g.bindings.map((b, j) => (j === bi ? { ...b, [field]: value } : b)) }
          : g
      )
    );
  const removeBinding = (gi: number, bi: number) =>
    setDraft((d) =>
      d.map((g, i) => (i === gi ? { ...g, bindings: g.bindings.filter((_, j) => j !== bi) } : g))
    );
  const addBinding = (gi: number) =>
    setDraft((d) =>
      d.map((g, i) => (i === gi ? { ...g, bindings: [...g.bindings, { key: "", action: "" }] } : g))
    );

  const handleSave = useCallback(async () => {
    const groups: CcKeybindingGroup[] = draft.map((g) => ({
      context: g.context.trim(),
      bindings: g.bindings.map((b) => ({ key: b.key.trim(), action: b.action.trim() })),
    }));
    // Mirror the server-side validation so users get instant, local feedback.
    const seen = new Set<string>();
    for (const g of groups) {
      if (!g.context) return setErr(t("keybindings.errContext"));
      if (seen.has(g.context))
        return setErr(t("keybindings.errDupContext", { context: g.context }));
      seen.add(g.context);
      const keys = new Set<string>();
      for (const b of g.bindings) {
        if (!b.key || !b.action) return setErr(t("keybindings.errEmpty", { context: g.context }));
        if (keys.has(b.key))
          return setErr(t("keybindings.errDupKey", { key: b.key, context: g.context }));
        keys.add(b.key);
      }
    }
    setSaving(true);
    setErr(null);
    try {
      const result = await api.ccConfig.writeKeybindings(groups);
      onToast({
        kind: "success",
        message: result.created
          ? t("edit.saveSuccessNew")
          : t("edit.saveSuccess", { path: result.backupPath || "-" }),
      });
      setEditing(false);
      setDraft([]);
      onSaved();
    } catch (e: unknown) {
      const msg = e instanceof Error ? e.message : "unknown error";
      setErr(msg);
      onToast({ kind: "error", message: t("edit.writeError", { message: msg }) });
    } finally {
      setSaving(false);
    }
  }, [draft, onSaved, onToast, t]);

  if (!data) return <SkeletonRows n={3} />;

  const headerBar = (
    <div className="rounded-lg border border-border bg-surface-2 px-3 py-2 flex items-center gap-2 text-[11px] text-fg-muted flex-wrap">
      <FileText className="w-3.5 h-3.5" />
      <span className="font-mono truncate flex-1 min-w-0">{data.file}</span>
      {data.docs && (
        <a
          href={data.docs}
          target="_blank"
          rel="noopener noreferrer"
          className="text-[11px] text-accent hover:underline inline-flex items-center gap-1"
        >
          <ExternalLink className="w-3 h-3" />
          {t("keybindings.docsLink")}
        </a>
      )}
      {editing ? (
        <div className="inline-flex items-center gap-1.5">
          <button
            type="button"
            onClick={cancelEdit}
            disabled={saving}
            className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-border bg-surface-3 hover:bg-surface-1 text-fg-secondary disabled:opacity-50"
          >
            {t("edit.cancel")}
          </button>
          <button
            type="button"
            onClick={handleSave}
            disabled={saving}
            className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5 disabled:opacity-50"
          >
            <Save className="w-3 h-3" />
            {saving ? t("edit.saving") : t("edit.save")}
          </button>
        </div>
      ) : (
        <button
          type="button"
          onClick={startEdit}
          className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-accent/30 bg-accent/10 hover:bg-accent/20 text-accent inline-flex items-center gap-1.5"
        >
          <Pencil className="w-3 h-3" />
          {t("edit.editButton")}
        </button>
      )}
    </div>
  );

  // ── Edit mode ────────────────────────────────────────────────────────
  if (editing) {
    return (
      <div className="space-y-3">
        {headerBar}
        {err && (
          <div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-3 py-2 text-[11px] text-status-danger flex items-center gap-2">
            <AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
            <span>{err}</span>
          </div>
        )}
        {draft.map((g, gi) => (
          <div key={gi} className="rounded-lg border border-border bg-surface-2">
            <div className="border-b border-border px-3 py-2 flex items-center gap-2">
              <span className="text-[11px] text-fg-muted flex-shrink-0">
                {t("keybindings.context")}
              </span>
              <input
                value={g.context}
                onChange={(e) => updateContext(gi, e.target.value)}
                placeholder={t("keybindings.contextPlaceholder")}
                className="h-7 flex-1 min-w-0 bg-surface-1 border border-border rounded px-2 text-[11px] font-mono text-fg-primary focus:outline-none focus:ring-1 focus:ring-accent/40"
              />
              <button
                type="button"
                onClick={() => removeContext(gi)}
                title={t("keybindings.removeContext")}
                aria-label={t("keybindings.removeContext")}
                className="h-7 w-7 flex-shrink-0 inline-flex items-center justify-center rounded-md text-fg-muted hover:text-status-danger hover:bg-status-danger/10"
              >
                <Trash2 className="w-3.5 h-3.5" />
              </button>
            </div>
            <div className="divide-y divide-border">
              {g.bindings.map((b, bi) => (
                <div key={bi} className="px-3 py-1.5 flex items-center gap-2">
                  <input
                    value={b.key}
                    onChange={(e) => updateBinding(gi, bi, "key", e.target.value)}
                    placeholder={t("keybindings.key")}
                    className="h-7 w-40 flex-shrink-0 bg-surface-1 border border-border rounded px-2 text-[11px] font-mono text-fg-primary focus:outline-none focus:ring-1 focus:ring-accent/40"
                  />
                  <span className="text-fg-muted flex-shrink-0">→</span>
                  <input
                    value={b.action}
                    onChange={(e) => updateBinding(gi, bi, "action", e.target.value)}
                    placeholder={t("keybindings.action")}
                    className="h-7 flex-1 min-w-0 bg-surface-1 border border-border rounded px-2 text-[11px] font-mono text-fg-primary focus:outline-none focus:ring-1 focus:ring-accent/40"
                  />
                  <button
                    type="button"
                    onClick={() => removeBinding(gi, bi)}
                    title={t("keybindings.removeBinding")}
                    aria-label={t("keybindings.removeBinding")}
                    className="h-7 w-7 flex-shrink-0 inline-flex items-center justify-center rounded-md text-fg-muted hover:text-status-danger hover:bg-status-danger/10"
                  >
                    <Trash2 className="w-3.5 h-3.5" />
                  </button>
                </div>
              ))}
              <div className="px-3 py-1.5">
                <button
                  type="button"
                  onClick={() => addBinding(gi)}
                  className="h-7 text-[11px] font-medium px-2.5 rounded-md border border-border bg-surface-3 hover:bg-surface-1 text-fg-secondary inline-flex items-center gap-1.5"
                >
                  <Plus className="w-3 h-3" />
                  {t("keybindings.addBinding")}
                </button>
              </div>
            </div>
          </div>
        ))}
        <button
          type="button"
          onClick={addContext}
          className="h-8 w-full text-[11px] font-medium rounded-md border border-dashed border-border bg-surface-2 hover:bg-surface-3 text-fg-secondary inline-flex items-center justify-center gap-1.5"
        >
          <Plus className="w-3 h-3" />
          {t("keybindings.addContext")}
        </button>
      </div>
    );
  }

  // ── Read-only mode ───────────────────────────────────────────────────
  if (!data.exists) {
    return (
      <div className="space-y-3">
        {headerBar}
        <div className="rounded-lg border border-border bg-surface-2 px-4 py-6 text-center text-sm text-fg-muted">
          {t("keybindings.missing", { path: data.file })}
        </div>
      </div>
    );
  }
  const q = search.toLowerCase();
  return (
    <div className="space-y-3">
      {headerBar}
      {data.groups.map((g) => {
        const filtered = g.bindings.filter(
          (b) =>
            !q ||
            b.key.toLowerCase().includes(q) ||
            b.action.toLowerCase().includes(q) ||
            g.context.toLowerCase().includes(q)
        );
        if (filtered.length === 0) return null;
        return (
          <div key={g.context} className="rounded-lg border border-border bg-surface-2">
            <div className="border-b border-border px-4 py-2 text-xs font-medium text-fg-secondary">
              {t("keybindings.context")}: <span className="text-fg-primary">{g.context}</span>
              <span className="ml-2 text-[10px] text-fg-muted">({filtered.length})</span>
            </div>
            <div className="divide-y divide-border">
              {filtered.map((b) => (
                <div key={b.key} className="px-4 py-1.5 flex items-center gap-3">
                  <kbd className="font-mono text-[11px] px-1.5 py-0.5 rounded bg-surface-3 border border-border text-fg-secondary min-w-20 text-center">
                    {b.key}
                  </kbd>
                  <span className="font-mono text-[11px] text-fg-secondary">{b.action}</span>
                </div>
              ))}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ── Shared atoms ──────────────────────────────────────────────────────

function ScopeBadge({ scope }: { scope: string }) {
  const { t } = useTranslation("ccConfig");
  const color =
    scope === "user"
      ? "bg-sky-500/10 text-sky-300 border-sky-500/30"
      : scope === "project"
        ? "bg-status-success/10 text-status-success border-status-success/30"
        : scope === "project-local"
          ? "bg-violet-500/10 text-violet-300 border-violet-500/30"
          : "bg-surface-3 text-fg-secondary border-border";
  const label =
    scope === "project-local"
      ? t("scope.projectLocal")
      : scope === "user"
        ? t("scope.user")
        : scope === "project"
          ? t("scope.project")
          : scope;
  return (
    <span className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${color}`}>{label}</span>
  );
}

function CopyButton({ value }: { value: string }) {
  const { t } = useTranslation("ccConfig");
  const [copied, setCopied] = useState(false);
  return (
    <button
      onClick={async () => {
        try {
          await navigator.clipboard.writeText(value);
          setCopied(true);
          setTimeout(() => setCopied(false), 1500);
        } catch {
          /* clipboard unavailable */
        }
      }}
      className="text-[10px] font-medium px-1.5 py-1 rounded border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1 flex-shrink-0"
      title={t("common.copyPath")}
    >
      {copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
    </button>
  );
}

function Empty() {
  const { t } = useTranslation("ccConfig");
  return (
    <div className="rounded-lg border border-dashed border-border bg-surface-2 px-4 py-8 text-center text-sm text-fg-muted">
      {t("common.empty")}
    </div>
  );
}

function SkeletonRows({ n }: { n: number }) {
  return (
    <div className="space-y-2">
      {Array.from({ length: n }).map((_, i) => (
        <div key={i} className="h-16 rounded-lg border border-border bg-surface-2 animate-pulse" />
      ))}
    </div>
  );
}

function formatBytes(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}

// ── File viewer modal ─────────────────────────────────────────────────

function FileViewer({
  state,
  onClose,
}: {
  state: { path: string; data: CcFileResponse | null; error: string | null };
  onClose: () => void;
}) {
  const { t } = useTranslation("ccConfig");
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div
      className="fixed inset-0 bg-black/60 z-40 flex items-center justify-center p-4"
      onClick={onClose}
    >
      <div
        className="relative w-full max-w-4xl max-h-[85vh] rounded-xl border border-border bg-surface-1 flex flex-col"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
          <FileText className="w-4 h-4 text-fg-muted" />
          <span className="font-mono text-[12px] text-fg-secondary truncate flex-1">
            {state.path}
          </span>
          <CopyButton value={state.path} />
          <button
            onClick={onClose}
            className="text-fg-secondary hover:text-fg-primary p-1 rounded-md hover:bg-surface-3"
            aria-label={t("common.close")}
          >
            <X className="w-4 h-4" />
          </button>
        </div>
        <div className="overflow-auto p-4">
          {state.error ? (
            <div className="text-sm text-status-danger inline-flex items-center gap-2">
              <AlertCircle className="w-4 h-4" />
              {state.error}
            </div>
          ) : !state.data ? (
            <div className="text-sm text-fg-muted">…</div>
          ) : (
            <pre className="text-[11px] font-mono text-fg-secondary whitespace-pre-wrap break-words">
              {state.data.text}
              {state.data.truncated && (
                <span className="text-fg-muted italic">
                  {"\n\n"}
                  {t("common.truncated")}
                </span>
              )}
            </pre>
          )}
        </div>
      </div>
    </div>
  );
}

// ── Editor modal (create + edit) ──────────────────────────────────────

interface EditorModalProps {
  state: NonNullable<EditorState>;
  onClose: () => void;
  onSave: (args: {
    type: CcArtifactType;
    targetScope: "user" | "project" | "auto-memory";
    name: string | undefined;
    content: string;
    project?: string;
  }) => Promise<void>;
}

function EditorModal({ state, onClose, onSave }: EditorModalProps) {
  const { t } = useTranslation("ccConfig");
  const isCreate = state.mode === "create";
  const isAutoMemory = state.type === "auto-memory";
  const [content, setContent] = useState<string>(isCreate ? state.template : "");
  const [name, setName] = useState<string>("");
  const [targetScope, setTargetScope] = useState<"user" | "project">(
    isCreate ? state.defaultScope : state.scope === "auto-memory" ? "user" : state.scope
  );
  const [loading, setLoading] = useState(!isCreate);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // For edit mode, fetch the actual file content
  useEffect(() => {
    if (state.mode === "edit") {
      setLoading(true);
      api.ccConfig
        .file(state.filePath)
        .then((r) => {
          setContent(r.text);
          setLoading(false);
        })
        .catch((err: unknown) => {
          const msg = err instanceof Error ? err.message : "unknown";
          setError(msg);
          setLoading(false);
        });
    }
  }, [state]);

  // Esc to close
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const handleSave = useCallback(async () => {
    setSaving(true);
    setError(null);
    try {
      if (state.type !== "memory" && state.mode === "create" && !name) {
        setError(t("edit.nameLabel"));
        setSaving(false);
        return;
      }
      // For auto-memory creates, append a .md extension when the user omits it.
      const createName = isAutoMemory && !/\.md$/i.test(name) ? `${name}.md` : name;
      const effectiveName =
        state.mode === "edit" ? state.name : state.type === "memory" ? undefined : createName;
      await onSave({
        type: state.type,
        targetScope: isAutoMemory ? "auto-memory" : targetScope,
        name: effectiveName,
        content,
        project: state.project,
      });
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : "unknown";
      setError(t("edit.writeError", { message: msg }));
    } finally {
      setSaving(false);
    }
  }, [state, targetScope, name, content, isAutoMemory, onSave, t]);

  const titleText = isCreate
    ? isAutoMemory
      ? t("memory.newFileTitle", { project: state.project ?? "" })
      : t("edit.newTitle", { type: state.type })
    : t("edit.editTitle", { name: state.mode === "edit" ? state.name : "" });

  return (
    <div
      className="fixed inset-0 bg-black/60 z-40 flex items-center justify-center p-4"
      onClick={onClose}
    >
      <div
        className="relative w-full max-w-4xl max-h-[90vh] rounded-xl border border-border bg-surface-1 flex flex-col"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
          <Pencil className="w-4 h-4 text-fg-muted" />
          <span className="text-sm font-medium text-fg-primary flex-1 truncate">{titleText}</span>
          <button
            onClick={onClose}
            className="text-fg-secondary hover:text-fg-primary p-1 rounded-md hover:bg-surface-3"
            aria-label={t("common.close")}
          >
            <X className="w-4 h-4" />
          </button>
        </div>

        <div className="overflow-auto p-4 space-y-3">
          {isCreate && state.type !== "memory" && (
            <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
              <div>
                <label className="block text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1">
                  {t("edit.nameLabel")}
                </label>
                <input
                  value={name}
                  onChange={(e) => setName(e.target.value)}
                  placeholder={
                    isAutoMemory ? t("memory.namePlaceholder") : t("edit.namePlaceholder")
                  }
                  pattern={isAutoMemory ? undefined : "[A-Za-z0-9][A-Za-z0-9._-]{0,63}"}
                  className="w-full bg-surface-2 border border-border rounded-md px-3 py-1.5 text-sm font-mono text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50"
                />
                <p className="mt-1 text-[10px] text-fg-muted">
                  {isAutoMemory ? t("memory.nameHelp") : t("edit.nameHelp")}
                </p>
              </div>
              {isAutoMemory ? (
                <div>
                  <label className="block text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1">
                    {t("memory.projectLabel")}
                  </label>
                  <div className="rounded-md border border-border bg-surface-2 px-3 py-1.5 font-mono text-[11px] text-fg-secondary truncate">
                    {state.project}
                  </div>
                  <p className="mt-1 text-[10px] text-fg-muted">{t("memory.projectHelp")}</p>
                </div>
              ) : (
                <div>
                  <label className="block text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1">
                    {t("edit.scopePicker")}
                  </label>
                  <div className="inline-flex rounded-md border border-border bg-surface-2 p-0.5">
                    {(["user", "project"] as const).map((s) => (
                      <button
                        key={s}
                        onClick={() => setTargetScope(s)}
                        className={`px-3 py-1 text-[11px] font-medium rounded ${
                          targetScope === s
                            ? "bg-accent/20 text-accent border border-accent/30"
                            : "text-fg-secondary hover:text-fg-primary"
                        }`}
                      >
                        {t(`scope.${s}`)}
                      </button>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )}

          <div>
            <label className="block text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1">
              {t("edit.contentLabel")}
            </label>
            {loading ? (
              <div className="h-72 rounded-md border border-border bg-surface-2 animate-pulse" />
            ) : (
              <textarea
                value={content}
                onChange={(e) => setContent(e.target.value)}
                spellCheck={false}
                className="w-full h-72 bg-surface-2 border border-border rounded-md px-3 py-2 text-[11px] font-mono text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50 resize-y"
              />
            )}
          </div>

          {error && (
            <div className="rounded-md border border-status-danger/40 bg-status-danger/10 px-3 py-2 text-xs text-status-danger inline-flex items-center gap-2">
              <AlertCircle className="w-3.5 h-3.5" />
              {error}
            </div>
          )}
        </div>

        <div className="border-t border-border px-4 py-3 flex items-center justify-end gap-2">
          <button
            onClick={onClose}
            className="text-[12px] font-medium px-3 py-1.5 rounded-md border border-border bg-surface-2 hover:bg-surface-3 text-fg-secondary"
          >
            {t("edit.cancel")}
          </button>
          <button
            onClick={handleSave}
            disabled={saving || loading}
            className="text-[12px] font-medium px-3 py-1.5 rounded-md border border-accent/40 bg-accent/15 hover:bg-accent/25 text-accent inline-flex items-center gap-1.5 disabled:opacity-60"
          >
            <Save className="w-3.5 h-3.5" />
            {saving ? t("edit.saving") : t("edit.save")}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Confirm-delete modal ──────────────────────────────────────────────

interface ConfirmDeleteModalProps {
  state: NonNullable<ConfirmDeleteState>;
  onCancel: () => void;
  onConfirm: () => Promise<void>;
}

function ConfirmDeleteModal({ state, onCancel, onConfirm }: ConfirmDeleteModalProps) {
  const { t } = useTranslation("ccConfig");
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onCancel();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onCancel]);

  const handleConfirm = useCallback(async () => {
    setBusy(true);
    try {
      await onConfirm();
    } finally {
      setBusy(false);
    }
  }, [onConfirm]);

  return (
    <div
      className="fixed inset-0 bg-black/60 z-40 flex items-center justify-center p-4"
      onClick={onCancel}
    >
      <div
        className="relative w-full max-w-lg rounded-xl border border-status-danger/40 bg-surface-1"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="border-b border-border px-4 py-2.5 flex items-center gap-2">
          <ShieldAlert className="w-4 h-4 text-status-danger" />
          <span className="text-sm font-medium text-fg-primary flex-1">
            {t("edit.confirmDelete")}
          </span>
        </div>
        <div className="p-4 space-y-3">
          <p className="text-xs text-fg-secondary leading-relaxed">{t("edit.confirmDeleteBody")}</p>
          <div className="rounded-md border border-border bg-surface-2 px-3 py-2 font-mono text-[11px] text-fg-secondary break-all">
            {t("edit.confirmDeletePath", { path: state.path })}
          </div>
        </div>
        <div className="border-t border-border px-4 py-3 flex items-center justify-end gap-2">
          <button
            onClick={onCancel}
            disabled={busy}
            className="text-[12px] font-medium px-3 py-1.5 rounded-md border border-border bg-surface-2 hover:bg-surface-3 text-fg-secondary disabled:opacity-60"
          >
            {t("edit.cancel")}
          </button>
          <button
            onClick={handleConfirm}
            disabled={busy}
            className="text-[12px] font-medium px-3 py-1.5 rounded-md border border-status-danger/50 bg-status-danger/15 hover:bg-status-danger/25 text-status-danger inline-flex items-center gap-1.5 disabled:opacity-60"
          >
            <Trash2 className="w-3.5 h-3.5" />
            {busy ? t("edit.deleting") : t("edit.confirmDeleteAction")}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Toast (5s auto-dismiss) ───────────────────────────────────────────

function ToastNotice({ toast, onDismiss }: { toast: NonNullable<Toast>; onDismiss: () => void }) {
  const isErr = toast.kind === "error";
  return (
    <div className="fixed bottom-6 right-6 z-50 max-w-md">
      <div
        className={`rounded-lg border px-3 py-2 shadow-lg flex items-start gap-2 ${
          isErr
            ? "border-status-danger/50 bg-status-danger/15 text-status-danger"
            : "border-status-success/40 bg-status-success/10 text-status-success"
        }`}
      >
        {isErr ? (
          <AlertCircle className="w-4 h-4 flex-shrink-0 mt-0.5" />
        ) : (
          <Check className="w-4 h-4 flex-shrink-0 mt-0.5" />
        )}
        <span className="text-xs leading-relaxed flex-1 break-all">{toast.message}</span>
        <button
          onClick={onDismiss}
          className="text-current/70 hover:text-current p-0.5"
          aria-label="dismiss"
        >
          <X className="w-3 h-3" />
        </button>
      </div>
    </div>
  );
}

// ── Read-only explainer banner ─────────────────────────────────────────

interface ExplainerBannerProps {
  title: string;
  body: string;
  howTo: string;
  commands: { cmd: string; note: string }[];
}

function ExplainerBanner({ title, body, howTo, commands }: ExplainerBannerProps) {
  return (
    <div className="rounded-lg border border-status-warning/30 bg-status-warning/[0.04] px-4 py-3">
      <div className="flex items-start gap-2">
        <Lock className="w-4 h-4 text-status-warning flex-shrink-0 mt-0.5" />
        <div className="min-w-0 flex-1 space-y-2">
          <div className="text-sm font-medium text-status-warning">{title}</div>
          <p className="text-xs text-fg-secondary leading-relaxed">{body}</p>
          {commands.length > 0 && (
            <div className="pt-1">
              <div className="text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5">
                {howTo}
              </div>
              <div className="space-y-1.5">
                {commands.map((c, i) => (
                  <CommandSnippet key={i} command={c.cmd} label={c.note} />
                ))}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ── Inline copyable command ────────────────────────────────────────────

function CommandSnippet({ command, label }: { command: string; label?: string }) {
  const { t } = useTranslation("ccConfig");
  const [copied, setCopied] = useState(false);
  return (
    <div className="rounded-md border border-border bg-surface-2 px-2.5 py-1.5 flex items-center gap-2">
      <Terminal className="w-3 h-3 text-fg-muted flex-shrink-0" />
      <code className="font-mono text-[11px] text-fg-secondary truncate flex-1">{command}</code>
      {label && (
        <span className="text-[10px] text-fg-muted hidden md:inline truncate">{label}</span>
      )}
      <button
        onClick={async () => {
          try {
            await navigator.clipboard.writeText(command);
            setCopied(true);
            setTimeout(() => setCopied(false), 1500);
          } catch {
            /* clipboard unavailable */
          }
        }}
        className="text-[10px] font-medium px-1.5 py-1 rounded border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1 flex-shrink-0"
        title={t("snippet.copy")}
      >
        {copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
        <span>{copied ? t("snippet.copied") : t("snippet.copy")}</span>
      </button>
    </div>
  );
}

// ── Backups modal ──────────────────────────────────────────────────────

function BackupsModal({ onClose }: { onClose: () => void }) {
  const { t } = useTranslation("ccConfig");
  const [items, setItems] = useState<CcBackup[] | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    api.ccConfig
      .backups()
      .then((r) => setItems(r.items))
      .catch((err: unknown) => setError(err instanceof Error ? err.message : "unknown"));
  }, []);

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  return (
    <div
      className="fixed inset-0 bg-black/60 z-40 flex items-center justify-center p-4"
      onClick={onClose}
    >
      <div
        className="relative w-full max-w-3xl max-h-[85vh] rounded-xl border border-border bg-surface-1 flex flex-col"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
          <History className="w-4 h-4 text-fg-muted" />
          <span className="text-sm font-medium text-fg-primary flex-1">{t("backups.title")}</span>
          <button
            onClick={onClose}
            className="text-fg-secondary hover:text-fg-primary p-1 rounded-md hover:bg-surface-3"
            aria-label={t("common.close")}
          >
            <X className="w-4 h-4" />
          </button>
        </div>
        <div className="px-4 py-3 border-b border-border">
          <p className="text-[11px] text-fg-muted leading-relaxed">{t("backups.subtitle")}</p>
        </div>
        <div className="overflow-auto p-4 space-y-2">
          {error && (
            <div className="text-sm text-status-danger inline-flex items-center gap-2">
              <AlertCircle className="w-4 h-4" />
              {error}
            </div>
          )}
          {items === null && !error && <SkeletonRows n={4} />}
          {items !== null && items.length === 0 && (
            <div className="rounded-lg border border-dashed border-border bg-surface-2 px-4 py-8 text-center text-sm text-fg-muted">
              {t("backups.empty")}
            </div>
          )}
          {items?.map((b) => (
            <BackupRow key={b.backupPath} backup={b} />
          ))}
        </div>
      </div>
    </div>
  );
}

function BackupRow({ backup }: { backup: CcBackup }) {
  const { t } = useTranslation("ccConfig");
  // Heuristic restore: rename the backup back to the active path. We don't
  // shell out from the dashboard for this (too risky to silently overwrite
  // a current active version) - show the user a copyable mv command instead.
  const restoreCmd = `mv ${shellEscape(backup.backupPath)} ${shellEscape(deriveActivePath(backup))}`;
  return (
    <div className="rounded-lg border border-border bg-surface-2 px-3 py-2.5">
      <div className="flex items-center gap-2 flex-wrap">
        <ScopeBadge scope={backup.scope} />
        <span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface-3 text-fg-secondary border border-border">
          {backup.type}
        </span>
        <span className="font-mono text-xs text-fg-primary truncate flex-1 min-w-0">
          {backup.name}
        </span>
        <span className="text-[10px] text-fg-muted">{new Date(backup.mtime).toLocaleString()}</span>
        {backup.size != null && (
          <span className="text-[10px] text-fg-muted">{formatBytes(backup.size)}</span>
        )}
      </div>
      <div className="mt-1.5 font-mono text-[10px] text-fg-muted truncate">{backup.backupPath}</div>
      <div className="mt-2">
        <div className="text-[10px] text-fg-muted mb-1">{t("backups.restoreHint")}</div>
        <CommandSnippet command={restoreCmd} />
      </div>
    </div>
  );
}

// Best-effort: derive the active path the backup would restore to. We strip
// the trailing `.<ISO>.bak` segment from the basename and put the result back
// under the right active subdir. If the format doesn't match, fall back to
// "(unknown)" - the user can still copy the backup path itself.
function deriveActivePath(b: CcBackup): string {
  // Strip ".<ISO>.bak" suffix from the basename.
  const m = b.name.match(/^(.+?)\.[^.]+\.bak$/);
  const baseName = m ? m[1] : b.name;
  const backupDir = b.backupPath.replace(/\/[^/]+$/, ""); // dirname
  // Backups live at <root>/cc-config-backups/<type>/<name>.<ts>.bak
  // Active lives at <root>/<type>/<baseName> (or .../skills/<baseName>/SKILL.md handled at use-time)
  const rootMatch = backupDir.match(/^(.*)\/cc-config-backups\/([^/]+)$/);
  if (rootMatch) {
    const root = rootMatch[1];
    const type = rootMatch[2];
    return `${root}/${type}/${baseName}`;
  }
  // memory backups live at <projectRoot>/.cc-config-backups/memory/<name>.<ts>.bak
  const memMatch = backupDir.match(/^(.*)\/\.cc-config-backups\/memory$/);
  if (memMatch) return `${memMatch[1]}/${baseName}`;
  // auto-memory backups live at <memoryDir>/.cc-config-backups/auto-memory/<name>.<ts>.bak
  const autoMatch = backupDir.match(/^(.*)\/\.cc-config-backups\/auto-memory$/);
  if (autoMatch) return `${autoMatch[1]}/${baseName}`;
  return "<active path>";
}

// Quote for POSIX shells: wrap in single quotes, escape any embedded single quotes.
function shellEscape(s: string): string {
  if (/^[A-Za-z0-9_/.@:=+,-]+$/.test(s)) return s;
  return `'${s.replace(/'/g, `'\\''`)}'`;
}
