import {
  openAIService,
  OpenAIMessage,
  VisionMessage,
  VisionMessageContentPart,
  ToolCall,
  ToolDefinition,
} from "./openai";
import { config } from "../../package.json";

const HTML_NS = "http://www.w3.org/1999/xhtml";
import {
  getChatStateManager,
  resetChatStateManager,
  ChatStateManager,
} from "./chat/stateManager";
import { wireCodePreviewButtons } from "./chat/ui/messageRenderer";
import {
  SelectedItem,
  SelectedNote,
  SelectedTable,
  ChatMessage,
  selectionConfigs,
  AIModelConfig,
  ConversationMetadata,
} from "./chat/types";
import {
  getModelConfigs,
  getActiveModelConfig,
  setActiveModelId,
  hasModelConfigs,
} from "./chat/modelConfig";
import {
  parseMarkdown,
  processStreamingContent,
  stripThinkTags,
} from "./chat/markdown";
import { getMessageStore } from "./chat/messageStore";
import {
  createImageContentParts,
  countImageAttachments,
  stripBase64Data,
} from "./chat/imageUtils";
import { getTableStore } from "./chat/tableStore";
import { advancedSearch } from "./searchUtils";
import {
  TableConfig,
  TableColumn,
  TableRow,
  TableData,
  AssistantTab,
  ColumnPreset,
  defaultColumns,
  SearchState,
  defaultSearchState,
  SearchAnalysisColumn,
  SearchColumnConfig,
  defaultSearchColumnConfig,
  PersistedPdfDiscovery,
} from "./chat/tableTypes";
import { OcrService } from "./ocr";
import {
  semanticScholarService,
  unpaywallService,
  SemanticScholarPaper,
  SemanticScholarAuthorDetails,
  SearchResult,
  FIELDS_OF_STUDY,
  PUBLICATION_TYPES,
} from "./semanticScholar";
import { firecrawlService, PdfDiscoveryResult } from "./firecrawl";
import {
  getActiveProvider,
  isActiveProviderConfigured,
} from "./webSearchProvider";
import { getTheme } from "../utils/theme";
import { getPref, setPref } from "../utils/prefs";
import {
  runConcurrentTasks,
  formatTaskStats,
  formatTaskSummary,
  TaskStats,
} from "../utils/concurrentRunner";
// Prompt Library & Placeholder System imports
import {
  PromptTemplate,
  loadPrompts,
  getPromptsByCategory,
  addPrompt,
} from "./chat/promptLibrary";
import { showPromptPicker } from "./chat/ui/promptPicker";
import {
  initPlaceholderAutocomplete,
  createPlaceholderMenuButton,
  hideDropdown,
  triggerNextPlaceholder,
  isDropdownOpen,
} from "./chat/ui/placeholderDropdown";
import { showChatSettings } from "./chat/ui/chatSettings";
import {
  isTtsConfigured,
  autoPlayTtsResponse,
  playTts,
  createRAGProgressUI,
} from "./chat/ui/messageRenderer";
import { ChatContextManager } from "./chat/context/contextManager";
import { convertDocxToMarkdown } from "./docxConverter";
import { createContextChipsArea } from "./chat/context/contextUI";
import { ContextItem, ContextItemType } from "./chat/context/contextTypes";
import {
  showDriveModal,
  loadDriveContextForChat,
  inheritAndLoadDriveContext,
  clearDriveContextForChat,
  refreshDriveContextForChat,
} from "./drive";
import {
  handleAgenticChat,
  isAgenticModeEnabled,
  AgentUIObserver,
  createToolExecutionUI,
  createToolProcessUI,
  createQuestionPanel,
} from "./chat/agenticChat";
import { ToolResult } from "./chat/tools/toolTypes";
import { getFilteredAgentTools } from "./chat/tools/toolDefinitions";
import { getAgentConfigFromPrefs } from "./chat/tools";
import { DetachedWindowManager } from "./ui/windowManager";
import {
  retrieveContext,
  shouldActivateRAG,
  getRAGConfig,
  computeTokenBudget,
} from "./chat/rag/retrievalEngine";
import { getEmbeddingService } from "./chat/rag/embeddingService";
import type { RAGProgressEvent } from "./chat/rag/types";
import {
  createWorkspaceSidebar,
  refreshWorkspaceSidebar,
  WorkspaceEditorManager,
  createIconButton,
} from "./chat/workspace";
import { getWorkspaceStore } from "./chat/workspace/store";
import { WORKSPACE_SYSTEM_PROMPT } from "./chat/workspace/tools";
import { createCloudTabContent } from "./cloud/cloudTab";

// Debounce timer for autocomplete
const autocompleteTimeout: ReturnType<typeof setTimeout> | null = null;

// Stored messages for conversation continuity (loaded from persistence)
let conversationMessages: ChatMessage[] = [];

// Track the current item ID to detect navigation
let currentItemId: number | null = null;
// Track last interacted row for range selection
let lastInteractedRowId: number | null = null;

// Store container reference for re-rendering
let currentContainer: HTMLElement | null = null;
let currentItem: Zotero.Item | null = null;

// Active tab state
let activeTab: AssistantTab = "chat";
// Table state cache
let currentTableConfig: TableConfig | null = null;
let currentTableData: TableData | null = null;
let isTableGenerating: boolean = false;

// Search state
let currentSearchState: SearchState = { ...defaultSearchState };
let currentSearchResults: SemanticScholarPaper[] = [];
let currentSearchToken: string | null = null; // For pagination

/**
 * Persistable state for an active agent operation
 */
interface AgentSession {
  text: string;
  fullResponse: string;
  toolResults: {
    toolCall: ToolCall;
    result?: ToolResult;
    uiElement?: HTMLElement;
  }[];
  isThinking: boolean;
  iterationCount: number;
  messagesArea: HTMLElement | null;
  contentDiv: HTMLElement | null;
  toolContainer: HTMLElement | null; // Keeps track of the tool list container for direct access
  isToolDetailsOpen: boolean; // Persist open/closed state of tool details
  currentTextBlock: HTMLElement | null; // Text block for current turn (interleaved with tool cards)
  turnTextSoFar: string; // Accumulated text for the current turn block
  previousTurnsTextLength: number; // Total text length rendered in previous turns
  toolProcessState?: {
    container: HTMLElement;
    setThinking: () => void;
    setExecutingTool: (toolName: string) => void;
    setCompleted: (count: number, toolCount?: number) => void;
    updateProgress: (count: number, toolCount?: number) => void;
    setFailed: (error: string) => void;
    updateStats: (stats: {
      turns: number;
      totalTools: number;
      estimatedTokens: number;
      completedTodos: number;
      totalTodos: number;
      lastToolName: string;
      lastToolSummary: string;
      isThinking: boolean;
    }) => void;
  };
  completedTodos: number;
  totalTodos: number;
  lastToolName: string;
  lastToolSummary: string;
  cumulativeTokens: number;
}

let activeAgentSession: AgentSession | null = null;

/**
 * Build tool usage instruction sections for the system prompt,
 * conditionally excluding sections whose tools are denied.
 */
function buildToolPromptSections(): string {
  const availableTools = new Set(
    getFilteredAgentTools().map((t) => t.function.name),
  );

  const has = (name: string) => availableTools.has(name);
  const sections: string[] = [];

  if (has("table")) {
    sections.push(`Table Management (use the 'table' tool with the 'action' parameter):
- table({ action: "list" }) — list all existing tables
- table({ action: "create", name: "...", paper_ids: [...] }) — create a new analysis table with optional papers
- table({ action: "add_papers", table_id: "...", paper_ids: [...] }) — add papers to an existing table
- table({ action: "add_column", table_id: "...", column_name: "...", ai_prompt: "..." }) — add an AI-powered analysis column (e.g., "Methodology", "Result Summary")
- table({ action: "generate", table_id: "...", column_id?: "..." }) — trigger AI generation of column data. Only call AFTER adding all desired columns.
- table({ action: "read", table_id: "...", include_data: true }) — read table contents with generated data`);
  }

  if (has("collection")) {
    sections.push(`Collection Management (use the 'collection' tool with the 'action' parameter):
- collection({ action: "find", name: "..." }) — find collections by name
- collection({ action: "create", name: "...", parent_id?: ... }) — create a new collection/subfolder
- collection({ action: "list", collection_id: ... }) — browse contents of a collection
- collection({ action: "add_item", collection_id: ..., item_ids: [...] }) — add items to a collection
- collection({ action: "remove_item", collection_id: ..., item_ids: [...] }) — remove items from a collection`);
  }

  if (has("note")) {
    sections.push(`Notes (use the 'note' tool with the 'action' parameter):
- note({ action: "create", title: "...", content: "...", parent_item_id?: ..., collection_id?: ... }) — create a new note
- note({ action: "edit", note_id: ..., operations: [...] }) — edit an existing note`);
  }

  const libraryTools = [
    has("search_library"),
    has("search_external"),
    has("import_paper"),
    has("get_item_metadata"),
    has("read_item_content"),
    has("generate_item_tags"),
  ];
  if (libraryTools.some(Boolean)) {
    const lines: string[] = ["Research & Paper Discovery:"];
    if (has("search_library"))
      lines.push(
        "1. Use 'search_library' to find papers already in the user's Zotero library.",
      );
    if (has("search_external"))
      lines.push(
        "2. Use 'search_external' to find new papers on Semantic Scholar.",
      );
    if (has("import_paper"))
      lines.push(
        "3. Use 'import_paper' to bring a paper into Zotero. Set 'trigger_ocr: true' for immediate PDF text extraction.",
      );
    if (has("get_item_metadata"))
      lines.push(
        "4. Use 'get_item_metadata' to get detailed bibliographic info for a specific item.",
      );
    if (has("read_item_content"))
      lines.push(
        "5. Use 'read_item_content' with 'trigger_ocr: true' to extract text from PDFs for quantitative analysis.",
      );
    if (has("generate_item_tags"))
      lines.push(
        "6. Use 'generate_item_tags' to auto-tag items based on their content.",
      );
    sections.push(lines.join("\n"));
  }

  return sections.length > 0 ? "\n" + sections.join("\n\n") : "";
}

/**
 * Build a tree-formatted listing of workspace files for the system prompt.
 * This lets the agent know what files already exist without calling workspace_glob.
 */
async function buildWorkspaceTreePrompt(): Promise<string> {
  try {
    const store = getWorkspaceStore();
    let entries = await store.listFileTree();
    if (!entries || entries.length === 0) {
      const flat = await store.listFiles();
      entries = flat
        .filter((f) => !f.path.includes("\0"))
        .map((f) => ({
          path: f.path,
          name: f.name,
          isDirectory: f.isDirectory,
          extension: f.extension,
          status: f.status,
        }));
    }

    if (!entries || entries.length === 0) return "";

    const SKIP_DIRS = new Set([".git", ".agent", ".agents", ".conversations"]);
    const filtered = entries.filter((e) => !SKIP_DIRS.has(e.name));

    if (filtered.length === 0) return "";

    function formatTree(
      items: typeof entries,
      prefix: string,
      isLast: boolean[],
    ): string {
      let result = "";
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        const last = i === items.length - 1;
        const connector = last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
        const name = item.isDirectory ? `${item.name}/` : item.name;

        result += prefix + connector + name + "\n";

        if (item.isDirectory && item.children && item.children.length > 0) {
          const childPrefix = prefix + (last ? "    " : "\u2502   ");
          const childLast: boolean[] = [...isLast, last];
          result += formatTree(item.children, childPrefix, childLast);
        }
      }
      return result;
    }

    const tree = ".\n" + formatTree(filtered, "", []);
    return (
      "\n## Workspace Files\n" +
      "The following files already exist in your workspace:\n" +
      "```\n" +
      tree +
      "```\n" +
      "Read existing files with workspace_read_file before modifying them.\n"
    );
  } catch (e) {
    Zotero.debug(`[seerai] buildWorkspaceTreePrompt failed: ${e}`);
    return "";
  }
}
let currentDraftText = "";
const currentPastedImages: { id: string; image: string; mimeType: string }[] =
  [];
let totalSearchResults: number = 0; // Total count from API
let isSearching = false;
let renderGeneration = 0;
let renderLock: Promise<void> = Promise.resolve();

// Cache for Unpaywall PDF URLs (paperId -> pdfUrl)
const unpaywallPdfCache = new Map<string, string>();

// Cache for Firecrawl PDF discovery results (paperId -> result)
const firecrawlPdfCache = new Map<string, PdfDiscoveryResult>();

// Cache for Zotero Find Full Text results (cacheKey -> pdfPath or null)
const zoteroFindPdfCache: Map<string, string | null> = new Map();

// ==================== PDF Search State Tracker ====================
// Tracks active and completed PDF searches for persistence across tab switches
type PdfSearchStatus =
  | "queued"
  | "searching"
  | "retrying"
  | "found"
  | "notfound"
  | "skipped"
  | "error";
interface PdfSearchState {
  paperId: number;
  status: PdfSearchStatus;
  message?: string;
  startedAt: number;
}
// Active PDF search state (persists across tab switches)
const activePdfSearches: Map<number, PdfSearchState> = new Map();
// Overall batch search state
let pdfSearchBatchActive = false;
let pdfSearchBatchStats = {
  total: 0,
  completed: 0,
  succeeded: 0,
  failed: 0,
  skipped: 0,
};

// Search analysis column configuration (persisted)
let searchColumnConfig: SearchColumnConfig = { ...defaultSearchColumnConfig };
let searchColumnConfigFilePath: string | null = null;

// Chat History State
let conversationHistory: ConversationMetadata[] = [];
let isHistorySidebarVisible = true;
let systemPromptExpanded = false;
const selectedChatIds: Set<string> = new Set();
let lastClickedChatId: string | null = null;

/**
 * Use Zotero's Find Full Text resolver to fetch PDF for a paper.
 * Creates a temporary item, calls addAvailablePDF, extracts PDF path, then deletes the temp item.
 * Returns PDF URL/path if found, null otherwise.
 */
async function findPdfViaZotero(
  doi?: string,
  arxivId?: string,
  pmid?: string,
  title?: string,
  url?: string,
): Promise<string | null> {
  const cacheKey = doi || arxivId || pmid || title?.slice(0, 50) || "";
  if (!cacheKey) return null;

  if (zoteroFindPdfCache.has(cacheKey)) {
    return zoteroFindPdfCache.get(cacheKey) || null;
  }

  let tempItem: Zotero.Item | null = null;

  try {
    // Create temporary item with minimal metadata for resolver lookup
    tempItem = new Zotero.Item("journalArticle");
    tempItem.libraryID = Zotero.Libraries.userLibraryID;
    if (title) tempItem.setField("title", title);
    if (doi) tempItem.setField("DOI", doi);
    if (url) tempItem.setField("url", url);
    await tempItem.saveTx();

    Zotero.debug(
      `[seerai] Zotero Find Full Text: Created temp item ${tempItem.id}`,
    );

    // Call Zotero's Find Full Text resolver
    const attachment = await (Zotero.Attachments as any).addAvailablePDF(
      tempItem,
    );

    let pdfPath: string | null = null;
    if (attachment && attachment.id) {
      pdfPath = await attachment.getFilePathAsync();
      Zotero.debug(`[seerai] Zotero Find Full Text: Found PDF at ${pdfPath}`);
    }

    zoteroFindPdfCache.set(cacheKey, pdfPath);
    return pdfPath;
  } catch (error) {
    Zotero.debug(`[seerai] Zotero Find Full Text error: ${error}`);
    zoteroFindPdfCache.set(cacheKey, null);
    return null;
  } finally {
    // Always delete temporary item
    if (tempItem) {
      try {
        await tempItem.eraseTx();
        Zotero.debug(`[seerai] Zotero Find Full Text: Deleted temp item`);
      } catch (e) {
        Zotero.debug(`[seerai] Error deleting temp item: ${e}`);
      }
    }
  }
}

/**
 * Find PDF via arXiv - direct URL pattern for arXiv papers
 * ArXiv PDFs are always at https://arxiv.org/pdf/{id}.pdf
 */
async function findPdfViaArxiv(arxivId?: string): Promise<string | null> {
  if (!arxivId) return null;

  // Normalize arXiv ID (remove version suffix if present for URL, keep for specificity)
  const normalizedId = arxivId.replace(/^arxiv:/i, "");
  const pdfUrl = `https://arxiv.org/pdf/${normalizedId}.pdf`;

  try {
    Zotero.debug(`[seerai] arXiv: Checking ${pdfUrl}`);
    const response = await Zotero.HTTP.request("HEAD", pdfUrl, {
      timeout: 5000,
    });
    if (response.status === 200) {
      Zotero.debug(`[seerai] arXiv: Found PDF at ${pdfUrl}`);
      return pdfUrl;
    }
  } catch (error) {
    Zotero.debug(`[seerai] arXiv: Request failed: ${error}`);
  }
  return null;
}

/**
 * Find PDF via PubMed Central using PMID
 * First converts PMID to PMCID, then constructs PDF URL
 */
async function findPdfViaPmc(pmid?: string): Promise<string | null> {
  if (!pmid) return null;

  try {
    // Use NCBI ELink to convert PMID to PMCID
    const idUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?dbfrom=pubmed&db=pmc&id=${pmid}&retmode=json`;
    Zotero.debug(`[seerai] PMC: Looking up PMCID for PMID ${pmid}`);

    const resp = await Zotero.HTTP.request("GET", idUrl, {
      responseType: "json",
      timeout: 10000,
    });

    const data = resp.response as any;
    const linksets = data?.linksets?.[0]?.linksetdbs;
    const pmcLink = linksets?.find((l: any) => l.dbto === "pmc");

    if (pmcLink?.links?.[0]) {
      const pmcid = pmcLink.links[0];
      const pdfUrl = `https://www.ncbi.nlm.nih.gov/pmc/articles/PMC${pmcid}/pdf/`;
      Zotero.debug(`[seerai] PMC: Found PMCID ${pmcid}, PDF at ${pdfUrl}`);
      return pdfUrl;
    }
    Zotero.debug(`[seerai] PMC: No PMCID found for PMID ${pmid}`);
  } catch (error) {
    Zotero.debug(`[seerai] PMC: Request failed: ${error}`);
  }
  return null;
}

/**
 * Find PDF via bioRxiv/medRxiv for preprints
 * Only works for DOIs starting with 10.1101/
 */
async function findPdfViaBiorxiv(doi?: string): Promise<string | null> {
  if (!doi || !doi.startsWith("10.1101/")) return null;

  // Try bioRxiv first
  const biorxivUrl = `https://www.biorxiv.org/content/${doi}v1.full.pdf`;
  try {
    Zotero.debug(`[seerai] bioRxiv: Checking ${biorxivUrl}`);
    const response = await Zotero.HTTP.request("HEAD", biorxivUrl, {
      timeout: 5000,
    });
    if (response.status === 200) {
      Zotero.debug(`[seerai] bioRxiv: Found PDF`);
      return biorxivUrl;
    }
  } catch {
    /* try medRxiv */
  }

  // Try medRxiv
  const medrxivUrl = `https://www.medrxiv.org/content/${doi}v1.full.pdf`;
  try {
    Zotero.debug(`[seerai] medRxiv: Checking ${medrxivUrl}`);
    const response = await Zotero.HTTP.request("HEAD", medrxivUrl, {
      timeout: 5000,
    });
    if (response.status === 200) {
      Zotero.debug(`[seerai] medRxiv: Found PDF`);
      return medrxivUrl;
    }
  } catch (error) {
    Zotero.debug(`[seerai] bioRxiv/medRxiv: Request failed: ${error}`);
  }
  return null;
}

/**
 * Find PDF via Europe PMC - alternative OA source
 * Searches by DOI or PMID and returns PDF link if available
 */
async function findPdfViaEuropePmc(
  doi?: string,
  pmid?: string,
): Promise<string | null> {
  const query = doi ? `DOI:${doi}` : pmid ? `EXT_ID:${pmid}` : null;
  if (!query) return null;

  try {
    const url = `https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=${encodeURIComponent(query)}&format=json`;
    Zotero.debug(`[seerai] EuropePMC: Searching with query ${query}`);

    const resp = await Zotero.HTTP.request("GET", url, {
      responseType: "json",
      timeout: 10000,
    });

    const data = resp.response as any;
    const result = data?.resultList?.result?.[0];

    if (result?.pmcid) {
      const pdfUrl = `https://europepmc.org/backend/ptpmcrender.fcgi?accid=${result.pmcid}&blobtype=pdf`;
      Zotero.debug(`[seerai] EuropePMC: Found PMCID ${result.pmcid}`);
      return pdfUrl;
    }
    Zotero.debug(`[seerai] EuropePMC: No PMCID found`);
  } catch (error) {
    Zotero.debug(`[seerai] EuropePMC: Request failed: ${error}`);
  }
  return null;
}

/**
 * Download PDF from URL and attach to a Zotero item
 * Includes timeout to prevent hanging on slow/stuck downloads
 */
async function downloadAndAttachPdf(
  item: Zotero.Item,
  pdfUrl: string,
  timeoutMs: number = 30000, // 30 second default timeout per download
): Promise<boolean> {
  try {
    Zotero.debug(`[seerai] Downloading PDF from: ${pdfUrl}`);

    // Create a timeout promise
    const timeoutPromise = new Promise<null>((_, reject) => {
      setTimeout(
        () => reject(new Error(`Download timed out after ${timeoutMs}ms`)),
        timeoutMs,
      );
    });

    // Race between download and timeout
    const downloadPromise = Zotero.Attachments.importFromURL({
      url: pdfUrl,
      parentItemID: item.id,
      title: `${item.getField("title")}.pdf`,
      contentType: "application/pdf",
    });

    const attachment = await Promise.race([downloadPromise, timeoutPromise]);

    if (attachment) {
      Zotero.debug(
        `[seerai] PDF attached successfully: ${(attachment as any).id}`,
      );
      return true;
    } else {
      Zotero.debug(
        `[seerai] PDF attachment failed: check Zotero logs for network/permission issues`,
      );
      return false;
    }
  } catch (error) {
    Zotero.debug(`[seerai] PDF download/attach failed for ${pdfUrl}: ${error}`);
    return false;
  }
}

/**
 * Extract PMID from Zotero item's Extra field
 * Looks for patterns like "PMID: 12345678" or "pmid: 12345678"
 */
function extractPmidFromItem(item: Zotero.Item): string | undefined {
  const extra = (item.getField("extra") as string) || "";
  const match = extra.match(/pmid:\s*(\d+)/i);
  return match ? match[1] : undefined;
}

/**
 * Extract ArXiv ID from Zotero item's Extra field
 * Looks for patterns like "arXiv: 2301.12345" or "arxiv:2301.12345v1"
 */
function extractArxivFromItem(item: Zotero.Item): string | undefined {
  const extra = (item.getField("extra") as string) || "";
  const match = extra.match(/arxiv:\s*([\d.]+(?:v\d+)?)/i);
  return match ? match[1] : undefined;
}

/**
 * Run 6-step PDF discovery for a Zotero item and attach if found
 * Returns true if PDF was successfully found and attached
 */
export async function findAndAttachPdfForItem(
  item: Zotero.Item,
  onProgress?: (step: string) => void,
): Promise<boolean> {
  let doi = (item.getField("DOI") as string) || undefined;
  let pmid = extractPmidFromItem(item);
  let arxivId = extractArxivFromItem(item);
  const title = (item.getField("title") as string) || undefined;
  const url = (item.getField("url") as string) || undefined;

  Zotero.debug(
    `[seerai] findAndAttachPdfForItem: DOI=${doi}, PMID=${pmid}, ArXiv=${arxivId}`,
  );

  // Step 1: Semantic Scholar Open Access (OA) Check
  const ssId = doi
    ? `DOI:${doi}`
    : pmid
      ? `PMID:${pmid}`
      : arxivId
        ? `ARXIV:${arxivId}`
        : null;
  if (ssId) {
    onProgress?.("📖 Checking SS OA...");
    try {
      const paper = await semanticScholarService.getPaper(ssId);
      if (paper?.openAccessPdf?.url) {
        onProgress?.("📥 Attaching SS PDF...");
        if (await downloadAndAttachPdf(item, paper.openAccessPdf.url)) {
          return true;
        }
      }
    } catch (e) {
      Zotero.debug(`[seerai] SS OA lookup failed for ${ssId}: ${e}`);
    }
  }

  // Step 1b: SS Title Fallback (if title exists and no ID-based success)
  if (title) {
    onProgress?.("🔍 SS Title search...");
    try {
      const results = await semanticScholarService.searchPapers({
        query: title,
        limit: 5,
      });
      if (results && results.data.length > 0) {
        const lowerTitle = title.toLowerCase().trim();
        for (const paper of results.data) {
          if (
            paper.title.toLowerCase().trim() === lowerTitle ||
            paper.title.toLowerCase().includes(lowerTitle) ||
            lowerTitle.includes(paper.title.toLowerCase())
          ) {
            // Extract identifiers from SS result to enable subsequent discovery steps
            const discoveredDoi = paper.externalIds?.DOI;
            const discoveredPmid = paper.externalIds?.PMID;
            const discoveredArxivId = paper.externalIds?.ArXiv;

            let metadataUpdated = false;
            if (discoveredDoi && !doi) {
              doi = discoveredDoi;
              item.setField("DOI", discoveredDoi);
              metadataUpdated = true;
            }
            if (discoveredPmid && !pmid) {
              pmid = discoveredPmid;
              const currentExtra = (item.getField("extra") as string) || "";
              if (!currentExtra.includes("PMID:")) {
                item.setField(
                  "extra",
                  currentExtra +
                    (currentExtra ? "\n" : "") +
                    `PMID: ${discoveredPmid}`,
                );
                metadataUpdated = true;
              }
            }
            if (discoveredArxivId && !arxivId) {
              arxivId = discoveredArxivId;
              const currentExtra = (item.getField("extra") as string) || "";
              if (!currentExtra.includes("arXiv:")) {
                item.setField(
                  "extra",
                  currentExtra +
                    (currentExtra ? "\n" : "") +
                    `arXiv: ${discoveredArxivId}`,
                );
                metadataUpdated = true;
              }
            }

            if (metadataUpdated) {
              await item.saveTx();
              Zotero.debug(
                `[seerai] Updated item metadata from SS title match: DOI=${doi}, PMID=${pmid}, ArXiv=${arxivId}`,
              );
            }

            if (paper.openAccessPdf?.url) {
              onProgress?.("📥 Attaching SS PDF (Title)...");
              if (await downloadAndAttachPdf(item, paper.openAccessPdf.url)) {
                return true;
              }
            }

            // Found a match, so we stop searching results even if PDF attach failed.
            // The updated IDs will be used in subsequent steps (arXiv, Unpaywall, etc.)
            break;
          }
        }
      }
    } catch (e) {
      Zotero.debug(`[seerai] SS Title search failed: ${e}`);
    }
  }

  // Step 2: Zotero resolver
  onProgress?.("📚 Zotero...");
  const zoteroResult = await findPdfViaZotero(doi, arxivId, pmid, title, url);
  if (zoteroResult) {
    // Zotero already attached the PDF via temp item, need to re-run with real item
    try {
      const attachment = await (Zotero.Attachments as any).addAvailablePDF(
        item,
      );
      if (attachment) return true;
    } catch {
      /* continue to other methods */
    }
  }

  // Step 3: arXiv
  if (arxivId) {
    onProgress?.("📄 arXiv...");
    const arxivResult = await findPdfViaArxiv(arxivId);
    if (arxivResult && (await downloadAndAttachPdf(item, arxivResult))) {
      return true;
    }
  }

  // Step 4: PubMed Central
  if (pmid) {
    onProgress?.("🏥 PMC...");
    const pmcResult = await findPdfViaPmc(pmid);
    if (pmcResult && (await downloadAndAttachPdf(item, pmcResult))) {
      return true;
    }
  }

  // Step 5: bioRxiv/medRxiv
  if (doi?.startsWith("10.1101/")) {
    onProgress?.("🧬 bioRxiv...");
    const biorxivResult = await findPdfViaBiorxiv(doi);
    if (biorxivResult && (await downloadAndAttachPdf(item, biorxivResult))) {
      return true;
    }
  }

  // Step 6: Unpaywall
  if (doi) {
    onProgress?.("🔍 Unpaywall...");
    const unpaywallResult = await unpaywallService.getPdfUrl(doi);
    if (
      unpaywallResult &&
      (await downloadAndAttachPdf(item, unpaywallResult))
    ) {
      return true;
    }
  }

  // Step 7: Europe PMC
  onProgress?.("🇪🇺 EuropePMC...");
  const epmcResult = await findPdfViaEuropePmc(doi, pmid);
  if (epmcResult && (await downloadAndAttachPdf(item, epmcResult))) {
    return true;
  }

  // Step 7.5: Local Resolver
  try {
    // @ts-expect-error - import.meta.glob not in types
    const extraResolvers = import.meta.glob("../../doc/localResolver.ts", {
      eager: true,
    });
    for (const path in extraResolvers) {
      const mod = extraResolvers[path] as any;
      if (mod && typeof mod.findPdf === "function" && doi) {
        onProgress?.("🕵️ Checking local resolver...");
        try {
          const pdfUrl = await mod.findPdf(doi);
          if (pdfUrl && (await downloadAndAttachPdf(item, pdfUrl))) {
            return true;
          }
        } catch (e) {
          Zotero.debug(`[seerai] Local resolver failed: ${e}`);
        }
      }
    }
  } catch (e) {
    // Ignore
  }

  // Step 8: Source Link
  const sourceLink = getSourceLinkForPaper(doi, arxivId, pmid, undefined, url);
  if (sourceLink) {
    onProgress?.("🔗 Source Link...");
    if (await downloadAndAttachPdf(item, sourceLink)) {
      return true;
    }
  }

  // Step 9: Firecrawl (if configured)
  if (firecrawlService.isConfigured() && title) {
    onProgress?.("🔥 Firecrawl...");
    try {
      const creators = item.getCreators();
      const authors = creators.map((c) =>
        `${c.firstName || ""} ${c.lastName || ""}`.trim(),
      );
      const pdfResult = await firecrawlService.researchSearch(
        title,
        authors,
        doi,
      );
      if (
        pdfResult &&
        (pdfResult.status === "pdf_found" || pdfResult.status === "page_found")
      ) {
        const foundUrl = pdfResult.pdfUrl || pdfResult.pageUrl;
        if (foundUrl && (await downloadAndAttachPdf(item, foundUrl))) {
          return true;
        }
      }
    } catch (e) {
      Zotero.debug(`[seerai] Firecrawl search failed: ${e}`);
    }
  }

  return false;
}

/**
 * Get the canonical source URL for a paper based on available identifiers
 * Priority: DOI > ArXiv > PMC > PMID > URL field
 */
function getSourceLinkForPaper(
  doi?: string,
  arxivId?: string,
  pmid?: string,
  pmcid?: string,
  url?: string,
): string | null {
  if (doi) return `https://doi.org/${doi}`;
  if (arxivId) return `https://arxiv.org/${arxivId.replace(/^arxiv:/i, "")}`;
  if (pmcid) return `https://pmc.ncbi.nlm.nih.gov/articles/${pmcid}/`;
  if (pmid) return `https://pubmed.ncbi.nlm.nih.gov/${pmid}/`;
  if (url) return url;
  return null;
}

/**
 * Get PDF text for a Zotero item, auto-indexing if needed.
 * Uses Zotero 7 APIs: item.attachmentText, Zotero.FullText.indexItems()
 *
 * Content Hierarchy:
 * 1. ALL notes are always included
 * 2. If a same-title note exists (contains OCR/extracted content), skip indexed PDF to avoid duplication
 * 3. If NO same-title note exists, add indexed PDF text (auto-index if needed)
 * 4. If no PDF found, just return notes content (or null if no notes either)
 *
 * @param item - The parent Zotero item (regular item, not attachment)
 * @param maxLength - Maximum text length to return (0 = no limit)
 * @param autoIndex - Whether to auto-index unindexed PDFs (default true)
 * @param includeAllNotes - Whether to include all notes (default true)
 * @returns Combined content from notes and/or PDF, or null if not available
 */

interface FilterPreset {
  name: string;
  filters: SearchState;
}

function getFilterPresets(): FilterPreset[] {
  try {
    const stored = Zotero.Prefs.get(
      "extensions.seerai.filterPresets",
    ) as string;
    return stored ? JSON.parse(stored) : [];
  } catch {
    return [];
  }
}

function saveFilterPresets(presets: FilterPreset[]): void {
  Zotero.Prefs.set("extensions.seerai.filterPresets", JSON.stringify(presets));
}

function addFilterPreset(name: string, filters: SearchState): void {
  const presets = getFilterPresets();
  presets.push({ name, filters: { ...filters } });
  saveFilterPresets(presets);
}

function deleteFilterPreset(name: string): void {
  const presets = getFilterPresets().filter((p) => p.name !== name);
  saveFilterPresets(presets);
}

function getNextPresetName(): string {
  const presets = getFilterPresets();
  let num = 1;
  while (presets.some((p) => p.name === `Preset ${num}`)) num++;
  return `Preset ${num}`;
}

// ==================== Search History (File-based Persistence) ====================

interface SearchHistoryEntry {
  id: string;
  query: string;
  state: SearchState;
  results: SemanticScholarPaper[];
  totalResults: number;
  searchToken: string | null;
  savedAt: string;
}

const SEARCH_HISTORY_MAX_ENTRIES = 20;
let searchHistoryDataDir: string | null = null;
let searchHistoryFilePath: string | null = null;

function getSearchHistoryFilePath(): string {
  if (!searchHistoryFilePath) {
    searchHistoryDataDir = PathUtils.join(
      Zotero.DataDirectory.dir,
      config.addonRef,
    );
    searchHistoryFilePath = PathUtils.join(
      searchHistoryDataDir,
      "search_history.json",
    );
  }
  return searchHistoryFilePath;
}

async function ensureSearchHistoryDir(): Promise<void> {
  const dataDir = PathUtils.join(Zotero.DataDirectory.dir, config.addonRef);
  if (!(await IOUtils.exists(dataDir))) {
    await IOUtils.makeDirectory(dataDir, { ignoreExisting: true });
  }
}

async function getSearchHistory(): Promise<SearchHistoryEntry[]> {
  try {
    await ensureSearchHistoryDir();
    const filePath = getSearchHistoryFilePath();

    if (!(await IOUtils.exists(filePath))) {
      return [];
    }

    const contentBytes = await IOUtils.read(filePath);
    const content = new TextDecoder().decode(contentBytes);
    if (!content) return [];

    return JSON.parse(content) as SearchHistoryEntry[];
  } catch (e) {
    Zotero.debug(`[seerai] Error loading search history: ${e}`);
    return [];
  }
}

async function saveSearchHistory(entries: SearchHistoryEntry[]): Promise<void> {
  try {
    await ensureSearchHistoryDir();
    const filePath = getSearchHistoryFilePath();
    const encoder = new TextEncoder();
    await IOUtils.write(
      filePath,
      encoder.encode(JSON.stringify(entries, null, 2)),
    );
  } catch (e) {
    Zotero.debug(`[seerai] Error saving search history: ${e}`);
  }
}

async function addSearchHistoryEntry(entry: SearchHistoryEntry): Promise<void> {
  const history = await getSearchHistory();

  // Remove any existing entry with the same query (avoid duplicates)
  const filtered = history.filter(
    (h) => h.query.toLowerCase() !== entry.query.toLowerCase(),
  );

  // Add new entry at the beginning
  filtered.unshift(entry);

  // Keep only max entries
  const trimmed = filtered.slice(0, SEARCH_HISTORY_MAX_ENTRIES);

  await saveSearchHistory(trimmed);
  Zotero.debug(
    `[seerai] Added search history entry: "${entry.query}" (${entry.results.length} results)`,
  );
}

async function deleteSearchHistoryEntry(id: string): Promise<void> {
  const history = await getSearchHistory();
  const filtered = history.filter((h) => h.id !== id);
  await saveSearchHistory(filtered);
  Zotero.debug(`[seerai] Deleted search history entry: ${id}`);
}

/**
 * Update the most recent history entry for a query with AI insights
 */
async function updateSearchHistoryWithInsights(
  query: string,
  insights: string,
): Promise<void> {
  const history = await getSearchHistory();
  const entry = history.find(
    (h) => h.query.toLowerCase() === query.toLowerCase(),
  );
  if (entry) {
    entry.state.cachedAiInsights = insights;
    await saveSearchHistory(history);
    Zotero.debug(
      `[seerai] Updated search history entry with AI insights: "${query}"`,
    );
  }
}

// ==================== Search Column Configuration Persistence ====================

function getSearchColumnConfigPath(): string {
  if (!searchColumnConfigFilePath) {
    const dataDir = PathUtils.join(Zotero.DataDirectory.dir, config.addonRef);
    searchColumnConfigFilePath = PathUtils.join(dataDir, "search_columns.json");
  }
  return searchColumnConfigFilePath;
}

async function loadSearchColumnConfig(): Promise<SearchColumnConfig> {
  try {
    await ensureSearchHistoryDir(); // Reuse the same data directory
    const filePath = getSearchColumnConfigPath();

    if (!(await IOUtils.exists(filePath))) {
      return { ...defaultSearchColumnConfig };
    }

    const contentBytes = await IOUtils.read(filePath);
    const content = new TextDecoder().decode(contentBytes);
    if (!content) return { ...defaultSearchColumnConfig };

    const parsed = JSON.parse(content) as SearchColumnConfig;
    // Ensure structure is valid
    return {
      columns: parsed.columns || [],
      generatedData: parsed.generatedData || {},
      responseLength: parsed.responseLength,
    };
  } catch (e) {
    Zotero.debug(`[seerai] Error loading search column config: ${e}`);
    return { ...defaultSearchColumnConfig };
  }
}

async function saveSearchColumnConfig(): Promise<void> {
  try {
    await ensureSearchHistoryDir();
    const filePath = getSearchColumnConfigPath();
    const encoder = new TextEncoder();
    await IOUtils.write(
      filePath,
      encoder.encode(JSON.stringify(searchColumnConfig, null, 2)),
    );
    Zotero.debug(
      `[seerai] Saved search column config (${searchColumnConfig.columns.length} columns)`,
    );
  } catch (e) {
    Zotero.debug(`[seerai] Error saving search column config: ${e}`);
  }
}

// DataLabs service for PDF-to-note conversion
const ocrService = new OcrService();

export class Assistant {
  public static getOcrService() {
    return ocrService;
  }

  public static async getPdfTextForItem(
    item: Zotero.Item,
    maxLength: number = 0,
    autoIndex: boolean = true,
    includeAllNotes: boolean = true,
  ): Promise<string | null> {
    const stateManager = getChatStateManager();
    const options = stateManager.getOptions();

    if (options.includeNotesOnly) {
      Zotero.debug(
        "[seerai] includeNotesOnly is enabled, skipping PDF extraction",
      );
      // Just collect notes via a simplified path or let the existing logic run but skip Step 2
    }

    const parts: string[] = [];
    let hasSameTitleNote = false;
    const itemTitle = ((item.getField("title") as string) || "")
      .toLowerCase()
      .trim();

    // Step 1: Always collect all notes
    if (includeAllNotes) {
      const noteIds = item.getNotes();
      for (const noteId of noteIds) {
        const note = Zotero.Items.get(noteId);
        if (note) {
          const noteHTML = note.getNote();
          // Strip HTML tags to get plain text
          const plainText = noteHTML
            .replace(/<[^>]*>/g, " ")
            .replace(/\s+/g, " ")
            .trim();

          if (plainText.length > 0) {
            // Get note title
            const noteTitle = (note.getNoteTitle() || "").toLowerCase().trim();

            // Check if this is a same-title note (contains extracted PDF content)
            const isSameTitle =
              noteTitle &&
              itemTitle &&
              noteTitle.includes(
                itemTitle.substring(0, Math.min(30, itemTitle.length)),
              );

            if (isSameTitle) {
              hasSameTitleNote = true;
              Zotero.debug(
                `[seerai] Found same-title note for item ${item.id}`,
              );

              // If "Use notes only" is enabled, skip the giant OCR-dump note
              // unless "Allow duplicates" is explicitly on.
              if (
                options.includeNotesOnly &&
                !options.disableSameTitleNoteSkip
              ) {
                Zotero.debug(
                  `[seerai] Skipping same-title note for ${item.id} (Notes Only mode)`,
                );
                continue;
              }
            }

            parts.push(
              `[Note: ${note.getNoteTitle() || "Untitled Note"}]\n${plainText}`,
            );
          }
        }
      }
    }

    // Step 2: Add indexed PDF text ONLY if no same-title note exists (to avoid duplication)
    // UNLESS disableSameTitleNoteSkip is enabled or includeNotesOnly is enabled
    const skipPdf =
      (hasSameTitleNote && !options.disableSameTitleNoteSkip) ||
      options.includeNotesOnly;

    if (!skipPdf) {
      const attachmentIds = item.getAttachments();

      for (const attId of attachmentIds) {
        const att = Zotero.Items.get(attId);
        if (!att || att.attachmentContentType !== "application/pdf") continue;

        try {
          // Try Zotero 7 high-level API (item.attachmentText is async getter)
          let text = await (att as any).attachmentText;

          if (text && text.length > 0) {
            Zotero.debug(
              `[seerai] Got indexed PDF text for attachment ${attId} (${text.length} chars)`,
            );
            parts.push(`[Indexed PDF Text]\n${text}`);
            break; // Only need one PDF's text
          }

          // Not indexed - try to index if autoIndex enabled
          if (autoIndex) {
            Zotero.debug(
              `[seerai] PDF not indexed, triggering indexing for attachment ${attId}`,
            );

            // Ensure DB is ready before indexing
            await att.saveTx();

            // Call the asynchronous indexer
            await (Zotero.FullText as any).indexItems([attId]);

            // Retry after indexing
            text = await (att as any).attachmentText;
            if (text && text.length > 0) {
              Zotero.debug(
                `[seerai] Indexing complete, got ${text.length} chars`,
              );
              parts.push(`[Indexed PDF Text]\n${text}`);
              break;
            }

            Zotero.debug(
              `[seerai] No text after indexing - likely image-only PDF`,
            );
          }
        } catch (e) {
          Zotero.debug(
            `[seerai] Error getting PDF text for attachment ${attId}: ${e}`,
          );
        }
      }
    } else {
      Zotero.debug(
        `[seerai] Skipping indexed PDF for item ${item.id} - same-title note already included`,
      );
    }

    // Return combined content
    if (parts.length === 0) {
      return null;
    }

    const combined = parts.join("\n\n");
    return maxLength > 0 ? combined.substring(0, maxLength) : combined;
  }

  /**
   * Content extractor for the RAG retrieval engine.
   * Extracts structured content (abstract, notes, PDF text) from a Zotero item
   * in a format suitable for chunking and embedding.
   */
  public static async extractContentForRAG(itemId: number): Promise<{
    abstract?: string;
    notes?: string[];
    pdfText?: string;
    title?: string;
    authors?: string[];
  } | null> {
    try {
      const zoteroItem = Zotero.Items.get(itemId);
      if (!zoteroItem || !zoteroItem.isRegularItem()) return null;

      const title = (zoteroItem.getField("title") as string) || undefined;
      const abstract =
        (zoteroItem.getField("abstractNote") as string) || undefined;
      const authors = zoteroItem
        .getCreators()
        .map((c: any) => `${c.firstName} ${c.lastName}`.trim())
        .filter(Boolean);

      // Collect notes (with same-title note detection to avoid PDF duplication)
      const notes: string[] = [];
      let hasSameTitleNote = false;
      const itemTitleLower = (title || "").toLowerCase().trim();
      const noteIds = zoteroItem.getNotes();
      for (const noteId of noteIds) {
        const note = Zotero.Items.get(noteId);
        if (note) {
          const noteHTML = note.getNote();
          const plainText = noteHTML
            .replace(/<[^>]*>/g, " ")
            .replace(/\s+/g, " ")
            .trim();
          if (plainText.length > 0) {
            // Detect same-title notes (auto-generated from PDF/OCR extraction)
            const noteTitle = (note.getNoteTitle() || "").toLowerCase().trim();
            if (
              noteTitle &&
              itemTitleLower &&
              noteTitle.includes(
                itemTitleLower.substring(
                  0,
                  Math.min(30, itemTitleLower.length),
                ),
              )
            ) {
              hasSameTitleNote = true;
              Zotero.debug(
                `[seerai] RAG: same-title note detected for item ${itemId}, will skip PDF text`,
              );
            }
            notes.push(plainText);
          }
        }
      }

      // Get PDF text — skip if a same-title note already contains the content
      let pdfText: string | undefined;
      if (hasSameTitleNote) {
        Zotero.debug(
          `[seerai] RAG: skipping PDF extraction for item ${itemId} — same-title note already included`,
        );
      } else {
        const attachmentIds = zoteroItem.getAttachments();
        for (const attId of attachmentIds) {
          const att = Zotero.Items.get(attId);
          if (!att || att.attachmentContentType !== "application/pdf") continue;
          try {
            const text = await (att as any).attachmentText;
            if (text && text.length > 0) {
              pdfText = text;
              break;
            }
          } catch (e) {
            Zotero.debug(
              `[seerai] RAG: error extracting PDF for attachment ${attId}: ${e}`,
            );
          }
        }
      }

      if (!abstract && notes.length === 0 && !pdfText) return null;

      return { title, abstract, notes, pdfText, authors };
    } catch (e) {
      Zotero.debug(
        `[seerai] RAG: error extracting content for item ${itemId}: ${e}`,
      );
      return null;
    }
  }

  /**
   * Expand context items for RAG: resolve tags, collections, and authors
   * into their constituent paper item IDs, and collect passthrough content
   * for non-indexable types (tables, files, topics).
   *
   * Returns:
   *   expandedItems — deduplicated paper-type context items for vector search
   *   passthroughContext — verbatim context string for tables, files, topics
   */
  public static async expandContextItemsForRAG(
    contextItems: Array<{
      id: number | string;
      type: string;
      displayName: string;
      metadata?: Record<string, any>;
    }>,
  ): Promise<{
    expandedItems: Array<{
      id: number;
      type: string;
      displayName: string;
      metadata?: Record<string, any>;
    }>;
    passthroughContext: string;
  }> {
    const seenPaperIds = new Set<number>();
    const paperItems: Array<{
      id: number;
      type: string;
      displayName: string;
      metadata?: Record<string, any>;
    }> = [];
    const passthroughParts: string[] = [];

    for (const item of contextItems) {
      if (item.type === "paper" && typeof item.id === "number") {
        // Direct paper reference — pass through
        if (!seenPaperIds.has(item.id)) {
          seenPaperIds.add(item.id);
          paperItems.push({
            id: item.id,
            type: "paper",
            displayName: item.displayName,
            metadata: item.metadata,
          });
        }
      } else if (item.type === "tag") {
        // Expand tag → all papers with this tag (search all libraries)
        try {
          const tagName = item.displayName;
          const itemIDs = await Assistant.searchAllLibraries(
            "tag",
            "is",
            tagName,
          );

          for (const itemID of itemIDs) {
            if (seenPaperIds.has(itemID)) continue;
            const zItem = Zotero.Items.get(itemID);
            if (!zItem || !zItem.isRegularItem()) continue;
            seenPaperIds.add(itemID);
            paperItems.push({
              id: itemID,
              type: "paper",
              displayName:
                (zItem.getField("title") as string) || `Item ${itemID}`,
            });
          }
          Zotero.debug(
            `[seerai] RAG expand: tag "${tagName}" → ${itemIDs.length} papers`,
          );
        } catch (e) {
          Zotero.debug(
            `[seerai] RAG expand: error expanding tag "${item.displayName}": ${e}`,
          );
        }
      } else if (item.type === "collection") {
        // Expand collection → all child items (recursive)
        try {
          const collectionId = item.id as number;
          const collection = Zotero.Collections.get(collectionId);
          if (collection) {
            const childItemIDs = collection.getChildItems(true);
            for (const childId of childItemIDs) {
              if (seenPaperIds.has(childId)) continue;
              const zItem = Zotero.Items.get(childId);
              if (!zItem || !zItem.isRegularItem()) continue;
              seenPaperIds.add(childId);
              paperItems.push({
                id: childId,
                type: "paper",
                displayName:
                  (zItem.getField("title") as string) || `Item ${childId}`,
              });
            }
            Zotero.debug(
              `[seerai] RAG expand: collection "${item.displayName}" → ${childItemIDs.length} items`,
            );
          }
        } catch (e) {
          Zotero.debug(
            `[seerai] RAG expand: error expanding collection "${item.displayName}": ${e}`,
          );
        }
      } else if (item.type === "author") {
        // Expand author → all papers by this author (search all libraries)
        try {
          const authorName = item.displayName;
          const itemIDs = await Assistant.searchAllLibraries(
            "creator",
            "contains",
            authorName,
          );

          for (const itemID of itemIDs) {
            if (seenPaperIds.has(itemID)) continue;
            const zItem = Zotero.Items.get(itemID);
            if (!zItem || !zItem.isRegularItem()) continue;
            seenPaperIds.add(itemID);
            paperItems.push({
              id: itemID,
              type: "paper",
              displayName:
                (zItem.getField("title") as string) || `Item ${itemID}`,
            });
          }
          Zotero.debug(
            `[seerai] RAG expand: author "${authorName}" → ${itemIDs.length} papers`,
          );
        } catch (e) {
          Zotero.debug(
            `[seerai] RAG expand: error expanding author "${item.displayName}": ${e}`,
          );
        }
      } else if (item.type === "table") {
        // Tables: include verbatim as passthrough context (not vector-searchable)
        try {
          const storedTables = await getTableStore().getAllTables();
          const tableConfig = storedTables.find((t: any) => t.id === item.id);
          if (tableConfig) {
            let tablePart = `\n--- Table: ${tableConfig.name} ---`;
            const columnNames = tableConfig.columns.map(
              (c: any) => c.name || c.title,
            );
            tablePart += `\nColumns: ${columnNames.join(", ")}`;
            tablePart += `\nTotal papers: ${tableConfig.addedPaperIds.length}`;

            const generatedData = tableConfig.generatedData || {};
            if (tableConfig.addedPaperIds.length > 0) {
              tablePart += `\n\n=== Table Data ===`;
              for (const paperId of tableConfig.addedPaperIds) {
                const zoteroItem = Zotero.Items.get(paperId);
                if (!zoteroItem) continue;
                const title = zoteroItem.getField("title") || "Untitled";
                const creators = zoteroItem.getCreators();
                const authorStr =
                  creators.length > 0
                    ? creators
                        .map((c: any) => c.lastName || c.name)
                        .slice(0, 3)
                        .join(", ") + (creators.length > 3 ? " et al." : "")
                    : "";
                const year =
                  zoteroItem.getField("year") ||
                  zoteroItem.getField("date")?.substring(0, 4) ||
                  "";
                tablePart += `\n\n--- Paper: ${title} ---`;
                if (authorStr) tablePart += `\nAuthors: ${authorStr}`;
                if (year) tablePart += ` (${year})`;

                const paperData = generatedData[paperId];
                if (paperData && Object.keys(paperData).length > 0) {
                  tablePart += `\n`;
                  for (const column of tableConfig.columns as any[]) {
                    const columnId = column.id;
                    const columnName = column.name || column.title || columnId;
                    const value = paperData[columnId];
                    if (
                      ["title", "author", "year", "sources"].includes(columnId)
                    )
                      continue;
                    if (value) {
                      tablePart += `\n${columnName}: ${value}`;
                    }
                  }
                }
              }
            }
            passthroughParts.push(tablePart);
          }
        } catch (e) {
          Zotero.debug(
            `[seerai] RAG expand: error building table passthrough for "${item.displayName}": ${e}`,
          );
        }
      } else if (item.type === "file") {
        const filename =
          (item.metadata?.filename as string) || item.displayName;
        const workspacePath = item.metadata?.workspacePath as
          | string
          | undefined;

        let resolvedContent: string | undefined;
        if (workspacePath) {
          try {
            const store = getWorkspaceStore();
            const absPath = PathUtils.join(store.workspaceDir, workspacePath);
            if (absPath.toLowerCase().endsWith(".docx")) {
              const raw = await Zotero.File.getBinaryContentsAsync(absPath);
              const ab = raw as unknown as ArrayBuffer;
              const result = await convertDocxToMarkdown(ab);
              resolvedContent = stripBase64Data(result.markdown);
            } else {
              const raw = await IOUtils.read(absPath);
              resolvedContent = stripBase64Data(new TextDecoder().decode(raw));
            }
          } catch (e) {
            Zotero.debug(
              `[seerai] Failed to resolve workspace file for passthrough: ${e}`,
            );
          }
        }

        const category = item.metadata?.fileCategory as string | undefined;
        const extractedContent = item.metadata?.extractedContent as
          | string
          | undefined;
        const extractionError = item.metadata?.extractionError as
          | string
          | undefined;

        const safeContent =
          resolvedContent ??
          (extractedContent ? stripBase64Data(extractedContent) : undefined);

        let filePart = `\n--- File: ${filename} ---`;
        if (safeContent && safeContent.length > 0) {
          if (category === "audio") {
            filePart += `\n[Transcribed audio: ${filename}]\n${safeContent}`;
          } else {
            filePart += `\n${safeContent}`;
          }
        } else if (extractionError) {
          filePart += `\n(${extractionError})`;
        } else {
          filePart += `\n(No text content could be extracted from this file)`;
        }
        passthroughParts.push(filePart);
      } else if (item.type === "topic") {
        // Topics: include as passthrough focus directive
        passthroughParts.push(
          `\nFocus Topic: "${item.displayName}" - Please consider this topic when answering.`,
        );
      }
    }

    return {
      expandedItems: paperItems,
      passthroughContext: passthroughParts.join("\n"),
    };
  }

  public static getSearchState(): SearchState {
    return currentSearchState;
  }

  public static setSearchState(state: SearchState): void {
    currentSearchState = state;
  }

  private static playNotificationSound(): void {
    try {
      const soundEnabled = Zotero.Prefs.get(
        `${addon.data.config.prefsPrefix}.tableGenerationSound`,
        true,
      ) as boolean;
      if (!soundEnabled) return;

      const win = Zotero.getMainWindow();
      if (!win) return;

      const audioContext = new win.AudioContext();
      const oscillator = audioContext.createOscillator();
      const gainNode = audioContext.createGain();

      oscillator.connect(gainNode);
      gainNode.connect(audioContext.destination);

      oscillator.type = "sine";
      oscillator.frequency.setValueAtTime(880, audioContext.currentTime);
      oscillator.frequency.exponentialRampToValueAtTime(
        1760,
        audioContext.currentTime + 0.1,
      );

      gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
      gainNode.gain.exponentialRampToValueAtTime(
        0.01,
        audioContext.currentTime + 0.3,
      );

      oscillator.start(audioContext.currentTime);
      oscillator.stop(audioContext.currentTime + 0.3);
    } catch (e) {
      Zotero.debug(`[seerai] Failed to play notification sound: ${e}`);
    }
  }

  // UI state
  private static isStreaming: boolean = false;
  private static lastRenderTime: number = 0;
  private static lastRenderValue: string = "";
  private static lastRenderedItemId: number = 0;
  private static lastRenderedTab: string = "";
  private static lastRenderedContainer: HTMLElement | null = null;
  private static readonly RENDER_THROTTLE_MS: number = 60;

  /**
   * Throttled markdown rendering to prevent layout thrashing and crashes
   */
  private static smartRender(
    mdContainer: HTMLElement,
    content: string,
    force: boolean = false,
  ) {
    const now = Date.now();
    if (
      !force &&
      now - this.lastRenderTime < this.RENDER_THROTTLE_MS &&
      content === this.lastRenderValue
    ) {
      return;
    }

    if (!force && now - this.lastRenderTime < this.RENDER_THROTTLE_MS) {
      return;
    }

    mdContainer.setAttribute("data-raw", content);
    try {
      mdContainer.innerHTML = parseMarkdown(content);
    } catch (e) {
      Zotero.debug(`[seerai] smartRender failed: ${e}`);
      // Only fallback if it's the first time or if previous renders also failed
      // We don't want to clear the screen if possible, but innerHTML assignment
      // failure usually happens BEFORE anything is rendered.
      mdContainer.textContent = content;
    }
    this.lastRenderTime = now;
    this.lastRenderValue = content;
  }

  static register() {
    Zotero.ItemPaneManager.registerSection({
      paneID: "smart-assistant",
      pluginID: config.addonID,
      header: {
        l10nID: "assistant-header-label",
        icon: `chrome://${config.addonRef}/content/icons/icon-16.png`,
      },
      sidenav: {
        l10nID: "assistant-sidenav-tooltip",
        icon: `chrome://${config.addonRef}/content/icons/icon-20.png`,
      },
      onRender: ({ body, item, setSectionSummary }) => {
        // Update current item tracking (no longer resets state on navigation)
        // This allows multi-selection to persist across item changes
        currentItemId = item.id;
        currentContainer = body;
        currentItem = item;

        // If window is detached, show minimal "Open in Window" UI instead
        if (DetachedWindowManager.isDetached()) {
          this.renderDetachedPlaceholder(body);
          setSectionSummary("Open in separate window");
          return;
        }

        this.renderInterface(body, item, true);
        const stateManager = getChatStateManager();
        setSectionSummary(stateManager.getSummary());
      },
    });
  }

  /**
   * Render a placeholder UI when the main interface is in a detached window
   */
  private static renderDetachedPlaceholder(container: HTMLElement): void {
    const doc = container.ownerDocument!;

    const placeholder = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        height: "200px",
        textAlign: "center",
        padding: "20px",
        gap: "16px",
      },
    });

    const icon = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "🪟" },
      styles: {
        fontSize: "48px",
        marginBottom: "8px",
      },
    });

    const text = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "SeerAI is open in a separate window" },
      styles: {
        fontSize: "14px",
        color: "var(--text-secondary)",
        marginBottom: "12px",
      },
    });

    const focusBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Focus Window" },
      styles: {
        padding: "8px 16px",
        borderRadius: "6px",
        background: "var(--highlight-primary)",
        color: "white",
        border: "none",
        cursor: "pointer",
        fontWeight: "500",
        fontSize: "13px",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            DetachedWindowManager.focusWindow();
          },
        },
      ],
    });

    const attachBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Bring Back to Sidebar" },
      styles: {
        padding: "8px 16px",
        borderRadius: "6px",
        background: "transparent",
        color: "var(--text-secondary)",
        border: "1px solid var(--border-primary)",
        cursor: "pointer",
        fontWeight: "500",
        fontSize: "13px",
        marginTop: "8px",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            DetachedWindowManager.attach();
          },
        },
      ],
    });

    const shortcutHint = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "Shortcut: Ctrl+Shift+S" },
      styles: {
        fontSize: "11px",
        color: "var(--text-tertiary)",
        marginTop: "12px",
      },
    });

    placeholder.appendChild(icon);
    placeholder.appendChild(text);
    placeholder.appendChild(focusBtn);
    placeholder.appendChild(attachBtn);
    placeholder.appendChild(shortcutHint);
    container.replaceChildren(placeholder);
  }

  /**
   * Used by the detached window to render the full interface
   */
  public static async renderToContainer(
    container: HTMLElement,
    item: Zotero.Item,
  ): Promise<void> {
    currentContainer = container;
    currentItem = item;
    currentItemId = item.id;
    await this.renderInterface(container, item);
  }

  /**
   * Re-render just the selection area (for efficient updates)
   */
  private static reRenderSelectionArea(): void {
    // Selection area is currently hidden, so no-op
    // If it becomes visible in future, implement re-rendering logic here
  }

  /**
   * Convert Zotero item to SelectedItem format
   */
  private static itemToSelection(item: Zotero.Item): SelectedItem {
    const creators = item
      .getCreators()
      .map((c) => `${c.firstName || ""} ${c.lastName || ""}`.trim());
    return {
      id: item.id,
      type: (item.itemType as SelectedItem["type"]) || "other",
      title: (item.getField("title") as string) || "Untitled",
      abstract: (item.getField("abstractNote") as string) || undefined,
      creators,
      year: (item.getField("year") as string) || undefined,
    };
  }

  /**
   * Get ALL notes from a Zotero item as SelectedNote objects
   */
  private static async getItemNotesAsSelections(
    item: Zotero.Item,
  ): Promise<SelectedNote[]> {
    const notes: SelectedNote[] = [];

    let targetItem = item;
    if (item.isAttachment() && item.parentID) {
      const parent = Zotero.Items.get(item.parentID);
      if (parent) targetItem = parent as Zotero.Item;
    }

    if (!targetItem.isRegularItem()) return notes;

    const noteIDs = targetItem.getNotes();
    for (const id of noteIDs) {
      const noteItem = Zotero.Items.get(id);
      if (noteItem) {
        const noteHTML = noteItem.getNote();
        const plainText = this.stripHtml(noteHTML);
        if (plainText.trim()) {
          notes.push({
            id: noteItem.id,
            type: "note",
            title: `Note: ${((targetItem.getField("title") as string) || "").slice(0, 30)}...`,
            parentItemId: targetItem.id,
            content: plainText.trim(),
            dateModified: noteItem.dateModified,
          });
        }
      }
    }
    return notes;
  }

  /**
   * Strips HTML tags from a string to get plain text
   */
  private static stripHtml(html: string): string {
    const temp = new DOMParser().parseFromString(html, "text/html");
    return temp.body?.textContent || "";
  }

  /**
   * Format a date as relative time (e.g., "2 hours ago", "Yesterday")
   */
  private static formatRelativeTime(date: Date): string {
    const now = new Date();
    const diffMs = now.getTime() - date.getTime();
    const diffSecs = Math.floor(diffMs / 1000);
    const diffMins = Math.floor(diffSecs / 60);
    const diffHours = Math.floor(diffMins / 60);
    const diffDays = Math.floor(diffHours / 24);

    if (diffSecs < 60) return "Just now";
    if (diffMins < 60) return `${diffMins}m ago`;
    if (diffHours < 24) return `${diffHours}h ago`;
    if (diffDays === 1) return "Yesterday";
    if (diffDays < 7) return `${diffDays}d ago`;
    return date.toLocaleDateString();
  }

  /**
   * Restore search state from a history entry
   */
  private static restoreSearchFromHistory(
    entry: SearchHistoryEntry,
    doc: Document,
    searchInput: HTMLInputElement,
  ): void {
    // Restore state
    currentSearchState = { ...entry.state };
    currentSearchResults = [...entry.results];
    totalSearchResults = entry.totalResults;
    currentSearchToken = entry.searchToken;

    // Update search input
    searchInput.value = entry.query;

    // Re-render filters (requires rebuilding the filters UI)
    const filtersContainer = doc.querySelector(".search-filters-container");
    if (filtersContainer && filtersContainer.parentElement) {
      const parent = filtersContainer.parentElement;
      const newFilters = this.createSearchFilters(doc);
      filtersContainer.replaceWith(newFilters);
    }

    // Re-render results
    const resultsArea = doc.getElementById("semantic-scholar-results");
    if (resultsArea && currentItem) {
      this.renderSearchResults(doc, resultsArea as HTMLElement, currentItem);
    }

    Zotero.debug(
      `[seerai] Restored search from history: "${entry.query}" (${entry.results.length} results)`,
    );
  }

  /**
   * Add an item and its notes to the selection
   */
  /**
   * Add an item to the unified context manager
   */
  private static async addItemWithNotes(item: Zotero.Item) {
    const contextManager = ChatContextManager.getInstance();
    const stateManager = getChatStateManager();

    // Map Zotero Item to ContextItem
    const contextItem: ContextItem = {
      id: item.id,
      type: "paper",
      displayName: item.getField("title"),
      fullName: item.getField("title"),
      trigger: "/",
      source: "selection",
      metadata: {
        itemKey: item.key,
        libraryID: item.libraryID,
      },
    };

    const mode = stateManager.getOptions().selectionMode;

    if (mode === "explore") {
      // Additive - Treat as command so it persists
      contextManager.addItem(
        contextItem.id,
        contextItem.type,
        contextItem.displayName,
        "command",
        contextItem.metadata,
      );
    } else {
      // Default/Focus: Replace
      contextManager.syncFromSelection([item]);
    }
  }

  /**
   * Remove an item and its associated notes
   */
  /**
   * Estimate tokens for context items.
   *
   * For small selections (≤50 items total after expansion) we extract actual
   * content so the number shown matches what the model will see.  For large
   * selections we fall back to a per-item heuristic to keep the UI responsive.
   */
  private static async estimateContextTokens(
    items: ContextItem[],
  ): Promise<number> {
    const stateManager = getChatStateManager();
    const options = stateManager.getOptions();
    let total = 0;

    for (const item of items) {
      // 1. Base item metadata (title/id)
      total += ChatStateManager.countTokens(item.displayName);

      if (item.type === "paper") {
        const zoteroItem = Zotero.Items.get(item.id as number);
        if (!zoteroItem) continue;

        // Metadata tokens
        total += 50; // Average for authors/year

        if (options.includeAbstracts) {
          const abstract = zoteroItem.getField("abstractNote");
          if (abstract) total += ChatStateManager.countTokens(abstract);
        }

        // Content tokens
        try {
          const content = await Assistant.getPdfTextForItem(
            zoteroItem,
            0,
            false,
            true,
          );
          if (content) total += ChatStateManager.countTokens(content);
        } catch (e) {
          total += options.includeNotesOnly ? 150 : 1500;
        }
      } else if (item.type === "table") {
        // Count actual table content: generated data + metadata per paper
        const storedTables = await getTableStore().getAllTables();
        const tableConfig = storedTables.find((t) => t.id === item.id);
        if (tableConfig) {
          // Header + column definitions
          const columnNames = tableConfig.columns
            .map((c: any) => c.name || c.id)
            .join(", ");
          total += ChatStateManager.countTokens(
            `Table: ${tableConfig.name}\nColumns: ${columnNames}\nTotal papers: ${tableConfig.addedPaperIds.length}`,
          );

          // Actual generated data
          const generatedData = tableConfig.generatedData || {};
          for (const paperId of tableConfig.addedPaperIds) {
            const zoteroItem = Zotero.Items.get(paperId);
            if (!zoteroItem) continue;

            // Paper metadata
            const title = (zoteroItem.getField("title") as string) || "";
            const creators = zoteroItem.getCreators();
            const authorStr =
              creators.length > 0
                ? creators
                    .map((c: any) => c.lastName || c.name)
                    .slice(0, 3)
                    .join(", ") + (creators.length > 3 ? " et al." : "")
                : "";
            const year =
              (zoteroItem.getField("year") as string) ||
              (zoteroItem.getField("date") as string)?.substring(0, 4) ||
              "";

            total += ChatStateManager.countTokens(
              `Paper: ${title}\nAuthors: ${authorStr} (${year})`,
            );

            // Generated column values
            const paperData = generatedData[paperId];
            if (paperData) {
              for (const column of tableConfig.columns) {
                if (["title", "author", "year", "sources"].includes(column.id))
                  continue;
                const value = paperData[column.id];
                if (value) {
                  total += ChatStateManager.countTokens(
                    `${column.name || column.id}: ${value}`,
                  );
                }
              }
            }
          }
        }
      } else if (item.type === "file") {
        const workspacePath = item.metadata?.workspacePath as
          | string
          | undefined;
        if (workspacePath) {
          try {
            const store = getWorkspaceStore();
            const absPath = PathUtils.join(store.workspaceDir, workspacePath);
            if (absPath.toLowerCase().endsWith(".docx")) {
              const raw = await Zotero.File.getBinaryContentsAsync(absPath);
              const ab = raw as unknown as ArrayBuffer;
              const result = await convertDocxToMarkdown(ab);
              const safe = stripBase64Data(result.markdown);
              total += ChatStateManager.countTokens(safe);
            } else {
              const raw = await IOUtils.read(absPath);
              const txt = stripBase64Data(new TextDecoder().decode(raw));
              total += ChatStateManager.countTokens(txt);
            }
          } catch (e) {
            Zotero.debug(
              `[seerai] Failed to estimate tokens for workspace file: ${e}`,
            );
            total += 100; // rough fallback
          }
        } else {
          // Uploaded file — token estimate was computed at upload time and stored in metadata
          const fileTokens = item.metadata?.estimatedTokens as
            | number
            | undefined;
          if (fileTokens && fileTokens > 0) {
            total += fileTokens;
          } else {
            // Fallback: compute from extractedContent or charCount
            const content = item.metadata?.extractedContent as
              | string
              | undefined;
            if (content) {
              total += ChatStateManager.countTokens(content);
            } else {
              const charCount = item.metadata?.charCount as number | undefined;
              if (charCount) {
                total += Math.ceil(charCount / 4);
              }
            }
          }
        }
      } else if (
        item.type === "collection" ||
        item.type === "tag" ||
        item.type === "author"
      ) {
        // Resolve to actual paper IDs, then count real content
        try {
          const resolvedIds = await Assistant.resolveContextItemToIds(item);

          if (resolvedIds.length === 0) continue;

          // For ≤50 items, count actual content for accuracy
          if (resolvedIds.length <= 50) {
            for (const itemID of resolvedIds) {
              const zoteroItem = Zotero.Items.get(itemID);
              if (!zoteroItem || !zoteroItem.isRegularItem()) continue;

              // Metadata
              total += 50;
              const title = zoteroItem.getField("title") as string;
              if (title) total += ChatStateManager.countTokens(title);

              if (options.includeAbstracts) {
                const abstract = zoteroItem.getField("abstractNote");
                if (abstract) total += ChatStateManager.countTokens(abstract);
              }

              // Content
              try {
                const content = await Assistant.getPdfTextForItem(
                  zoteroItem,
                  0,
                  false,
                  true,
                );
                if (content) total += ChatStateManager.countTokens(content);
              } catch (e) {
                total += options.includeNotesOnly ? 150 : 1500;
              }
            }
          } else {
            // Large selection — use heuristic
            total +=
              resolvedIds.length * (options.includeNotesOnly ? 250 : 1800);
          }

          Zotero.debug(
            `[seerai] Expanded ${item.type} "${item.displayName}": ${resolvedIds.length} items`,
          );
        } catch (e) {
          Zotero.debug(`[seerai] Expansion failed for ${item.type}: ${e}`);
          total += 1000; // Baseline fallback
        }
      }
    }

    return total;
  }

  /**
   * Search across all Zotero libraries for items matching a condition.
   * Returns deduplicated item IDs. Used for tags/authors which may span
   * group libraries, not just the user library.
   */
  private static async searchAllLibraries(
    conditionName: string,
    conditionOperator: string,
    conditionValue: string,
  ): Promise<number[]> {
    const allIds = new Set<number>();
    const libraries = Zotero.Libraries.getAll();

    for (const library of libraries) {
      try {
        const s = new Zotero.Search({ libraryID: library.libraryID });
        (s as any).addCondition(
          conditionName,
          conditionOperator,
          conditionValue,
        );
        s.addCondition("itemType", "isNot", "attachment");
        s.addCondition("itemType", "isNot", "note");
        const ids = await s.search();
        for (const id of ids) {
          allIds.add(id);
        }
      } catch (e) {
        Zotero.debug(
          `[seerai] searchAllLibraries: error in library ${library.libraryID}: ${e}`,
        );
      }
    }

    return Array.from(allIds);
  }

  /**
   * Resolve a context item (collection, tag, author) to an array of Zotero item IDs.
   * Shared helper used by token estimation and copy.
   */
  public static async resolveContextItemToIds(
    item: ContextItem,
  ): Promise<number[]> {
    if (item.type === "collection") {
      const collectionId = typeof item.id === "number" ? item.id : null;
      const collectionKey = item.metadata?.key || item.metadata?.collectionKey;

      let collection: any = null;
      if (collectionId) {
        collection = Zotero.Collections.get(collectionId);
      } else if (collectionKey) {
        const libID =
          item.metadata?.libraryID ?? Zotero.Libraries.userLibraryID;
        collection = Zotero.Collections.getByLibraryAndKey(
          libID,
          collectionKey,
        );
      }

      if (!collection) return [];

      const childItems = collection.getChildItems(true); // recursive
      const regularIds: number[] = [];
      for (const id of childItems) {
        const zItem = Zotero.Items.get(id);
        if (zItem && zItem.isRegularItem()) {
          regularIds.push(typeof id === "number" ? id : zItem.id);
        }
      }
      return regularIds;
    } else if (item.type === "tag") {
      return await Assistant.searchAllLibraries("tag", "is", item.displayName);
    } else if (item.type === "author") {
      return await Assistant.searchAllLibraries(
        "creator",
        "contains",
        item.displayName,
      );
    }

    return [];
  }

  private static removeItemWithNotes(itemId: number) {
    const stateManager = getChatStateManager();

    // Remove any notes that belong to this item
    const states = stateManager.getStates();
    const notesToRemove = states.notes.filter((n) => n.parentItemId === itemId);
    for (const note of notesToRemove) {
      stateManager.removeSelection("notes", note.id);
    }

    // Remove the item
    stateManager.removeSelection("items", itemId);

    // Re-render selection area
    this.reRenderSelectionArea();
  }

  /**
   * Session Management
   */

  public static async loadHistory() {
    try {
      conversationHistory = await getMessageStore().getHistory();
    } catch (e) {
      Zotero.debug(`[seerai] Error loading history: ${e}`);
    }
  }

  public static async createNewChat() {
    const store = getMessageStore();
    const newId = `chat_${Date.now()}`;

    const history = await store.getHistory();
    const currentId = store.getConversationId();
    const currentConv = history.find((h) => h.id === currentId);
    const folder = currentConv?.folder || null;

    // Save current state if any before switching
    if (conversationMessages.length > 0) {
      const stateManager = getChatStateManager();
      await store.saveConversationState(
        stateManager.getStates(),
        stateManager.getOptions(),
      );
    }

    store.setConversationId(newId);
    getWorkspaceStore().switchChatContext(newId, folder);
    conversationMessages = [];
    resetChatStateManager();
    ChatContextManager.getInstance().clearAll();
    await store.setContinuation(undefined);

    // Initial history entry for the new chat
    await store.updateConversationMetadata({
      id: newId,
      title: "New Chat",
      messageCount: 0,
      preview: "Start a new conversation...",
    });

    if (folder) {
      await store.moveConversationToFolder(newId, folder);
    }

    // Inherit drive context from previous chat
    if (currentId && currentId !== newId) {
      await inheritAndLoadDriveContext(currentId, newId);
    }

    await this.loadHistory();

    if (currentContainer && currentItem) {
      this.renderInterface(currentContainer, currentItem);
    }

    return newId;
  }

  public static async loadChat(id: string) {
    const store = getMessageStore();

    // Save current state before switching
    if (conversationMessages.length > 0) {
      const stateManager = getChatStateManager();
      await store.saveConversationState(
        stateManager.getStates(),
        stateManager.getOptions(),
      );
    }

    // Clear context for the new chat
    ChatContextManager.getInstance().clearAll();

    store.setConversationId(id);
    const history = await store.getHistory();
    const conv = history.find((h) => h.id === id);
    getWorkspaceStore().switchChatContext(id, conv?.folder || null);

    // Load messages
    conversationMessages = await store.loadMessages();

    // Load state
    const savedState = await store.getConversationState();
    resetChatStateManager();
    const stateManager = getChatStateManager();
    if (savedState) {
      stateManager.fromJSON(savedState);
    }

    // Load drive context files for this chat
    await loadDriveContextForChat(id);

    if (currentContainer && currentItem) {
      this.renderInterface(currentContainer, currentItem);
    }
  }

  public static async createNewChatInFolder(folder: string) {
    const store = getMessageStore();
    const newId = `chat_${Date.now()}`;

    const currentId = store.getConversationId();

    if (conversationMessages.length > 0) {
      const stateManager = getChatStateManager();
      await store.saveConversationState(
        stateManager.getStates(),
        stateManager.getOptions(),
      );
    }

    store.setConversationId(newId);
    getWorkspaceStore().switchChatContext(newId, folder);
    conversationMessages = [];
    resetChatStateManager();
    ChatContextManager.getInstance().clearAll();
    await store.setContinuation(undefined);

    await store.updateConversationMetadata({
      id: newId,
      title: "New Chat",
      messageCount: 0,
      preview: "Start a new conversation...",
    });

    await store.moveConversationToFolder(newId, folder);

    // Inherit drive context from previous chat
    if (currentId && currentId !== newId) {
      await inheritAndLoadDriveContext(currentId, newId);
    }

    await this.loadHistory();

    if (currentContainer && currentItem) {
      this.renderInterface(currentContainer, currentItem);
    }
  }

  public static async deleteChat(id: string) {
    const store = getMessageStore();

    const wasCurrentChat = store.getConversationId() === id;

    await store.deleteConversation(id);
    await clearDriveContextForChat(id);

    await this.loadHistory();

    if (wasCurrentChat) {
      if (conversationHistory.length > 0) {
        await this.loadChat(conversationHistory[0].id);
      } else {
        await this.createNewChat();
      }
    } else if (currentContainer && currentItem) {
      this.renderInterface(currentContainer, currentItem);
    }
  }

  public static async deleteChats(ids: string[]) {
    if (ids.length === 0) return;
    const store = getMessageStore();
    const currentId = store.getConversationId();
    const includesCurrent = ids.includes(currentId);

    for (const id of ids) {
      await store.deleteConversation(id);
    }

    await this.loadHistory();

    if (includesCurrent) {
      if (conversationHistory.length > 0) {
        await this.loadChat(conversationHistory[0].id);
      } else {
        await this.createNewChat();
      }
    } else if (currentContainer && currentItem) {
      this.renderInterface(currentContainer, currentItem);
    }
  }

  public static async updateConversationTitle(firstMessage: string) {
    const store = getMessageStore();
    const currentId = store.getConversationId();
    const history = await store.getHistory();
    const conv = history.find((h) => h.id === currentId);

    if (conv && (conv.title === "New Chat" || !conv.title)) {
      // Simple auto-titling: use first 30 chars of the message
      let newTitle = firstMessage.trim().slice(0, 40);
      if (firstMessage.length > 40) newTitle += "...";

      await store.updateConversationMetadata({
        id: currentId,
        title: newTitle,
      });

      await this.loadHistory();

      // Re-render to show new title in sidebar
      if (currentContainer && currentItem) {
        this.renderInterface(currentContainer, currentItem);
      }
    }
  }

  /**
   * Main interface renderer
   */
  private static async renderInterface(
    container: HTMLElement,
    item: Zotero.Item,
    skipIfSame: boolean = false,
  ) {
    const myGeneration = ++renderGeneration;

    const doRender = async () => {
      try {
        if (
          skipIfSame &&
          this.lastRenderedItemId === item.id &&
          this.lastRenderedTab === activeTab &&
          this.lastRenderedContainer === container
        ) {
          return;
        }

        if (isTableGenerating && activeTab === "table") {
          Zotero.debug(
            "[seerai] renderInterface: Skipping render during table generation",
          );
          return;
        }

        this.lastRenderedItemId = item.id;
        this.lastRenderedTab = activeTab;
        this.lastRenderedContainer = container;

        const isDetachedWindow = !!container.ownerDocument?.getElementById(
          "seerai-detached-window",
        );

        container.style.width = "100%";
        container.style.maxWidth = "100%";
        container.style.boxSizing = "border-box";
        container.style.minWidth = "0";

        if (!isDetachedWindow) {
          container.style.height = "calc(100vh - 150px)";
          container.style.overflow = "hidden";
        } else {
          container.style.height = "100%";
          container.style.overflow = "hidden";
        }

        // Save current scroll position for chat tab
        const messagesScrollContainer =
          container.querySelector("#messages-area");
        const savedScrollTop = messagesScrollContainer?.scrollTop ?? 0;

        // Build entire UI off-screen before swapping
        const doc = container.ownerDocument!;
        Zotero.debug(
          `[seerai] renderInterface: Starting render for item ${item.id} (gen=${myGeneration})`,
        );

        // Ensure stylesheet is loaded
        const styleId = "seerai-stylesheet";
        if (!doc.getElementById(styleId)) {
          const link = ztoolkit.UI.createElement(doc, "link", {
            properties: {
              id: styleId,
              type: "text/css",
              rel: "stylesheet",
              href: `chrome://${config.addonRef}/content/zoteroPane.css`,
            },
          });
          doc.documentElement?.appendChild(link);
        }

        const stateManager = getChatStateManager();

        if (conversationHistory.length === 0) {
          await this.loadHistory();
        }

        if (conversationMessages.length === 0) {
          try {
            const store = getMessageStore();
            if (
              conversationHistory.length > 0 &&
              store.getConversationId() === "default"
            ) {
              store.setConversationId(conversationHistory[0].id);
            }
            conversationMessages = await store.loadMessages();

            const savedState = await store.getConversationState();
            if (savedState) {
              stateManager.fromJSON(savedState);
            }

            Zotero.debug(
              `[seerai] Loaded ${conversationMessages.length} messages for session ${store.getConversationId()}`,
            );
          } catch (e) {
            Zotero.debug(`[seerai] Error loading messages: ${e}`);
          }
        }

        if (!currentTableConfig || activeTab === "table") {
          try {
            const tableStore = getTableStore();
            currentTableConfig = await tableStore.loadConfig();
            Zotero.debug(
              `[seerai] Loaded table config: ${currentTableConfig.id} (activeTab=${activeTab})`,
            );
          } catch (e) {
            Zotero.debug(`[seerai] Error loading table config: ${e}`);
          }
        }

        // Abandon this render if a newer one started
        if (renderGeneration !== myGeneration) {
          Zotero.debug(
            `[seerai] renderInterface: Stale render (gen=${myGeneration}), abandoning`,
          );
          return;
        }

        // Auto-add current item with its notes based on selection mode
        const options = stateManager.getOptions();
        const mode = options.selectionMode;

        if (mode === "explore") {
          if (!stateManager.isSelected("items", item.id)) {
            this.addItemWithNotes(item);
          }
        } else if (mode === "default") {
          if (!stateManager.isSelected("items", item.id)) {
            stateManager.clearAll();
            this.addItemWithNotes(item);
          }
        }

        // Main Container with tabs
        const mainContainer = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            display: "flex",
            flexDirection: "column",
            height: "100%",
            width: "100%",
            minWidth: "0",
            maxWidth: "100%",
            overflow: "hidden",
            boxSizing: "border-box",
            fontFamily: "system-ui, -apple-system, sans-serif",
          },
        });

        try {
          // === TAB BAR ===
          const tabBar = this.createTabBar(doc, container, item);
          mainContainer.appendChild(tabBar);

          // === TAB CONTENT CONTAINER ===
          const tabContent = ztoolkit.UI.createElement(doc, "div", {
            properties: { id: "tab-content" },
            styles: {
              flex: "1 1 0",
              display: "flex",
              flexDirection: "column",
              width: "100%",
              minWidth: "0",
              maxWidth: "100%",
              minHeight: "0",
              boxSizing: "border-box",
              overflow: "hidden",
            },
          });

          if (activeTab === "chat") {
            const chatTabContent = await this.createChatTabContent(
              doc,
              item,
              stateManager,
            );
            tabContent.appendChild(chatTabContent);
          } else if (activeTab === "cloud") {
            const cloudTabContent = await createCloudTabContent(doc, item);
            tabContent.appendChild(cloudTabContent);
          } else if (activeTab === "table") {
            const tableTabContent = await this.createTableTabContent(doc, item);
            tabContent.appendChild(tableTabContent);
          } else if (activeTab === "search") {
            const searchTabContent = await this.createSearchTabContent(
              doc,
              item,
            );
            tabContent.appendChild(searchTabContent);
          }

          // Final stale check before committing DOM
          if (renderGeneration !== myGeneration) {
            Zotero.debug(
              `[seerai] renderInterface: Stale render before commit (gen=${myGeneration}), abandoning`,
            );
            return;
          }

          mainContainer.appendChild(tabContent);

          // Atomic swap: replace container contents in a single operation
          // This prevents the flash of empty content between clearing and rebuilding
          container.replaceChildren(mainContainer);

          // Restore scroll position for chat tab
          if (activeTab === "chat" && savedScrollTop > 0) {
            setTimeout(() => {
              const newMessagesArea = container.querySelector("#messages-area");
              if (newMessagesArea) {
                (newMessagesArea as HTMLElement).scrollTop = savedScrollTop;
              }
            }, 0);
          }

          Zotero.debug("[seerai] renderInterface: Render complete");
        } catch (error) {
          Zotero.debug(`[seerai] Error building UI components: ${error}`);

          // Show error in container
          try {
            const errorDiv = container.ownerDocument!.createElement("div");
            errorDiv.style.cssText =
              "padding: 20px; color: red; overflow: auto; height: 100%;";
            errorDiv.textContent = `Error rendering interface: ${error}\n\nStack: ${(error as Error).stack || "N/A"}`;
            container.replaceChildren(errorDiv);
          } catch {
            // Last resort
          }
        }
      } catch (error) {
        Zotero.debug(`[seerai] Error rendering interface (outer): ${error}`);
      }
    };

    const renderPromise = doRender();
    renderLock = renderLock.then(() => renderPromise);
    return renderPromise;
  }

  /**
   * Create the tab bar navigation
   */
  private static createTabBar(
    doc: Document,
    container: HTMLElement,
    item: Zotero.Item,
  ): HTMLElement {
    const tabBar = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "tab-bar" },
      styles: {
        display: "flex",
        gap: "0",
        flexShrink: "0",
        borderBottom: "1px solid var(--border-primary)",
        backgroundColor: "var(--background-secondary)",
        borderRadius: "6px 6px 0 0",
        overflow: "hidden",
      },
    });

    const tabs: { id: AssistantTab; label: string; icon: string }[] = [
      { id: "chat", label: "Chat", icon: "💬" },
      { id: "table", label: "Table", icon: "📊" },
      { id: "search", label: "Search", icon: "🔍" },
      { id: "cloud", label: "Cloud", icon: "☁️" },
    ];

    tabs.forEach((tab) => {
      const tabItem = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: {
          className: `tab-item ${activeTab === tab.id ? "active" : ""}`,
          innerText: `${tab.icon} ${tab.label}`,
        },
        styles: {
          padding: "4px 12px",
          cursor: "pointer",
          fontSize: "12px",
          fontWeight: "500",
          color:
            activeTab === tab.id
              ? "var(--highlight-primary)"
              : "var(--text-secondary)",
          backgroundColor:
            activeTab === tab.id ? "var(--background-primary)" : "transparent",
          border: "none",
          borderBottom:
            activeTab === tab.id
              ? "2px solid var(--highlight-primary)"
              : "2px solid transparent",
          flex: "1",
          textAlign: "center",
          transition: "all 0.2s ease",
        },
        listeners: [
          {
            type: "click",
            listener: () => {
              if (activeTab !== tab.id && currentItem) {
                activeTab = tab.id;
                this.renderInterface(container, currentItem);
              }
            },
          },
        ],
      });
      tabBar.appendChild(tabItem);
    });

    // Add detach button at the end
    const detachBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        className: "detach-btn",
        title: "Pop out to window (Ctrl+Shift+S)",
        innerText: "⇱",
      },
      styles: {
        padding: "4px 10px",
        cursor: "pointer",
        fontSize: "14px",
        fontWeight: "500",
        color: "var(--text-secondary)",
        backgroundColor: "transparent",
        border: "none",
        borderBottom: "2px solid transparent",
        transition: "all 0.2s ease",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            DetachedWindowManager.detach();
          },
        },
        {
          type: "mouseenter",
          listener: (e: Event) => {
            const btn = e.target as HTMLElement;
            btn.style.color = "var(--highlight-primary)";
            btn.style.backgroundColor = "var(--background-primary)";
          },
        },
        {
          type: "mouseleave",
          listener: (e: Event) => {
            const btn = e.target as HTMLElement;
            btn.style.color = "var(--text-secondary)";
            btn.style.backgroundColor = "transparent";
          },
        },
      ],
    });
    tabBar.appendChild(detachBtn);

    return tabBar;
  }

  /**
   * Create the History Sidebar with folder/space organization.
   */
  private static async createHistorySidebar(
    doc: Document,
  ): Promise<HTMLElement> {
    const store = getMessageStore();
    const history = conversationHistory;
    const folders = await store.getFolders();
    const currentId = store.getConversationId();

    const sidebar = doc.createElement("div");
    sidebar.className = "history-sidebar";
    sidebar.style.cssText = `
      width: ${isHistorySidebarVisible ? "clamp(120px, 40%, 250px)" : "0px"};
      min-width: ${isHistorySidebarVisible ? "120px" : "20px"};
      flex-shrink: ${isHistorySidebarVisible ? "0" : "0"};
      display: flex;
      flex-direction: column;
      border-right: 1px solid var(--border-primary);
      background-color: var(--background-secondary);
      transition: width 0.3s ease, min-width 0.3s ease;
      overflow: hidden;
      height: 100%;
      box-sizing: border-box;
    `;

    // Header — matches workspace (ARTIFACTS) sidebar style
    const header = doc.createElement("div");
    header.style.cssText = `
      padding: 8px 12px;
      border-bottom: 1px solid var(--border-primary);
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-shrink: 0;
    `;

    const headerTitle = doc.createElement("span");
    headerTitle.textContent = "Chats";
    headerTitle.style.cssText = `
      font-size: 11px;
      font-weight: 700;
      color: var(--text-primary);
      text-transform: uppercase;
      letter-spacing: 0.8px;
    `;
    header.appendChild(headerTitle);

    // Collapse with ◀ to hide sidebar (it's on left side, collapses leftward)
    const collapseHistoryBtn = createIconButton(
      doc,
      "\u25C0",
      "Hide sidebar",
      () => {
        isHistorySidebarVisible = false;
        sidebar.style.width = "20px";
        sidebar.style.minWidth = "20px";
        sidebar.style.borderRight = "1px solid var(--border-primary)";
        collapseHistoryBtn.textContent = "\u25B6";
        collapseHistoryBtn.title = "Show sidebar";
        header.style.display = "none";
        newChatBar.style.display = "none";
        if (list) list.style.display = "none";
        const historyHandle = sidebar.nextElementSibling as HTMLElement | null;
        if (historyHandle?.classList.contains("seerai-resize-handle")) {
          historyHandle.style.display = "none";
        }
        const narrowToggle = sidebar.querySelector(
          ".narrow-toggle",
        ) as HTMLElement | null;
        if (!narrowToggle) {
          const strip = doc.createElement("div");
          strip.className = "narrow-toggle";
          strip.style.cssText =
            "display: flex; flex-direction: column; align-items: center; padding: 8px 0; gap: 8px;";
          const expandBtn = createIconButton(
            doc,
            "\u25B6",
            "Show sidebar",
            () => {
              isHistorySidebarVisible = true;
              sidebar.style.width = "clamp(120px, 40%, 250px)";
              sidebar.style.minWidth = "120px";
              sidebar.style.borderRight = "1px solid var(--border-primary)";
              collapseHistoryBtn.textContent = "\u25C0";
              collapseHistoryBtn.title = "Hide sidebar";
              header.style.display = "flex";
              newChatBar.style.display = "flex";
              if (list) list.style.display = "";
              const hHandle = sidebar.nextElementSibling as HTMLElement | null;
              if (hHandle?.classList.contains("seerai-resize-handle")) {
                hHandle.style.display = "";
              }
              strip.remove();
            },
          );
          strip.appendChild(expandBtn);
          const label = doc.createElement("span");
          label.textContent = "C";
          label.style.cssText =
            "writing-mode: vertical-lr; font-size: 9px; font-weight: 700; color: var(--text-tertiary); letter-spacing: 2px;";
          strip.appendChild(label);
          sidebar.appendChild(strip);
        }
      },
    );
    header.appendChild(collapseHistoryBtn);

    sidebar.appendChild(header);

    // New chat / folder action bar below header
    const newChatBar = doc.createElement("div");
    newChatBar.style.cssText = `
      display: flex;
      gap: 4px;
      padding: 6px 8px;
      border-bottom: 1px solid var(--border-primary);
      flex-shrink: 0;
    `;

    const newChatBtn = doc.createElementNS(
      HTML_NS,
      "button",
    ) as HTMLButtonElement;
    newChatBtn.textContent = "+ New Chat";
    newChatBtn.style.cssText = `
      flex: 1;
      padding: 5px 8px;
      border-radius: 4px;
      background: var(--highlight-primary);
      color: white;
      border: none;
      cursor: pointer;
      font-size: 11px;
      font-weight: 500;
      transition: opacity 0.2s;
    `;
    newChatBtn.onclick = () => this.createNewChat();
    newChatBar.appendChild(newChatBtn);

    const newFolderBtn = doc.createElementNS(
      HTML_NS,
      "button",
    ) as HTMLButtonElement;
    newFolderBtn.textContent = "+ Folder";
    newFolderBtn.style.cssText = `
      flex: 1;
      padding: 5px 8px;
      border-radius: 4px;
      background: var(--background-primary);
      color: var(--text-secondary);
      border: 1px solid var(--border-primary);
      cursor: pointer;
      font-size: 11px;
      transition: all 0.2s;
    `;
    newFolderBtn.onclick = () => {
      // Remove any existing inline form
      const existing = doc.getElementById("folder-creation-form");
      if (existing) {
        existing.remove();
        return;
      }

      // Also remove the new-file inline if present
      const commitDropdown = doc.getElementById("commit-dropdown");
      if (commitDropdown) commitDropdown.remove();

      const form = doc.createElement("div");
      form.id = "folder-creation-form";
      form.style.cssText = `
        padding: 6px 8px;
        border-bottom: 1px solid var(--border-primary);
        background: var(--background-secondary);
        display: flex;
        flex-direction: column;
        gap: 4px;
        flex-shrink: 0;
      `;

      const nameInput = doc.createElement("input");
      nameInput.type = "text";
      nameInput.placeholder = "Folder name";
      nameInput.style.cssText = `
        width: 100%; padding: 4px 6px; font-size: 11px; font-family: inherit;
        border: 1px solid var(--border-primary); border-radius: 4px;
        background: var(--background-primary); color: var(--text-primary);
        outline: none; box-sizing: border-box;
      `;

      const pathInput = doc.createElement("input");
      pathInput.type = "text";
      pathInput.placeholder =
        "Workspace path (optional, defaults to folder name)";
      pathInput.style.cssText = `
        width: 100%; padding: 4px 6px; font-size: 10px; font-family: inherit;
        border: 1px solid var(--border-primary); border-radius: 4px;
        background: var(--background-primary); color: var(--text-secondary);
        outline: none; box-sizing: border-box;
      `;

      const btnRow = doc.createElement("div");
      btnRow.style.cssText =
        "display: flex; gap: 4px; justify-content: flex-end;";

      const cancelBtn = doc.createElementNS(
        HTML_NS,
        "button",
      ) as HTMLButtonElement;
      cancelBtn.textContent = "Cancel";
      cancelBtn.style.cssText = `
        padding: 2px 8px; font-size: 10px;
        border: 1px solid var(--border-primary); border-radius: 3px;
        background: transparent; color: var(--text-secondary); cursor: pointer;
      `;
      cancelBtn.addEventListener("click", () => form.remove());

      const createBtn = doc.createElementNS(
        HTML_NS,
        "button",
      ) as HTMLButtonElement;
      createBtn.textContent = "Create";
      createBtn.style.cssText = `
        padding: 2px 8px; font-size: 10px;
        border: 1px solid var(--border-primary); border-radius: 3px;
        background: var(--highlight-primary); color: white; cursor: pointer;
      `;

      const doCreate = async () => {
        const name = nameInput.value.trim();
        if (!name) return;
        const workspacePath = pathInput.value.trim() || undefined;
        const currentId = store.getConversationId();
        await store.moveConversationToFolder(currentId, name);
        if (workspacePath) {
          try {
            const { getWorkspaceStore } =
              await import("./chat/workspace/store");
            getWorkspaceStore().setFolderCustomPath(name, workspacePath);
          } catch (e) {
            Zotero.debug(`[seerai] Error saving workspace path mapping: ${e}`);
          }
        }
        form.remove();
        await this.loadHistory();
        await this.renderInterfaceSidebarsOnly();
      };

      nameInput.addEventListener("keydown", (e: KeyboardEvent) => {
        if (e.key === "Enter") doCreate();
        if (e.key === "Escape") form.remove();
      });
      createBtn.addEventListener("click", doCreate);

      btnRow.appendChild(cancelBtn);
      btnRow.appendChild(createBtn);
      form.appendChild(nameInput);
      form.appendChild(pathInput);
      form.appendChild(btnRow);

      // Insert after newChatBar, before list
      if (newChatBar.nextSibling) {
        sidebar.insertBefore(form, newChatBar.nextSibling);
      } else {
        sidebar.appendChild(form);
      }

      nameInput.focus();
    };
    newChatBar.appendChild(newFolderBtn);

    sidebar.appendChild(newChatBar);

    // Scrollable list
    const list = doc.createElement("div");
    list.style.cssText =
      "flex: 1; overflow-y: auto; display: flex; flex-direction: column; min-height: 0;";

    // Separate archived from active conversations
    const active: ConversationMetadata[] = [];
    const archived: ConversationMetadata[] = [];
    for (const conv of history) {
      if (conv.archived) {
        archived.push(conv);
      } else {
        active.push(conv);
      }
    }

    // Group active conversations by folder
    const unfolded: ConversationMetadata[] = [];
    const folderMap = new Map<string, ConversationMetadata[]>();
    for (const conv of active) {
      if (conv.folder) {
        const group = folderMap.get(conv.folder) || [];
        group.push(conv);
        folderMap.set(conv.folder, group);
      } else {
        unfolded.push(conv);
      }
    }

    // Build visual order of all chat IDs (folders sorted → unfiled → archived)
    const sortedFolders = Array.from(folderMap.keys()).sort();
    const visualChatOrder: string[] = [];
    for (const folderName of sortedFolders) {
      for (const conv of folderMap.get(folderName)!) {
        visualChatOrder.push(conv.id);
      }
    }
    for (const conv of unfolded) {
      visualChatOrder.push(conv.id);
    }
    for (const conv of archived) {
      visualChatOrder.push(conv.id);
    }

    // Render folder sections
    for (const folderName of sortedFolders) {
      const folderConvs = folderMap.get(folderName)!;
      const folderSection = this.createFolderSection(
        doc,
        folderName,
        folderConvs,
        currentId,
        visualChatOrder,
        false,
      );
      list.appendChild(folderSection);
    }

    // Unfiled section — always visible as a drop target
    {
      const unfiledSection = this.createFolderSection(
        doc,
        "Unfiled",
        unfolded,
        currentId,
        visualChatOrder,
        true,
      );
      list.appendChild(unfiledSection);
    }

    if (active.length === 0 && archived.length === 0) {
      const empty = doc.createElement("div");
      empty.textContent = "No chats yet";
      empty.style.cssText =
        "padding: 20px; text-align: center; color: var(--text-tertiary); font-style: italic; font-size: 12px;";
      list.appendChild(empty);
    }

    // Archived section (collapsed by default, no folders, just flat list)
    if (archived.length > 0) {
      const archivedSection = doc.createElement("div");

      const archHeader = doc.createElement("div");
      archHeader.style.cssText = `
        display: flex;
        align-items: center;
        padding: 6px 8px;
        cursor: pointer;
        font-size: 11px;
        font-weight: 600;
        color: var(--text-tertiary);
        text-transform: uppercase;
        letter-spacing: 0.5px;
        gap: 6px;
        user-select: none;
      `;

      const archArrow = doc.createElement("span");
      archArrow.textContent = "\u25B8";
      archArrow.style.cssText = "font-size: 10px; transition: transform 0.2s;";

      const archLabel = doc.createElement("span");
      archLabel.textContent = `Archived (${archived.length})`;
      archLabel.style.cssText =
        "flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;";

      archHeader.appendChild(archArrow);
      archHeader.appendChild(archLabel);

      const archItems = doc.createElement("div");
      archItems.style.cssText = "display: none;";

      for (const conv of archived) {
        const item = doc.createElement("div");
        const isActive = conv.id === currentId;
        const isSelected = selectedChatIds.has(conv.id);
        item.style.cssText = `
          padding: 6px 8px 6px ${isSelected ? "6px" : "20px"};
          cursor: pointer;
          font-size: 12px;
          color: ${isSelected ? "white" : isActive ? "var(--text-primary)" : "var(--text-secondary)"};
          background: ${isSelected ? "var(--highlight-primary, #4a90d9)" : isActive ? "var(--background-primary)" : "transparent"};
          border-left: ${isActive ? "3px solid var(--highlight-primary)" : "3px solid transparent"};
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          opacity: ${isSelected ? "1" : "0.7"};
          transition: background 0.15s;
          display: flex;
          align-items: center;
          gap: 6px;
        `;

        if (isSelected) {
          const check = doc.createElementNS(HTML_NS, "span") as HTMLElement;
          check.textContent = "\u2713";
          check.style.cssText = `
            font-size: 12px;
            font-weight: 700;
            flex-shrink: 0;
            width: 14px;
            text-align: center;
            color: white;
          `;
          item.appendChild(check);
        }

        const title = doc.createElement("div");
        title.textContent = conv.title || "Untitled";
        title.style.cssText =
          "overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0;";
        item.appendChild(title);

        item.addEventListener("click", (e: MouseEvent) => {
          const ctrl = e.ctrlKey || e.metaKey;
          const shift = e.shiftKey;

          if (ctrl && !shift) {
            e.preventDefault();
            e.stopPropagation();
            if (selectedChatIds.has(conv.id)) {
              selectedChatIds.delete(conv.id);
            } else {
              selectedChatIds.add(conv.id);
            }
            lastClickedChatId = conv.id;
            if (currentContainer && currentItem) {
              Assistant.renderInterface(currentContainer, currentItem);
            }
          } else if (shift && lastClickedChatId) {
            e.preventDefault();
            e.stopPropagation();
            const startIdx = visualChatOrder.indexOf(lastClickedChatId);
            const endIdx = visualChatOrder.indexOf(conv.id);
            if (startIdx !== -1 && endIdx !== -1) {
              const lo = Math.min(startIdx, endIdx);
              const hi = Math.max(startIdx, endIdx);
              for (let i = lo; i <= hi; i++) {
                selectedChatIds.add(visualChatOrder[i]);
              }
              lastClickedChatId = conv.id;
              if (currentContainer && currentItem) {
                Assistant.renderInterface(currentContainer, currentItem);
              }
            }
          } else if (selectedChatIds.size > 0 && !ctrl && !shift) {
            e.preventDefault();
            e.stopPropagation();
            selectedChatIds.clear();
            lastClickedChatId = conv.id;
            if (currentContainer && currentItem) {
              Assistant.renderInterface(currentContainer, currentItem);
            }
          } else {
            lastClickedChatId = conv.id;
            Assistant.loadChat(conv.id);
          }
        });

        item.addEventListener("contextmenu", async (e: MouseEvent) => {
          e.preventDefault();
          e.stopPropagation();
          await Assistant.showHistoryContextMenu(
            doc,
            conv,
            e.clientX,
            e.clientY,
          );
        });
        item.onmouseenter = () => {
          item.style.background = isSelected
            ? "var(--highlight-primary, #4a90d9)"
            : "var(--background-hover, rgba(0,0,0,0.05))";
        };
        item.onmouseleave = () => {
          item.style.background = isSelected
            ? "var(--highlight-primary, #4a90d9)"
            : isActive
              ? "var(--background-primary)"
              : "transparent";
        };

        archItems.appendChild(item);
      }

      archHeader.onclick = () => {
        const collapsed = archItems.style.display === "none";
        archItems.style.display = collapsed ? "block" : "none";
        archArrow.textContent = collapsed ? "\u25BE" : "\u25B8";
      };

      archivedSection.appendChild(archHeader);
      archivedSection.appendChild(archItems);
      list.appendChild(archivedSection);
    }

    sidebar.appendChild(list);

    // Bulk action bar (visible when items are selected)
    if (selectedChatIds.size > 0) {
      const actionBar = doc.createElement("div");
      actionBar.style.cssText = `
        padding: 6px 8px;
        border-top: 1px solid var(--border-primary);
        display: flex;
        flex-wrap: wrap;
        gap: 4px;
        flex-shrink: 0;
        background: var(--background-secondary);
        position: relative;
        align-items: center;
      `;

      const countLabel = doc.createElement("span");
      countLabel.textContent = `${selectedChatIds.size}`;
      countLabel.title = `${selectedChatIds.size} selected`;
      countLabel.style.cssText = `
        font-size: 11px;
        font-weight: 600;
        color: var(--highlight-primary);
        display: flex;
        align-items: center;
        padding: 0 4px;
        flex-shrink: 0;
      `;
      actionBar.appendChild(countLabel);

      const allFolders = await store.getFolders();

      const deleteBtn = doc.createElementNS(
        HTML_NS,
        "button",
      ) as HTMLButtonElement;
      deleteBtn.textContent = "Delete";
      deleteBtn.style.cssText = `
        padding: 3px 6px;
        border-radius: 4px;
        background: #d32f2f;
        color: white;
        border: none;
        cursor: pointer;
        font-size: 10px;
        font-weight: 500;
        white-space: nowrap;
      `;
      deleteBtn.onclick = async () => {
        if (
          !doc.defaultView?.confirm(
            `Delete ${selectedChatIds.size} conversation(s)?`,
          )
        )
          return;
        const ids = Array.from(selectedChatIds);
        selectedChatIds.clear();
        await Assistant.deleteChats(ids);
      };
      actionBar.appendChild(deleteBtn);

      const archiveBtn = doc.createElementNS(
        HTML_NS,
        "button",
      ) as HTMLButtonElement;
      archiveBtn.textContent = "Archive";
      archiveBtn.style.cssText = `
        padding: 3px 6px;
        border-radius: 4px;
        background: var(--background-primary);
        color: var(--text-secondary);
        border: 1px solid var(--border-primary);
        cursor: pointer;
        font-size: 10px;
        white-space: nowrap;
      `;
      archiveBtn.onclick = async () => {
        const ids = Array.from(selectedChatIds);
        for (const id of ids) {
          await store.archiveConversation(id);
        }
        selectedChatIds.clear();
        await Assistant.loadHistory();
        if (currentContainer && currentItem) {
          Assistant.renderInterface(currentContainer, currentItem);
        }
      };
      actionBar.appendChild(archiveBtn);

      // Move to folder button (only if folders exist)
      if (allFolders.length > 0) {
        const moveBtn = doc.createElementNS(
          HTML_NS,
          "button",
        ) as HTMLButtonElement;
        moveBtn.textContent = "Move\u2026";
        moveBtn.style.cssText = `
          padding: 3px 6px;
          border-radius: 4px;
          background: var(--background-primary);
          color: var(--text-secondary);
          border: 1px solid var(--border-primary);
          cursor: pointer;
          font-size: 10px;
          white-space: nowrap;
        `;

        const moveMenu = doc.createElement("div");
        moveMenu.style.cssText = `
          position: absolute;
          bottom: 100%;
          left: 0;
          background: var(--background-primary);
          border: 1px solid var(--border-primary);
          border-radius: 6px;
          box-shadow: 0 4px 12px rgba(0,0,0,0.15);
          padding: 4px 0;
          min-width: 140px;
          max-width: 200px;
          font-size: 12px;
          z-index: 99999;
          display: none;
        `;

        for (const folder of allFolders) {
          const item = doc.createElement("div");
          item.textContent = `\u2192 ${folder}`;
          item.style.cssText =
            "padding: 6px 12px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;";
          item.onmouseenter = () => {
            item.style.background = "var(--background-hover, rgba(0,0,0,0.05))";
          };
          item.onmouseleave = () => {
            item.style.background = "transparent";
          };
          item.onclick = async () => {
            const ids = Array.from(selectedChatIds);
            for (const id of ids) {
              await store.moveConversationToFolder(id, folder);
            }
            selectedChatIds.clear();
            moveMenu.remove();
            await Assistant.loadHistory();
            if (currentContainer && currentItem) {
              Assistant.renderInterface(currentContainer, currentItem);
            }
          };
          moveMenu.appendChild(item);
        }

        moveBtn.onclick = (e) => {
          e.stopPropagation();
          moveMenu.style.display =
            moveMenu.style.display === "none" ? "block" : "none";
        };
        // Close menu on outside click
        const closeMoveMenu = (e2: Event) => {
          if (!moveMenu.contains(e2.target as Node)) {
            moveMenu.style.display = "none";
          }
        };
        doc.addEventListener("click", closeMoveMenu);
        actionBar.appendChild(moveBtn);
        actionBar.appendChild(moveMenu);
      }

      const selectAllBtn = doc.createElementNS(
        HTML_NS,
        "button",
      ) as HTMLButtonElement;
      selectAllBtn.textContent = "All";
      selectAllBtn.title = "Select all chats";
      selectAllBtn.style.cssText = `
        padding: 3px 6px;
        border-radius: 4px;
        background: var(--background-primary);
        color: var(--text-secondary);
        border: 1px solid var(--border-primary);
        cursor: pointer;
        font-size: 10px;
        white-space: nowrap;
      `;
      selectAllBtn.onclick = () => {
        for (const conv of conversationHistory) {
          selectedChatIds.add(conv.id);
        }
        if (currentContainer && currentItem) {
          Assistant.renderInterface(currentContainer, currentItem);
        }
      };
      actionBar.appendChild(selectAllBtn);

      const cancelBtn = doc.createElementNS(
        HTML_NS,
        "button",
      ) as HTMLButtonElement;
      cancelBtn.textContent = "\u2715";
      cancelBtn.title = "Cancel selection";
      cancelBtn.style.cssText = `
        padding: 3px 6px;
        border-radius: 4px;
        background: var(--background-primary);
        color: var(--text-secondary);
        border: 1px solid var(--border-primary);
        cursor: pointer;
        font-size: 10px;
        white-space: nowrap;
      `;
      cancelBtn.onclick = () => {
        selectedChatIds.clear();
        if (currentContainer && currentItem) {
          Assistant.renderInterface(currentContainer, currentItem);
        }
      };
      actionBar.appendChild(cancelBtn);

      sidebar.appendChild(actionBar);
    }

    if (!isHistorySidebarVisible) {
      header.style.display = "none";
      newChatBar.style.display = "none";
      list.style.display = "none";
      const strip = doc.createElement("div");
      strip.className = "narrow-toggle";
      strip.style.cssText =
        "display: flex; flex-direction: column; align-items: center; padding: 8px 0; gap: 8px;";
      const expandBtn = createIconButton(doc, "\u25B6", "Show sidebar", () => {
        isHistorySidebarVisible = true;
        sidebar.style.width = "clamp(120px, 40%, 250px)";
        sidebar.style.minWidth = "120px";
        sidebar.style.borderRight = "1px solid var(--border-primary)";
        collapseHistoryBtn.textContent = "\u25C0";
        collapseHistoryBtn.title = "Hide sidebar";
        header.style.display = "flex";
        newChatBar.style.display = "flex";
        list.style.display = "";
        const historyHandle = sidebar.nextElementSibling as HTMLElement | null;
        if (historyHandle?.classList.contains("seerai-resize-handle")) {
          historyHandle.style.display = "";
        }
        strip.remove();
      });
      strip.appendChild(expandBtn);
      const label = doc.createElement("span");
      label.textContent = "C";
      label.style.cssText =
        "writing-mode: vertical-lr; font-size: 9px; font-weight: 700; color: var(--text-tertiary); letter-spacing: 2px;";
      strip.appendChild(label);
      sidebar.appendChild(strip);
    }

    return sidebar;
  }

  /**
   * Create a collapsible folder section in the history sidebar.
   */
  private static createFolderSection(
    doc: Document,
    folderName: string,
    conversations: ConversationMetadata[],
    currentId: string,
    visualChatOrder: string[],
    isUnfiled: boolean = false,
  ): HTMLElement {
    const section = doc.createElement("div");
    section.dataset.folder = folderName;

    // Allow dropping conversations onto this folder
    section.addEventListener("dragover", (e) => {
      e.preventDefault();
      (e as DragEvent).dataTransfer!.dropEffect = "move";
      section.style.outline = "2px dashed var(--highlight-primary)";
      section.style.outlineOffset = "-2px";
    });
    section.addEventListener("dragleave", () => {
      section.style.outline = "";
    });
    section.addEventListener("drop", async (e) => {
      e.preventDefault();
      section.style.outline = "";
      const convId = (e as DragEvent).dataTransfer?.getData("text/plain");
      if (!convId) return;
      const store = getMessageStore();
      if (isUnfiled) {
        await store.removeConversationFromFolder(convId);
      } else {
        await store.moveConversationToFolder(convId, folderName);
      }
      await Assistant.loadHistory();
      if (currentContainer && currentItem) {
        Assistant.renderInterface(currentContainer, currentItem);
      }
    });

    // Folder header
    const header = doc.createElement("div");
    header.style.cssText = `
      display: flex;
      align-items: center;
      padding: 6px 8px;
      cursor: pointer;
      font-size: 11px;
      font-weight: 600;
      color: var(--text-secondary);
      text-transform: uppercase;
      letter-spacing: 0.5px;
      gap: 6px;
      user-select: none;
    `;

    const arrow = doc.createElement("span");
    arrow.textContent = "▾";
    arrow.style.cssText = "font-size: 10px; transition: transform 0.2s;";

    const label = doc.createElement("span");
    label.textContent = `${folderName} (${conversations.length})`;
    label.style.cssText =
      "flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;";

    header.appendChild(arrow);
    header.appendChild(label);

    if (!isUnfiled) {
      const newChatInFolderBtn = doc.createElement("span");
      newChatInFolderBtn.textContent = "+";
      newChatInFolderBtn.title = `New chat in "${folderName}"`;
      newChatInFolderBtn.style.cssText =
        "font-size: 14px; cursor: pointer; opacity: 0.5; padding: 0 2px; font-weight: 700; color: var(--highlight-primary, #4a90d9);";
      newChatInFolderBtn.addEventListener("click", async (e) => {
        e.stopPropagation();
        await Assistant.createNewChatInFolder(folderName);
      });
      header.appendChild(newChatInFolderBtn);

      const renameFolderBtn = doc.createElement("span");
      renameFolderBtn.textContent = "\u270E";
      renameFolderBtn.title = `Rename folder "${folderName}"`;
      renameFolderBtn.style.cssText =
        "font-size: 12px; cursor: pointer; opacity: 0.5; padding: 0 4px;";
      renameFolderBtn.addEventListener("click", async (e) => {
        e.stopPropagation();
        // Inline rename: turn label into an input
        const currentText = label.textContent || "";
        const parenIdx = currentText.lastIndexOf(" (");
        const bareName =
          parenIdx > 0 ? currentText.substring(0, parenIdx) : folderName;

        const input = doc.createElement("input");
        input.type = "text";
        input.value = bareName;
        input.style.cssText = `
          flex: 1; padding: 1px 4px; font-size: 11px; font-family: inherit;
          border: 1px solid var(--highlight-primary); border-radius: 3px;
          background: var(--background-primary); color: var(--text-primary);
          outline: none; min-width: 0;
        `;

        const originalContent = label.textContent;
        label.textContent = "";
        label.appendChild(input);
        input.focus();
        input.select();

        const commitRename = async () => {
          const newName = input.value.trim();
          if (!newName || newName === bareName) {
            label.textContent = originalContent;
            return;
          }
          try {
            const store = getMessageStore();
            const history = await store.getHistory();
            for (const conv of history) {
              if (conv.folder === folderName) {
                await store.moveConversationToFolder(conv.id, newName);
              }
            }
            try {
              const { getWorkspaceStore } =
                await import("./chat/workspace/store");
              const ws = getWorkspaceStore();
              // Carry over custom path mapping
              const paths = JSON.parse(getPref("workspaceFolderPaths") || "{}");
              const oldSlug = folderName
                .toLowerCase()
                .replace(/[^a-z0-9]+/g, "-")
                .replace(/^-|-$/g, "");
              const newSlug = newName
                .toLowerCase()
                .replace(/[^a-z0-9]+/g, "-")
                .replace(/^-|-$/g, "");
              if (paths[oldSlug]) {
                paths[newSlug] = paths[oldSlug];
                delete paths[oldSlug];
                setPref("workspaceFolderPaths", JSON.stringify(paths));
              }
              await ws.renameWorkspaceFolder(folderName, newName);
            } catch {
              // best-effort
            }
            Assistant.loadHistory();
            if (currentContainer && currentItem) {
              Assistant.renderInterface(currentContainer, currentItem);
            }
          } catch (e: any) {
            Zotero.debug(`[seerai] Error renaming folder: ${e}`);
            label.textContent = originalContent;
          }
        };

        input.addEventListener("keydown", (ev: KeyboardEvent) => {
          if (ev.key === "Enter") {
            ev.preventDefault();
            commitRename();
          } else if (ev.key === "Escape") {
            label.textContent = originalContent;
          }
          ev.stopPropagation();
        });
        input.addEventListener("blur", () => {
          // Small delay so click on the input itself works before blur
          setTimeout(() => {
            if (input.parentNode) commitRename();
          }, 150);
        });
      });
      header.appendChild(renameFolderBtn);

      const deleteFolderBtn = doc.createElement("span");
      deleteFolderBtn.textContent = "×";
      deleteFolderBtn.title = `Remove folder "${folderName}" (chats will become unfiled)`;
      deleteFolderBtn.style.cssText =
        "font-size: 14px; cursor: pointer; opacity: 0.5; padding: 0 4px;";
      deleteFolderBtn.addEventListener("click", async (e) => {
        e.stopPropagation();
        if (
          !doc.defaultView?.confirm(
            `Remove folder "${folderName}"? Chats will become unfiled and the shared workspace will be deleted.`,
          )
        )
          return;
        const store = getMessageStore();
        for (const conv of conversations) {
          await store.removeConversationFromFolder(conv.id);
        }
        try {
          const { getWorkspaceStore } = await import("./chat/workspace/store");
          await getWorkspaceStore().deleteWorkspaceFolder(folderName);
        } catch {
          // best-effort
        }
        await Assistant.loadHistory();
        if (currentContainer && currentItem) {
          Assistant.renderInterface(currentContainer, currentItem);
        }
      });
      header.appendChild(deleteFolderBtn);
    }

    // Conversation items container (initially expanded)
    const itemsContainer = doc.createElement("div");

    header.onclick = () => {
      const collapsed = itemsContainer.style.display === "none";
      itemsContainer.style.display = collapsed ? "block" : "none";
      arrow.textContent = collapsed ? "▾" : "▸";
    };

    // Render conversations
    for (const conv of conversations) {
      const item = doc.createElement("div");
      const isActive = conv.id === currentId;
      const isSelected = selectedChatIds.has(conv.id);
      item.style.cssText = `
        padding: 6px 8px 6px ${isSelected ? "6px" : "20px"};
        cursor: pointer;
        font-size: 12px;
        color: ${isSelected ? "white" : isActive ? "var(--text-primary)" : "var(--text-secondary)"};
        background: ${isSelected ? "var(--highlight-primary, #4a90d9)" : isActive ? "var(--background-primary)" : "transparent"};
        border-left: ${isActive ? "3px solid var(--highlight-primary)" : "3px solid transparent"};
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        position: relative;
        transition: background 0.15s;
        display: flex;
        align-items: center;
        gap: 6px;
      `;

      if (isSelected) {
        const check = doc.createElementNS(HTML_NS, "span") as HTMLElement;
        check.textContent = "\u2713";
        check.style.cssText = `
          font-size: 12px;
          font-weight: 700;
          flex-shrink: 0;
          width: 14px;
          text-align: center;
          color: white;
        `;
        item.appendChild(check);
      }

      const title = doc.createElement("div");
      title.textContent = conv.title || "Untitled";
      title.style.cssText =
        "overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0;";
      item.appendChild(title);

      item.addEventListener("click", (e: MouseEvent) => {
        const ctrl = e.ctrlKey || e.metaKey;
        const shift = e.shiftKey;

        if (ctrl && !shift) {
          e.preventDefault();
          e.stopPropagation();
          if (selectedChatIds.has(conv.id)) {
            selectedChatIds.delete(conv.id);
          } else {
            selectedChatIds.add(conv.id);
          }
          lastClickedChatId = conv.id;
          if (currentContainer && currentItem) {
            Assistant.renderInterface(currentContainer, currentItem);
          }
        } else if (shift && lastClickedChatId) {
          e.preventDefault();
          e.stopPropagation();
          const startIdx = visualChatOrder.indexOf(lastClickedChatId);
          const endIdx = visualChatOrder.indexOf(conv.id);
          if (startIdx !== -1 && endIdx !== -1) {
            const lo = Math.min(startIdx, endIdx);
            const hi = Math.max(startIdx, endIdx);
            for (let i = lo; i <= hi; i++) {
              selectedChatIds.add(visualChatOrder[i]);
            }
            lastClickedChatId = conv.id;
            if (currentContainer && currentItem) {
              Assistant.renderInterface(currentContainer, currentItem);
            }
          }
        } else if (selectedChatIds.size > 0 && !ctrl && !shift) {
          e.preventDefault();
          e.stopPropagation();
          selectedChatIds.clear();
          lastClickedChatId = conv.id;
          if (currentContainer && currentItem) {
            Assistant.renderInterface(currentContainer, currentItem);
          }
        } else {
          lastClickedChatId = conv.id;
          Assistant.loadChat(conv.id);
        }
      });

      item.addEventListener("contextmenu", (e: MouseEvent) => {
        e.preventDefault();
        e.stopPropagation();
        Assistant.showHistoryContextMenu(doc, conv, e.clientX, e.clientY, item);
      });

      // Drag and drop
      item.draggable = true;
      item.addEventListener("dragstart", (e) => {
        (e as DragEvent).dataTransfer?.setData("text/plain", conv.id);
        (e as DragEvent).dataTransfer!.effectAllowed = "move";
        item.style.opacity = "0.5";
      });
      item.addEventListener("dragend", () => {
        item.style.opacity = "1";
      });

      const dotsBtn = doc.createElement("span");
      dotsBtn.textContent = "\u22EE";
      dotsBtn.title = "More actions";
      dotsBtn.style.cssText = `
        position: absolute;
        right: 4px;
        top: 50%;
        transform: translateY(-50%);
        font-size: 14px;
        cursor: pointer;
        opacity: 0;
        transition: opacity 0.1s;
        padding: 0 4px;
        color: ${isSelected ? "rgba(255,255,255,0.7)" : "var(--text-secondary)"};
      `;
      dotsBtn.addEventListener("click", async (e: MouseEvent) => {
        e.stopPropagation();
        e.preventDefault();
        const itemRect = item.getBoundingClientRect();
        await Assistant.showHistoryContextMenu(
          doc,
          conv,
          itemRect.right,
          itemRect.top,
          item,
        );
      });
      item.appendChild(dotsBtn);

      item.onmouseenter = () => {
        item.style.background = isSelected
          ? "var(--highlight-primary, #4a90d9)"
          : isActive
            ? "var(--background-primary)"
            : "var(--background-hover, rgba(0,0,0,0.05))";
        dotsBtn.style.opacity = "1";
      };
      item.onmouseleave = () => {
        item.style.background = isSelected
          ? "var(--highlight-primary, #4a90d9)"
          : isActive
            ? "var(--background-primary)"
            : "transparent";
        dotsBtn.style.opacity = "0";
      };

      itemsContainer.appendChild(item);
    }

    section.appendChild(header);
    section.appendChild(itemsContainer);
    return section;
  }

  /**
   * Show a context menu for a history item (rename, delete, copy, clone, download, archive, move).
   */
  private static async showHistoryContextMenu(
    doc: Document,
    conv: ConversationMetadata,
    x: number,
    y: number,
    anchor?: HTMLElement,
  ) {
    const store = getMessageStore();
    const allFolders = await store.getFolders();

    // Remove any existing context menus
    const existing = doc.getElementById("history-context-menu");
    if (existing) existing.remove();

    const menu = doc.createElement("div");
    menu.id = "history-context-menu";
    menu.style.cssText = `
      position: fixed;
      left: ${x}px;
      top: ${y}px;
      z-index: 99999;
      background: var(--background-primary);
      border: 1px solid var(--border-primary);
      border-radius: 6px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      padding: 4px 0;
      min-width: 200px;
      font-size: 12px;
      max-height: 80vh;
      overflow-y: auto;
    `;

    const addItem = (
      text: string,
      action: () => void,
      danger: boolean = false,
    ) => {
      const el = doc.createElement("div");
      el.textContent = text;
      el.style.cssText = `
        padding: 6px 12px;
        cursor: pointer;
        color: ${danger ? "#ef5350" : "var(--text-primary)"};
        transition: background 0.1s;
        white-space: nowrap;
      `;
      el.onclick = async () => {
        menu.remove();
        await action();
      };
      el.onmouseenter = () => {
        el.style.background = "var(--background-hover, rgba(0,0,0,0.05))";
      };
      el.onmouseleave = () => {
        el.style.background = "transparent";
      };
      menu.appendChild(el);
    };

    const addSeparator = () => {
      const sep = doc.createElement("div");
      sep.style.cssText =
        "border-top: 1px solid var(--border-primary); margin: 4px 0;";
      menu.appendChild(sep);
    };

    const refreshHistory = async () => {
      await Assistant.loadHistory();
      if (currentContainer && currentItem) {
        Assistant.renderInterface(currentContainer, currentItem);
      }
    };

    // Rename
    addItem("Rename...", async () => {
      const newTitle = (
        doc.defaultView?.prompt("Rename conversation:", conv.title || "") || ""
      ).trim();
      if (!newTitle || newTitle === conv.title) return;
      await store.updateConversationMetadata({
        id: conv.id,
        title: newTitle,
      });
      await refreshHistory();
    });

    addSeparator();

    // Copy full chat history + context
    addItem("Copy", async () => {
      try {
        const exported = await store.exportConversation(conv.id);
        const clipText =
          "data:text/plain;charset=utf-8," + encodeURIComponent(exported);
        // In Zotero, we can use write to temp and copy to clipboard
        const textEncoder = new TextEncoder();
        const encoded = textEncoder.encode(exported);
        // Use a simple approach: copy via doc command
        const textarea = doc.createElement("textarea");
        textarea.value = exported;
        textarea.style.cssText = "position: fixed; top: -9999px;";
        doc.body?.appendChild(textarea);
        textarea.select();
        doc.execCommand("copy");
        textarea.remove();
      } catch (e) {
        Zotero.debug(`[seerai] Copy failed: ${e}`);
      }
    });

    // Clone
    addItem("Clone", async () => {
      try {
        const newId = await store.cloneConversation(conv.id);
        await refreshHistory();
        // Switch to the clone
        setTimeout(() => Assistant.loadChat(newId), 100);
      } catch (e) {
        Zotero.debug(`[seerai] Clone failed: ${e}`);
      }
    });

    addSeparator();

    // Archive / Unarchive
    if (conv.archived) {
      addItem("Unarchive", async () => {
        await store.unarchiveConversation(conv.id);
        await refreshHistory();
      });
    } else {
      addItem("Archive", async () => {
        await store.archiveConversation(conv.id);
        await refreshHistory();
      });
    }

    // Move to folder sub-items
    if (conv.folder) {
      addItem(`\u21A9 Remove from "${conv.folder}"`, async () => {
        await store.removeConversationFromFolder(conv.id);
        await refreshHistory();
      });
    }

    for (const folder of allFolders) {
      if (folder === conv.folder) continue;
      addItem(`\u2192 ${folder}`, async () => {
        await store.moveConversationToFolder(conv.id, folder);
        await refreshHistory();
      });
    }

    addItem("+ New folder...", async () => {
      const name = (
        doc.defaultView?.prompt("New folder name:", "") || ""
      ).trim();
      if (!name) return;
      await store.moveConversationToFolder(conv.id, name);
      await refreshHistory();
    });

    addSeparator();

    addItem(
      "Delete",
      async () => {
        if (doc.defaultView?.confirm("Delete this conversation?")) {
          await Assistant.deleteChat(conv.id);
        }
      },
      true,
    );

    const container = anchor || doc.body;
    if (!container) return;
    container.appendChild(menu);

    // Clamp to viewport
    setTimeout(() => {
      const menuRect = menu.getBoundingClientRect();
      const viewW = doc.defaultView?.innerWidth || 1200;
      const viewH = doc.defaultView?.innerHeight || 800;
      if (menuRect.right > viewW - 8) {
        menu.style.left = `${Math.max(8, viewW - menuRect.width - 8)}px`;
      }
      if (menuRect.bottom > viewH - 8) {
        menu.style.top = `${Math.max(8, viewH - menuRect.height - 8)}px`;
      }
    }, 0);

    // Click outside to close
    const closeListener = (e: Event) => {
      if (!menu.contains(e.target as Node)) {
        menu.remove();
        doc.removeEventListener("click", closeListener);
      }
    };
    setTimeout(() => doc.addEventListener("click", closeListener), 0);
  }

  /**
   * Re-render only the sidebars (history + workspace) without full reload.
   */
  private static async renderInterfaceSidebarsOnly() {
    if (currentContainer && currentItem) {
      await this.renderInterface(currentContainer, currentItem);
    }
  }

  /**
   * Create a horizontal resize handle for sidebar width adjustment.
   * @param direction "left" = drag right increases width (history sidebar),
   *                  "right" = drag left increases width (workspace sidebar)
   */
  private static createHorizontalResizeHandle(
    doc: Document,
    direction: "left" | "right",
    onResize: (width: number) => void,
    getWidth: () => number,
    minWidth: number = 120,
    maxWidth: number = 500,
    onDragEnd?: (width: number) => void,
  ): HTMLElement {
    const handle = doc.createElement("div");
    handle.className = "seerai-resize-handle";
    handle.style.cssText = `
      width: 4px;
      cursor: col-resize;
      background: transparent;
      flex-shrink: 0;
      transition: background 0.15s;
      z-index: 10;
    `;
    handle.addEventListener("mouseenter", () => {
      handle.style.background = "var(--highlight-primary)";
    });
    handle.addEventListener("mouseleave", () => {
      handle.style.background = "transparent";
    });

    let dragging = false;
    let startX = 0;
    let startWidth = 0;

    const sign = direction === "left" ? 1 : -1;

    handle.addEventListener("mousedown", (e) => {
      e.preventDefault();
      dragging = true;
      startX = (e as MouseEvent).clientX;
      startWidth = getWidth();
      handle.style.background = "var(--highlight-primary)";
      const body = handle.ownerDocument?.body;
      if (body) {
        body.style.cursor = "col-resize";
        body.style.userSelect = "none";
      }
    });

    doc.addEventListener("mousemove", (e) => {
      if (!dragging) return;
      const delta = ((e as MouseEvent).clientX - startX) * sign;
      const newWidth = Math.max(
        minWidth,
        Math.min(maxWidth, startWidth + delta),
      );
      onResize(newWidth);
    });

    doc.addEventListener("mouseup", () => {
      if (!dragging) return;
      dragging = false;
      handle.style.background = "transparent";
      const body = handle.ownerDocument?.body;
      if (body) {
        body.style.cursor = "";
        body.style.userSelect = "";
      }
      if (onDragEnd) onDragEnd(getWidth());
    });

    return handle;
  }

  /**
   * Create a vertical resize handle for editor height adjustment.
   * Drag down increases height.
   */
  private static createVerticalResizeHandle(
    doc: Document,
    onResize: (height: number) => void,
    getHeight: () => number,
    minHeight: number = 120,
    maxHeight: number = 800,
    onDragEnd?: (height: number) => void,
  ): HTMLElement {
    const handle = doc.createElement("div");
    handle.style.cssText = `
      height: 8px;
      cursor: row-resize;
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      z-index: 10;
      position: relative;
    `;

    // Visible grip indicator
    const grip = doc.createElement("div");
    grip.style.cssText = `
      width: 40px;
      height: 4px;
      border-radius: 2px;
      background: var(--text-tertiary);
      opacity: 0.4;
      transition: opacity 0.15s, background 0.15s;
    `;
    handle.appendChild(grip);

    handle.addEventListener("mouseenter", () => {
      grip.style.opacity = "0.8";
      grip.style.background = "var(--highlight-primary)";
      handle.style.background = "var(--background-hover, rgba(0,0,0,0.03))";
    });
    handle.addEventListener("mouseleave", () => {
      if (dragging) return;
      grip.style.opacity = "0.4";
      grip.style.background = "var(--text-tertiary)";
      handle.style.background = "transparent";
    });

    let dragging = false;
    let startY = 0;
    let startHeight = 0;

    handle.addEventListener("mousedown", (e) => {
      e.preventDefault();
      dragging = true;
      startY = (e as MouseEvent).clientY;
      startHeight = getHeight();
      grip.style.opacity = "1";
      grip.style.background = "var(--highlight-primary)";
      handle.style.background = "var(--background-hover, rgba(0,0,0,0.03))";
      const vBody = handle.ownerDocument?.body;
      if (vBody) {
        vBody.style.cursor = "row-resize";
        vBody.style.userSelect = "none";
      }
    });

    doc.addEventListener("mousemove", (e) => {
      if (!dragging) return;
      const delta = startY - (e as MouseEvent).clientY;
      const newHeight = Math.max(
        minHeight,
        Math.min(maxHeight, startHeight + delta),
      );
      onResize(newHeight);
    });

    doc.addEventListener("mouseup", () => {
      if (!dragging) return;
      dragging = false;
      grip.style.opacity = "0.4";
      grip.style.background = "var(--text-tertiary)";
      handle.style.background = "transparent";
      const vBody2 = handle.ownerDocument?.body;
      if (vBody2) {
        vBody2.style.cursor = "";
        vBody2.style.userSelect = "";
      }
      if (onDragEnd) onDragEnd(getHeight());
    });

    return handle;
  }

  /**
   * Create the Chat tab content (existing chat UI)
   */
  private static async createChatTabContent(
    doc: Document,
    item: Zotero.Item,
    stateManager: ReturnType<typeof getChatStateManager>,
  ): Promise<HTMLElement> {
    const mainWrapper = doc.createElement("div");
    mainWrapper.style.cssText =
      "display: flex; flex: 1 1 0; min-height: 0; width: 100%; min-width: 0; max-width: 100%; overflow: hidden; box-sizing: border-box;";

    // === CHAT CONTENT ===
    const chatContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        flex: "1 1 0",
        minHeight: "0",
        gap: "4px",
        padding: "2px 6px 10px 6px",
        minWidth: "0",
        maxWidth: "100%",
        position: "relative",
        boxSizing: "border-box",
        overflow: "hidden",
      },
    });

    // === SELECTION AREA ===
    let selectionArea: HTMLElement;
    try {
      selectionArea = this.createSelectionArea(doc, stateManager);
    } catch (e) {
      Zotero.debug(`[seerai] Error creating selection area: ${e}`);
      selectionArea = doc.createElement("div");
      selectionArea.textContent = `Error in selection area: ${e}`;
      selectionArea.style.color = "red";
    }

    // === MESSAGES AREA ===
    const messagesArea = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        flex: "1 1 0",
        minHeight: "0",
        overflowY: "auto",
        overflowX: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        padding: "8px",
        backgroundColor: "var(--background-primary)",
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
        userSelect: "text",
        cursor: "text",
      },
      properties: { id: "assistant-messages-area" },
    }) as HTMLElement;

    // Add copy event listener for smart markdown copy
    messagesArea.addEventListener("copy", (e: Event) => {
      Zotero.debug("[seerai] Copy event triggered in messages area");
      const clipboardEvent = e as ClipboardEvent;
      const selection = doc.getSelection();
      if (!selection || selection.rangeCount === 0) {
        Zotero.debug("[seerai] No selection found during copy");
        return;
      }

      const selectedText = selection.toString();
      Zotero.debug(`[seerai] Selected text length: ${selectedText.length}`);

      if (!selectedText.trim()) return;

      try {
        // Get the selected HTML content
        const range = selection.getRangeAt(0);
        const fragment = range.cloneContents();
        // Use HTML namespace to ensure proper element creation
        const tempDiv = doc.createElementNS
          ? doc.createElementNS("http://www.w3.org/1999/xhtml", "div")
          : doc.createElement("div");
        tempDiv.appendChild(fragment);

        Zotero.debug(
          `[seerai] Captured HTML fragment length: ${(tempDiv.innerHTML as string).length}`,
        );

        // Convert HTML to markdown-like text
        const markdownText = this.htmlToMarkdown(tempDiv as HTMLElement);
        Zotero.debug(
          `[seerai] Converted markdown length: ${markdownText.length}`,
        );

        // If markdown conversion produced different content, use it
        if (markdownText && markdownText.trim() !== selectedText.trim()) {
          Zotero.debug("[seerai] Using markdown for copy");
          e.preventDefault();
          if (clipboardEvent.clipboardData) {
            clipboardEvent.clipboardData.setData("text/plain", markdownText);
            // Also set HTML for rich paste targets
            // Cast to string to fix TS error
            clipboardEvent.clipboardData.setData(
              "text/html",
              tempDiv.innerHTML as string,
            );
          }
        } else {
          Zotero.debug(
            "[seerai] Using default copy (markdown identical or empty)",
          );
        }
      } catch (err) {
        Zotero.debug(`[seerai] Error in smart copy: ${err}`);
      }
    });

    // Restore previous messages
    try {
      const lastUserMsgIndex = conversationMessages
        .map((m) => m.role)
        .lastIndexOf("user");
      conversationMessages.forEach((msg, idx) => {
        const isLastUserMsg = msg.role === "user" && idx === lastUserMsgIndex;
        this.renderStoredMessage(messagesArea, msg, isLastUserMsg);
      });
    } catch (e) {
      Zotero.debug(`[seerai] Error restoring messages: ${e}`);
    }

    // === RESTORE ACTIVE SESSION ===
    if (activeAgentSession) {
      try {
        Zotero.debug(
          `[seerai] Restoring active agent session UI - isThinking: ${activeAgentSession.isThinking}, toolResults: ${activeAgentSession.toolResults.length}, fullResponse length: ${activeAgentSession.fullResponse?.length || 0}`,
        );

        const streamingDiv = this.appendMessage(
          messagesArea,
          "Assistant",
          activeAgentSession.fullResponse,
          "",
          false,
          activeAgentSession.toolResults,
          activeAgentSession.iterationCount,
        );
        const contentDiv = streamingDiv.querySelector(
          "[data-content]",
        ) as HTMLElement;

        // CRITICAL: Update session references IMMEDIATELY so any concurrent callbacks use new DOM
        activeAgentSession.messagesArea = messagesArea;
        activeAgentSession.contentDiv = contentDiv;

        // If it's thinking and has NO text yet AND no tools yet, ensure indicator is visible
        if (
          activeAgentSession.isThinking &&
          !activeAgentSession.fullResponse &&
          activeAgentSession.toolResults.length === 0
        ) {
          if (!contentDiv.querySelector(".typing-indicator")) {
            contentDiv.innerHTML = `
              <div class="typing-indicator" style="display: flex; align-items: center; gap: 4px; color: var(--text-secondary); font-style: italic;">
                  <span>Thinking</span>
                  <span class="dot" style="animation: blink 1.4s infinite .2s;">.</span>
                  <span class="dot" style="animation: blink 1.4s infinite .4s;">.</span>
                  <span class="dot" style="animation: blink 1.4s infinite .6s;">.</span>
              </div>
            `;
          }
        }

        // Re-extract the toolProcessState helpers for the restored UI
        // We find the existing container created by appendMessage
        const toolProcessContainer = streamingDiv.querySelector(
          ".tool-process-container",
        ) as HTMLElement;
        if (toolProcessContainer) {
          const label = toolProcessContainer.querySelector(
            ".process-label",
          ) as HTMLElement;
          const icon = toolProcessContainer.querySelector(
            ".process-icon",
          ) as HTMLElement;
          const listContainer = toolProcessContainer.querySelector(
            ".tool-list-container",
          ) as HTMLElement;

          const setThinking = () => {
            if (label) label.textContent = "Processing task...";
            if (icon) {
              icon.textContent = "⚡";
              icon.style.animation = "pulse 1.5s infinite";
            }
            (toolProcessContainer as any).open = false;
          };
          const setExecutingTool = (toolName: string) => {
            const displayName = toolName.replace(/_/g, " ");
            if (label) label.textContent = `Calling ${displayName}...`;
            if (icon) {
              icon.textContent = "🔧";
              icon.style.animation = "pulse 1s infinite";
            }
          };
          const setCompleted = (count: number, toolCount?: number) => {
            if (toolCount !== undefined && toolCount !== count) {
              if (label)
                label.textContent = `Completed ${toolCount} action${toolCount !== 1 ? "s" : ""} in ${count} turn${count !== 1 ? "s" : ""}`;
            } else {
              if (label)
                label.textContent = `Completed ${count} analysis turn${count !== 1 ? "s" : ""}`;
            }
            if (icon) {
              icon.textContent = "✓";
              icon.style.animation = "none";
              icon.style.color = "var(--accent-green, #34C759)";
            }
          };
          const setFailed = (error: string) => {
            if (label) {
              label.textContent = `Failed: ${error}`;
              label.style.color = "var(--accent-red, #FF3B30)";
            }
            if (icon) {
              icon.textContent = "✗";
              icon.style.animation = "none";
              icon.style.color = "var(--accent-red, #FF3B30)";
            }
            (toolProcessContainer as any).open = true;
          };
          const updateProgress = (count: number, toolCount?: number) => {
            if (label) {
              if (toolCount !== undefined && toolCount !== count) {
                label.textContent = `Processing ${toolCount} action${toolCount !== 1 ? "s" : ""} in ${count} turn${count !== 1 ? "s" : ""}`;
              } else {
                label.textContent = `Processing ${count} analysis turn${count !== 1 ? "s" : ""}`;
              }
            }
            if (icon) {
              icon.textContent = "⚡";
              icon.style.filter = "none";
              icon.style.color = "var(--text-secondary)";
              icon.style.animation = "pulse 2s infinite";
            }
          };

          activeAgentSession.toolProcessState = {
            container: toolProcessContainer,
            setThinking,
            setExecutingTool,
            setCompleted,
            updateProgress,
            setFailed,
            updateStats: () => {},
          };
          activeAgentSession.toolContainer = listContainer;

          // Restore persisted open state
          if (activeAgentSession.isToolDetailsOpen) {
            (toolProcessContainer as any).open = true;
          }

          // Add listener to track state changes
          toolProcessContainer.addEventListener("toggle", (e: Event) => {
            if (activeAgentSession) {
              activeAgentSession.isToolDetailsOpen = (
                e.target as HTMLDetailsElement
              ).open;
            }
          });

          // Set appropriate state indicator
          if (
            activeAgentSession.isThinking ||
            activeAgentSession.toolResults.some((tr) => !tr.result)
          ) {
            // Still thinking or mid-tool
            const finalCount = activeAgentSession.iterationCount || 1;
            const toolCount = activeAgentSession.toolResults.length;
            updateProgress(finalCount, toolCount);
          } else if (
            activeAgentSession.iterationCount > 0 ||
            activeAgentSession.toolResults.length > 0
          ) {
            const finalCount = activeAgentSession.iterationCount || 0;
            const toolCount = activeAgentSession.toolResults.length;
            setCompleted(finalCount, toolCount);
          } else {
            setThinking();
          }

          // CRITICAL: Update the uiElement references in toolResults so observer updates THIS element
          const toolCards = listContainer.querySelectorAll(
            ".tool-execution-card",
          );
          activeAgentSession.toolResults.forEach((tr, idx) => {
            if (toolCards[idx]) {
              tr.uiElement = toolCards[idx] as HTMLElement;
            }
          });
        }

        // Show stop button if streaming
        const stopBtn = doc.getElementById("stop-btn") as HTMLElement | null;
        if (stopBtn && this.isStreaming) {
          stopBtn.style.display = "inline-block";
        }

        // Auto-scroll
        messagesArea.scrollTop = messagesArea.scrollHeight;

        Zotero.debug("[seerai] Active session restoration complete");
      } catch (e) {
        Zotero.debug(`[seerai] Error restoring active session: ${e}`);
        const errDiv = doc.createElement("div");
        errDiv.textContent = `Error restoring session: ${e}`;
        errDiv.style.color = "red";
        messagesArea.appendChild(errDiv);
      }
    }

    // === INPUT AREA ===
    let inputArea: HTMLElement;
    try {
      inputArea = this.createInputArea(doc, messagesArea, stateManager);
    } catch (e) {
      Zotero.debug(`[seerai] Error creating input area: ${e}`);
      inputArea = doc.createElement("div");
      inputArea.textContent = `Error in input area: ${e}`;
      inputArea.style.color = "red";
    }

    // Function to update token counts
    let updateDebounce: ReturnType<typeof setTimeout> | null = null;
    const updateTokenDisplay = async () => {
      if (updateDebounce) clearTimeout(updateDebounce);
      updateDebounce = setTimeout(async () => {
        try {
          const items = ChatContextManager.getInstance().getItems();
          const contextTokens = await Assistant.estimateContextTokens(items);
          const cumulativeTokens =
            stateManager.getSessionCumulativeTokens(conversationMessages);

          const contextTokenLabel = doc.getElementById(
            "context-tokens-display",
          ) as HTMLElement | null;
          const cumulativeTokenLabel = doc.getElementById(
            "cumulative-tokens-display",
          ) as HTMLElement | null;

          if (contextTokenLabel) {
            contextTokenLabel.innerText = `Context: ${contextTokens.toLocaleString()}`;
          }
          if (cumulativeTokenLabel) {
            cumulativeTokenLabel.innerText = `Total: ${cumulativeTokens.toLocaleString()}`;
          }

          Zotero.debug(
            `[seerai] Token display updated: context=${contextTokens}, total=${cumulativeTokens}`,
          );
        } catch (e) {
          Zotero.debug(`[seerai] Error updating token display: ${e}`);
        }
      }, 500); // Debounce to prevent UI lag on rapid updates
    };

    // Update initially after a delay to ensure context is ready
    setTimeout(updateTokenDisplay, 500);

    const myGeneration = renderGeneration;
    ChatContextManager.getInstance().addListener(() => {
      if (renderGeneration === myGeneration) updateTokenDisplay();
    });
    stateManager.subscribe(() => {
      if (renderGeneration === myGeneration) updateTokenDisplay();
    });

    // === WORKSPACE EDITOR CONTAINER ===
    const workspaceEditorContainer = doc.createElement("div");
    workspaceEditorContainer.className = "workspace-editor-container";
    const editorManager = WorkspaceEditorManager.getInstance();
    editorManager.createEditorContainer(doc, workspaceEditorContainer);

    const makeOnFileClick = (
      entry: any,
      diffMode?: string,
      oldContent?: string,
      newContent?: string,
    ) => {
      if (diffMode) {
        return editorManager.openFileWithDiff(
          entry,
          oldContent ?? "",
          newContent ?? "",
        );
      }
      return editorManager.openFile(entry);
    };

    // === WORKSPACE SIDEBAR ===
    let workspaceSidebar: HTMLElement;
    try {
      workspaceSidebar = createWorkspaceSidebar(doc, {
        onFileClick: async (entry, diffMode, oldContent, newContent) => {
          await makeOnFileClick(entry, diffMode, oldContent, newContent);
        },
        onFileCreate: async () => {
          const name = (
            doc.defaultView?.prompt(
              "Enter file path (e.g., src/new-file.ts):",
              "",
            ) || ""
          ).trim();
          if (!name) return;
          const store = getWorkspaceStore();
          await store.writeFile(name, "", `Created ${name}`, "user");
          await refreshWorkspaceSidebar(workspaceSidebar, {
            onFileClick: async (entry, diffMode, oldContent, newContent) => {
              await makeOnFileClick(entry, diffMode, oldContent, newContent);
            },
            onFileCreate: async () => {},
            onFileDelete: async (entry) => {},
            onFileRename: async (entry) => {},
          });
        },
        onFileDelete: async (entry) => {
          if (
            !doc.defaultView?.confirm(
              `Delete "${entry.path}"? This cannot be undone.`,
            )
          )
            return;
          const store = getWorkspaceStore();
          await store.deleteFile(entry.path);
          editorManager.closeAll();
          await refreshWorkspaceSidebar(workspaceSidebar, {
            onFileClick: async (entry, diffMode, oldContent, newContent) => {
              await makeOnFileClick(entry, diffMode, oldContent, newContent);
            },
            onFileCreate: async () => {},
            onFileDelete: async (e) => {},
            onFileRename: async (e) => {},
          });
        },
        onFileRename: async (entry) => {
          const newPath = (
            doc.defaultView?.prompt("Rename to:", entry.path) || ""
          ).trim();
          if (!newPath || newPath === entry.path) return;
          const store = getWorkspaceStore();
          await store.renameFile(entry.path, newPath);
          await refreshWorkspaceSidebar(workspaceSidebar, {
            onFileClick: async (entry, diffMode, oldContent, newContent) => {
              await makeOnFileClick(entry, diffMode, oldContent, newContent);
            },
            onFileCreate: async () => {},
            onFileDelete: async (e) => {},
            onFileRename: async (e) => {},
          });
        },
      });

      // Refresh callbacks (re-bind with workspaceSidebar reference)
      const refreshCallbacks = () => ({
        onFileClick: async (
          entry: any,
          diffMode?: string,
          oldContent?: string,
          newContent?: string,
        ) => {
          await makeOnFileClick(entry, diffMode, oldContent, newContent);
        },
        onFileCreate: async () => {
          const name = (
            doc.defaultView?.prompt(
              "Enter file path (e.g., src/new-file.ts):",
              "",
            ) || ""
          ).trim();
          if (!name) return;
          const store = getWorkspaceStore();
          await store.writeFile(name, "", `Created ${name}`, "user");
          await refreshWorkspaceSidebar(workspaceSidebar, refreshCallbacks());
        },
        onFileDelete: async (entry: any) => {
          if (
            !doc.defaultView?.confirm(
              `Delete "${entry.path}"? This cannot be undone.`,
            )
          )
            return;
          const store = getWorkspaceStore();
          await store.deleteFile(entry.path);
          editorManager.closeAll();
          await refreshWorkspaceSidebar(workspaceSidebar, refreshCallbacks());
        },
        onFileRename: async (entry: any) => {
          const newPath = (
            doc.defaultView?.prompt("Rename to:", entry.path) || ""
          ).trim();
          if (!newPath || newPath === entry.path) return;
          const store = getWorkspaceStore();
          await store.renameFile(entry.path, newPath);
          await refreshWorkspaceSidebar(workspaceSidebar, refreshCallbacks());
        },
      });

      // Wire up auto-refresh when workspace tools write/delete/edit files
      getWorkspaceStore().onWorkspaceChanged = async () => {
        await refreshWorkspaceSidebar(workspaceSidebar, refreshCallbacks());
      };
    } catch (e) {
      Zotero.debug(`[seerai] Error creating workspace sidebar: ${e}`);
      workspaceSidebar = doc.createElement("div");
      workspaceSidebar.textContent = `Error: ${e}`;
    }

    // Assemble
    chatContainer.appendChild(selectionArea);
    chatContainer.appendChild(this.createPromptBanner(doc, stateManager));
    chatContainer.appendChild(messagesArea);
    chatContainer.appendChild(workspaceEditorContainer);
    chatContainer.appendChild(inputArea);

    const historySidebar = await this.createHistorySidebar(doc);

    // Ensure workspace sidebar has flex-shrink: 0
    workspaceSidebar.style.flexShrink = "0";

    // Read persisted sizes
    const prefPrefix = config.prefsPrefix;
    const historyWidth =
      (Zotero.Prefs.get(`${prefPrefix}.historySidebarWidth`) as number) || 220;
    const workspaceWidth =
      (Zotero.Prefs.get(`${prefPrefix}.workspaceSidebarWidth`) as number) ||
      260;
    const editorHeight =
      (Zotero.Prefs.get(`${prefPrefix}.workspaceEditorHeight`) as number) ||
      300;

    // Apply sizes (respect sidebar collapsed states)
    if (isHistorySidebarVisible) {
      historySidebar.style.width = `${historyWidth}px`;
      historySidebar.style.minWidth = "0";
    }
    if (!(workspaceSidebar as any)._isCollapsed) {
      workspaceSidebar.style.width = `${workspaceWidth}px`;
      workspaceSidebar.style.minWidth = "0";
    }
    workspaceEditorContainer.style.height = `${editorHeight}px`;
    workspaceEditorContainer.style.maxHeight = `${editorHeight}px`;

    // History resize handle (right edge of left sidebar)
    const historyHandle = this.createHorizontalResizeHandle(
      doc,
      "left",
      (w) => {
        historySidebar.style.width = `${w}px`;
        historySidebar.style.minWidth = "0";
      },
      () => parseInt(historySidebar.style.width) || historyWidth,
      120,
      500,
      (w) => Zotero.Prefs.set(`${prefPrefix}.historySidebarWidth`, w),
    );

    // Workspace resize handle (left edge of right sidebar)
    const workspaceHandle = this.createHorizontalResizeHandle(
      doc,
      "right",
      (w) => {
        workspaceSidebar.style.width = `${w}px`;
        workspaceSidebar.style.minWidth = "0";
      },
      () => parseInt(workspaceSidebar.style.width) || workspaceWidth,
      120,
      500,
      (w) => Zotero.Prefs.set(`${prefPrefix}.workspaceSidebarWidth`, w),
    );

    // Editor height resize handle (top edge of editor container)
    const editorHeightHandle = this.createVerticalResizeHandle(
      doc,
      (h) => {
        workspaceEditorContainer.style.height = `${h}px`;
        workspaceEditorContainer.style.maxHeight = `${h}px`;
      },
      () => parseInt(workspaceEditorContainer.style.height) || editorHeight,
      120,
      800,
      (h) => Zotero.Prefs.set(`${prefPrefix}.workspaceEditorHeight`, h),
    );

    mainWrapper.appendChild(historySidebar);
    mainWrapper.appendChild(historyHandle);
    mainWrapper.appendChild(chatContainer);
    mainWrapper.appendChild(workspaceHandle);
    mainWrapper.appendChild(workspaceSidebar);

    // Hide resize handles for collapsed sidebars
    if (!isHistorySidebarVisible) {
      historyHandle.style.display = "none";
    }
    if ((workspaceSidebar as any)._isCollapsed) {
      workspaceHandle.style.display = "none";
    }

    // Insert editor height handle before editor container
    chatContainer.insertBefore(editorHeightHandle, workspaceEditorContainer);

    // Populate workspace sidebar with current files
    await refreshWorkspaceSidebar(workspaceSidebar, {
      onFileClick: async (entry, diffMode, oldContent, newContent) => {
        if (diffMode) {
          await editorManager.openFileWithDiff(
            entry,
            oldContent ?? "",
            newContent ?? "",
          );
        } else {
          await editorManager.openFile(entry);
        }
      },
      onFileCreate: async () => {},
      onFileDelete: async (entry) => {},
      onFileRename: async (entry) => {},
    });

    return mainWrapper;
  }

  /**
   * Create a collapsible prompt config banner above the messages area.
   * Clicking the header expands/collapses an inline editor (not a popup).
   */
  private static createPromptBanner(
    doc: Document,
    stateManager: ReturnType<typeof getChatStateManager>,
  ): HTMLElement {
    const store = getMessageStore();
    const options = stateManager.getOptions();
    const systemPrompt = options.systemPrompt || "";
    const skillIds = options.skillIds || [];

    const banner = doc.createElement("div");
    banner.id = "prompt-config-banner";
    banner.style.cssText = `
      padding: 6px 10px;
      font-size: 11px;
      flex-shrink: 0;
      border: 1px solid var(--border-primary);
      border-radius: 6px;
      background: var(--background-secondary);
    `;

    const header = doc.createElement("div");
    header.style.cssText =
      "display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;";

    const arrow = doc.createElement("span");
    arrow.textContent = "\u25B8";
    arrow.style.cssText = "font-size: 10px; transition: transform 0.2s;";

    const label = doc.createElement("span");
    label.style.cssText =
      "font-weight: 600; color: var(--text-secondary); white-space: nowrap;";
    label.textContent = "System Prompt";

    const preview = doc.createElement("span");
    preview.style.cssText =
      "color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;";
    preview.textContent = systemPrompt
      ? systemPrompt.replace(/\n/g, " ").slice(0, 60) +
        (systemPrompt.length > 60 ? "\u2026" : "")
      : "None set";

    // Skills count badge
    if (skillIds.length > 0) {
      const badge = doc.createElement("span");
      badge.textContent = `${skillIds.length} skill(s)`;
      badge.style.cssText =
        "background: var(--accent-blue, #007AFF); color: #fff; border-radius: 8px; padding: 0 6px; font-size: 10px; white-space: nowrap;";
      header.appendChild(badge);
    }

    header.appendChild(arrow);
    header.appendChild(label);
    header.appendChild(preview);

    // Inline editable body (collapsed by default unless persisted)
    const body = doc.createElement("div");
    body.style.cssText = systemPromptExpanded
      ? "margin-top: 8px;"
      : "display: none; margin-top: 8px;";
    body.dataset.loaded = "false";

    if (systemPromptExpanded) {
      arrow.textContent = "\u25BE";
      // Build the editor body immediately since it's already expanded
      void this.buildPromptEditorBody(doc, body, banner, stateManager).then(
        () => {
          body.dataset.loaded = "true";
        },
      );
    }

    // Build the editor inline on first expand
    header.addEventListener("click", async () => {
      const collapsed = body.style.display === "none";
      if (collapsed) {
        body.style.display = "block";
        arrow.textContent = "\u25BE";
        systemPromptExpanded = true;
        if (body.dataset.loaded !== "true") {
          await this.buildPromptEditorBody(doc, body, banner, stateManager);
          body.dataset.loaded = "true";
        }
      } else {
        body.style.display = "none";
        arrow.textContent = "\u25B8";
        systemPromptExpanded = false;
      }
    });

    banner.appendChild(header);
    banner.appendChild(body);

    return banner;
  }

  /**
   * Populate the inline prompt editor body with textarea + skills + actions.
   */
  private static async buildPromptEditorBody(
    doc: Document,
    body: HTMLElement,
    banner: HTMLElement,
    stateManager: ReturnType<typeof getChatStateManager>,
  ): Promise<void> {
    // Clear any existing content (prevents duplicates on rapid re-expand)
    body.innerHTML = "";

    const store = getMessageStore();
    const options = stateManager.getOptions();
    const currentPrompt = options.systemPrompt || "";
    const currentSkillIds = options.skillIds || [];

    // System prompt textarea
    const promptLabel = doc.createElement("label");
    promptLabel.textContent = "System Prompt";
    promptLabel.style.cssText =
      "font-weight: 600; color: var(--text-secondary); font-size: 11px; display: block; margin-bottom: 4px;";
    body.appendChild(promptLabel);

    const promptArea = doc.createElement("textarea");
    promptArea.value = currentPrompt;
    promptArea.placeholder =
      "Enter a custom system prompt for this conversation...";
    promptArea.style.cssText = `
      width: 100%;
      padding: 8px;
      font-family: "Menlo", "Monaco", "Consolas", monospace;
      font-size: 11px;
      line-height: 1.5;
      border: 1px solid var(--border-primary);
      border-radius: 4px;
      background: var(--background-primary);
      color: var(--text-primary);
      resize: vertical;
      box-sizing: border-box;
      min-height: 80px;
      outline: none;
    `;
    promptArea.rows = 5;
    body.appendChild(promptArea);

    // Skills selection
    const skillsLabel = doc.createElement("div");
    skillsLabel.textContent = "Skills (from Prompt Library)";
    skillsLabel.style.cssText =
      "font-weight: 600; color: var(--text-secondary); font-size: 11px; margin-top: 8px; margin-bottom: 4px;";
    body.appendChild(skillsLabel);

    const skillsContainer = doc.createElement("div");
    skillsContainer.style.cssText =
      "max-height: 160px; overflow-y: auto; border: 1px solid var(--border-primary); border-radius: 4px; padding: 4px;";
    body.appendChild(skillsContainer);

    const selectedSkills = new Set<string>(currentSkillIds);

    try {
      const skills = await getPromptsByCategory("skills");

      if (skills.length === 0) {
        const empty = doc.createElement("div");
        empty.textContent = "No skills templates found. Create one below.";
        empty.style.cssText =
          "padding: 8px; color: var(--text-tertiary); font-style: italic; font-size: 11px;";
        skillsContainer.appendChild(empty);
      } else {
        for (const skill of skills) {
          const row = doc.createElement("label");
          row.style.cssText =
            "display: flex; align-items: center; gap: 6px; padding: 4px 6px; cursor: pointer; font-size: 11px;";
          row.addEventListener("mouseenter", () => {
            row.style.background = "var(--background-hover, rgba(0,0,0,0.05))";
          });
          row.addEventListener("mouseleave", () => {
            row.style.background = "transparent";
          });

          const cb = doc.createElement("input");
          cb.type = "checkbox";
          cb.checked = selectedSkills.has(skill.id);
          cb.addEventListener("change", () => {
            if (cb.checked) selectedSkills.add(skill.id);
            else selectedSkills.delete(skill.id);
          });

          const name = doc.createElement("span");
          name.textContent = skill.name;
          name.style.cssText = "flex: 1;";

          row.appendChild(cb);
          row.appendChild(name);
          skillsContainer.appendChild(row);
        }
      }

      // "New Skill" inline form
      const newSkillRow = doc.createElement("div");
      newSkillRow.style.cssText =
        "display: flex; gap: 4px; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border-primary);";
      const newSkillInput = doc.createElement("input");
      newSkillInput.type = "text";
      newSkillInput.placeholder = "New skill name...";
      newSkillInput.style.cssText = `
        flex: 1;
        padding: 4px 8px;
        font-size: 11px;
        border: 1px solid var(--border-primary);
        border-radius: 4px;
        background: var(--background-primary);
        color: var(--text-primary);
        outline: none;
      `;
      const newSkillBtn = doc.createElementNS(
        HTML_NS,
        "button",
      ) as HTMLButtonElement;
      newSkillBtn.textContent = "+";
      newSkillBtn.title = "Add skill template";
      newSkillBtn.style.cssText = `
        padding: 4px 8px;
        font-size: 11px;
        border: 1px solid var(--border-primary);
        border-radius: 4px;
        background: var(--background-secondary);
        color: var(--text-secondary);
        cursor: pointer;
      `;
      newSkillBtn.addEventListener("click", async () => {
        const name = newSkillInput.value.trim();
        if (!name) return;
        try {
          const template = promptArea.value.trim();
          await addPrompt({
            name,
            description: `Skill: ${name}`,
            template: template || `You are skilled at ${name.toLowerCase()}.`,
            category: "skills",
            tags: ["skill", name.toLowerCase()],
          });
          newSkillInput.value = "";
          selectedSkills.add(name.toLowerCase().replace(/\s+/g, "-"));
          newSkillBtn.textContent = "\u2713";
          setTimeout(() => {
            newSkillBtn.textContent = "+";
          }, 1000);
        } catch (e) {
          Zotero.debug(`[seerai] Error adding skill: ${e}`);
        }
      });
      newSkillRow.appendChild(newSkillInput);
      newSkillRow.appendChild(newSkillBtn);
      skillsContainer.appendChild(newSkillRow);
    } catch (e) {
      const err = doc.createElement("div");
      err.textContent = `Error loading skills: ${e}`;
      err.style.cssText = "padding: 8px; color: #ef5350;";
      skillsContainer.appendChild(err);
    }

    // Action buttons
    const actions = doc.createElement("div");
    actions.style.cssText =
      "display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px;";

    const cancelBtn = doc.createElementNS(
      HTML_NS,
      "button",
    ) as HTMLButtonElement;
    cancelBtn.textContent = "Cancel";
    cancelBtn.style.cssText = `
      padding: 5px 10px;
      font-size: 11px;
      border: 1px solid var(--border-primary);
      border-radius: 4px;
      background: var(--background-secondary);
      color: var(--text-secondary);
      cursor: pointer;
    `;
    cancelBtn.addEventListener("click", () => {
      body.style.display = "none";
      systemPromptExpanded = false;
      const arrowEl = banner.querySelector("span");
      if (arrowEl) arrowEl.textContent = "\u25B8";
    });
    actions.appendChild(cancelBtn);

    const saveBtn = doc.createElementNS(HTML_NS, "button") as HTMLButtonElement;
    saveBtn.textContent = "Apply";
    saveBtn.style.cssText = `
      padding: 5px 12px;
      font-size: 11px;
      font-weight: 600;
      border: none;
      border-radius: 4px;
      background: var(--highlight-primary);
      color: #fff;
      cursor: pointer;
    `;
    saveBtn.addEventListener("click", async () => {
      const newPrompt = promptArea.value.trim();
      const newSkillIds = Array.from(selectedSkills);

      stateManager.setOptions({
        systemPrompt: newPrompt || undefined,
        skillIds: newSkillIds.length > 0 ? newSkillIds : undefined,
      } as any);

      await store.updateConversationMetadata({
        systemPrompt: newPrompt || undefined,
        skillIds: newSkillIds.length > 0 ? newSkillIds : undefined,
      });

      // Refresh banner to show updated preview text
      if (banner.parentNode) {
        const newBanner = await this.createPromptBanner(doc, stateManager);
        banner.parentNode.replaceChild(newBanner, banner);
      }
    });
    actions.appendChild(saveBtn);

    body.appendChild(actions);
  }

  /**
   * Show the prompt configuration modal for editing system prompt and skills.
   * @deprecated Use the inline expander in createPromptBanner instead.
   */
  private static async showPromptConfigModal(
    doc: Document,
    stateManager: ReturnType<typeof getChatStateManager>,
    banner: HTMLElement,
  ): Promise<void> {
    const existing = doc.getElementById("prompt-config-modal");
    if (existing) existing.remove();
    const existingBackdrop = doc.getElementById("prompt-config-modal-backdrop");
    if (existingBackdrop) existingBackdrop.remove();

    const store = getMessageStore();
    const options = stateManager.getOptions();
    const currentPrompt = options.systemPrompt || "";
    const currentSkillIds = options.skillIds || [];

    const overlay = doc.createElement("div");
    overlay.id = "prompt-config-modal-backdrop";
    overlay.style.cssText = `
      position: absolute;
      inset: 0;
      z-index: 100000;
      background: rgba(0,0,0,0.3);
      display: flex;
      align-items: center;
      justify-content: center;
    `;
    overlay.addEventListener("click", (e) => {
      if (e.target === overlay) overlay.remove();
    });

    const modal = doc.createElement("div");
    modal.id = "prompt-config-modal";
    modal.style.cssText = `
      background: var(--background-primary);
      border: 1px solid var(--border-primary);
      border-radius: 8px;
      box-shadow: 0 8px 24px rgba(0,0,0,0.2);
      padding: 16px;
      min-width: 440px;
      max-width: 600px;
      max-height: 80vh;
      display: flex;
      flex-direction: column;
      gap: 12px;
      font-size: 12px;
    `;

    // Title
    const title = doc.createElement("div");
    title.style.cssText =
      "display: flex; justify-content: space-between; align-items: center;";
    const titleText = doc.createElement("span");
    titleText.textContent = "Configure Prompt & Skills";
    titleText.style.cssText = "font-weight: 600; font-size: 14px;";
    const closeBtn = doc.createElement("span");
    closeBtn.textContent = "\u2715";
    closeBtn.style.cssText =
      "cursor: pointer; font-size: 16px; color: var(--text-secondary); padding: 0 4px;";
    closeBtn.addEventListener("click", () => overlay.remove());
    title.appendChild(titleText);
    title.appendChild(closeBtn);
    modal.appendChild(title);

    // System prompt textarea
    const promptLabel = doc.createElement("label");
    promptLabel.textContent = "System Prompt";
    promptLabel.style.cssText =
      "font-weight: 600; color: var(--text-secondary);";
    modal.appendChild(promptLabel);

    const promptArea = doc.createElement("textarea");
    promptArea.value = currentPrompt;
    promptArea.placeholder =
      "Enter a custom system prompt for this conversation...";
    promptArea.style.cssText = `
      width: 100%;
      padding: 8px;
      font-family: "Menlo", "Monaco", "Consolas", monospace;
      font-size: 11px;
      line-height: 1.5;
      border: 1px solid var(--border-primary);
      border-radius: 4px;
      background: var(--background-primary);
      color: var(--text-primary);
      resize: vertical;
      box-sizing: border-box;
      min-height: 80px;
      outline: none;
    `;
    promptArea.rows = 5;
    modal.appendChild(promptArea);

    // Skills selection
    const skillsLabel = doc.createElement("div");
    skillsLabel.textContent = "Skills (from Prompt Library)";
    skillsLabel.style.cssText =
      "font-weight: 600; color: var(--text-secondary); margin-top: 4px;";
    modal.appendChild(skillsLabel);

    const skillsContainer = doc.createElement("div");
    skillsContainer.style.cssText =
      "max-height: 200px; overflow-y: auto; border: 1px solid var(--border-primary); border-radius: 4px; padding: 4px;";

    // Load skills from prompt library
    try {
      const skills = await getPromptsByCategory("skills");
      const selectedSkills = new Set(currentSkillIds);

      if (skills.length === 0) {
        const empty = doc.createElement("div");
        empty.textContent =
          "No skills templates found. Add 'skills' category templates in the Prompt Library settings.";
        empty.style.cssText =
          "padding: 8px; color: var(--text-tertiary); font-style: italic;";
        skillsContainer.appendChild(empty);
      } else {
        for (const skill of skills) {
          const row = doc.createElement("label");
          row.style.cssText =
            "display: flex; align-items: center; gap: 6px; padding: 4px 6px; cursor: pointer; font-size: 11px;";
          row.addEventListener("mouseenter", () => {
            row.style.background = "var(--background-hover, rgba(0,0,0,0.05))";
          });
          row.addEventListener("mouseleave", () => {
            row.style.background = "transparent";
          });

          const cb = doc.createElement("input");
          cb.type = "checkbox";
          cb.checked = selectedSkills.has(skill.id);
          cb.addEventListener("change", () => {
            if (cb.checked) selectedSkills.add(skill.id);
            else selectedSkills.delete(skill.id);
          });

          const name = doc.createElement("span");
          name.textContent = skill.name;
          name.style.cssText = "flex: 1;";

          row.appendChild(cb);
          row.appendChild(name);
          skillsContainer.appendChild(row);
        }
      }

      // Store selectedSkills set for save
      (modal as any)._selectedSkills = selectedSkills;

      // Add "New Skill" form
      const newSkillRow = doc.createElement("div");
      newSkillRow.style.cssText =
        "display: flex; gap: 4px; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border-primary);";
      const newSkillInput = doc.createElement("input");
      newSkillInput.type = "text";
      newSkillInput.placeholder = "Skill name...";
      newSkillInput.style.cssText = `
        flex: 1;
        padding: 4px 8px;
        font-size: 11px;
        border: 1px solid var(--border-primary);
        border-radius: 4px;
        background: var(--background-primary);
        color: var(--text-primary);
        outline: none;
      `;
      const newSkillBtn = doc.createElementNS(
        HTML_NS,
        "button",
      ) as HTMLButtonElement;
      newSkillBtn.textContent = "+";
      newSkillBtn.title = "Add skill template";
      newSkillBtn.style.cssText = `
        padding: 4px 8px;
        font-size: 11px;
        border: 1px solid var(--border-primary);
        border-radius: 4px;
        background: var(--background-secondary);
        color: var(--text-secondary);
        cursor: pointer;
      `;
      newSkillBtn.addEventListener("click", async () => {
        const name = newSkillInput.value.trim();
        if (!name) return;
        try {
          const template = promptArea.value.trim();
          await addPrompt({
            name,
            description: `Skill: ${name}`,
            template: template || `You are skilled at ${name.toLowerCase()}.`,
            category: "skills",
            tags: ["skill", name.toLowerCase()],
          });
          newSkillInput.value = "";
          const newCb = doc.createElement("input");
          newCb.type = "checkbox";
          newCb.checked = true;
          selectedSkills.add(name.toLowerCase().replace(/\s+/g, "-"));
          newSkillBtn.textContent = "\u2713";
          setTimeout(() => {
            newSkillBtn.textContent = "+";
          }, 1000);
        } catch (e) {
          Zotero.debug(`[seerai] Error adding skill: ${e}`);
        }
      });
      newSkillRow.appendChild(newSkillInput);
      newSkillRow.appendChild(newSkillBtn);
      skillsContainer.appendChild(newSkillRow);
    } catch (e) {
      const err = doc.createElement("div");
      err.textContent = `Error loading skills: ${e}`;
      err.style.cssText = "padding: 8px; color: #ef5350;";
      skillsContainer.appendChild(err);
    }

    modal.appendChild(skillsContainer);

    // Action buttons
    const actions = doc.createElement("div");
    actions.style.cssText =
      "display: flex; justify-content: space-between; gap: 8px; margin-top: 4px;";

    const presetBtn = doc.createElementNS(
      HTML_NS,
      "button",
    ) as HTMLButtonElement;
    presetBtn.textContent = "Save as Preset...";
    presetBtn.style.cssText = `
      padding: 6px 12px;
      font-size: 11px;
      border: 1px solid var(--border-primary);
      border-radius: 4px;
      background: var(--background-secondary);
      color: var(--text-secondary);
      cursor: pointer;
    `;
    presetBtn.addEventListener("click", async () => {
      const name = (doc.defaultView?.prompt("Preset name:", "") || "").trim();
      if (!name) return;
      const sel = (modal as any)._selectedSkills as Set<string>;
      const skillIds = Array.from(sel);
      // Save to prompt library
      try {
        await addPrompt({
          name,
          description: `Agent preset: ${promptArea.value ? "with prompt" : ""} ${skillIds.length > 0 ? "with " + skillIds.length + " skills" : ""}`,
          template: promptArea.value,
          category: "skills",
          tags: ["preset", ...skillIds],
        });
        Zotero.debug(`[seerai] Preset "${name}" saved to prompt library`);
      } catch (e) {
        Zotero.debug(`[seerai] Error saving preset: ${e}`);
      }
    });
    actions.appendChild(presetBtn);

    const rightActions = doc.createElement("div");
    rightActions.style.cssText = "display: flex; gap: 8px;";

    const cancelBtn = doc.createElementNS(
      HTML_NS,
      "button",
    ) as HTMLButtonElement;
    cancelBtn.textContent = "Cancel";
    cancelBtn.style.cssText = `
      padding: 6px 12px;
      font-size: 11px;
      border: 1px solid var(--border-primary);
      border-radius: 4px;
      background: var(--background-secondary);
      color: var(--text-secondary);
      cursor: pointer;
    `;
    cancelBtn.addEventListener("click", () => overlay.remove());
    rightActions.appendChild(cancelBtn);

    const saveBtn = doc.createElementNS(HTML_NS, "button") as HTMLButtonElement;
    saveBtn.textContent = "Apply";
    saveBtn.style.cssText = `
      padding: 6px 16px;
      font-size: 11px;
      font-weight: 600;
      border: none;
      border-radius: 4px;
      background: var(--highlight-primary);
      color: #fff;
      cursor: pointer;
    `;
    saveBtn.addEventListener("click", async () => {
      const sel = (modal as any)._selectedSkills as Set<string>;
      const newPrompt = promptArea.value.trim();
      const newSkillIds = Array.from(sel);

      // Update chat options
      stateManager.setOptions({
        systemPrompt: newPrompt || undefined,
        skillIds: newSkillIds.length > 0 ? newSkillIds : undefined,
      } as any);

      // Persist to conversation metadata
      await store.updateConversationMetadata({
        systemPrompt: newPrompt || undefined,
        skillIds: newSkillIds.length > 0 ? newSkillIds : undefined,
      });

      // Refresh banner
      if (banner.parentNode) {
        const newBanner = await this.createPromptBanner(doc, stateManager);
        banner.parentNode.replaceChild(newBanner, banner);
      }

      overlay.remove();
    });
    rightActions.appendChild(saveBtn);
    actions.appendChild(rightActions);
    modal.appendChild(actions);

    overlay.appendChild(modal);

    // Append overlay to the banner's nearest positioned ancestor or the chat container
    const chatContainerEl =
      banner.closest("#assistant-messages-area")?.parentElement ||
      banner.parentElement;
    if (chatContainerEl) {
      (chatContainerEl as HTMLElement).style.position = "relative";
      chatContainerEl.appendChild(overlay);
    } else {
      const appendTarget = doc.body || doc.documentElement;
      if (appendTarget) appendTarget.appendChild(overlay);
    }
    promptArea.focus();
  }

  /**
   * Create the Papers Table tab content
   */
  private static async createTableTabContent(
    doc: Document,
    item: Zotero.Item,
  ): Promise<HTMLElement> {
    const isDetachedWindow = !!doc.getElementById("seerai-detached-window");
    const tableContainer = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "papers-table-container" },
      styles: {
        display: "flex",
        flexDirection: "column",
        height: "100%",
        width: "100%",
        overflow: "hidden",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    // Load table data
    const tableData = await this.loadTableData();

    // === TOOLBAR ===
    const toolbar = this.createTableToolbar(doc, item);
    tableContainer.appendChild(toolbar);

    // === TABLE + SIDE STRIP CONTAINER ===
    const tableWithSideStrip = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "row",
        flex: "1",
        overflow: "hidden",
        minHeight: "0",
        width: "100%",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    // === TABLE WRAPPER ===
    const tableWrapper = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "table-wrapper" },
      styles: {
        flex: "1",
        minWidth: "0",
      },
    });

    if (tableData.rows.length === 0) {
      // Empty state
      const emptyState = this.createTableEmptyState(doc, item);
      tableWrapper.appendChild(emptyState);
    } else {
      // Render table
      const table = this.createPapersTable(doc, tableData);
      tableWrapper.appendChild(table);
    }

    tableWithSideStrip.appendChild(tableWrapper);

    // === SIDE STRIP (Right Side) ===
    const sideStrip = this.createTableSideStrip(doc, item, tableWithSideStrip);
    tableWithSideStrip.appendChild(sideStrip);

    tableContainer.appendChild(tableWithSideStrip);

    return tableContainer;
  }

  /**
   * Create vertical side strip for Tables tab with 3 action buttons
   * Matches the Search tab's side strip pattern
   */
  private static createTableSideStrip(
    doc: Document,
    item: Zotero.Item,
    container: HTMLElement,
  ): HTMLElement {
    const isDetachedWindow = !!doc.getElementById("seerai-detached-window");
    const sideStrip = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        width: "48px",
        minWidth: "48px",
        display: "flex",
        flexDirection: "column",
        height: "100%",
        position: "relative",
        backgroundColor: "var(--background-secondary)",
        borderLeft: "1px solid var(--border-primary)",
        paddingTop: isDetachedWindow ? "20px" : "120px",
        paddingBottom: isDetachedWindow ? "20px" : "300px",
        boxSizing: "border-box",
        transition: "background-color 0.2s ease",
      },
    });

    sideStrip.addEventListener("mouseenter", () => {
      sideStrip.style.backgroundColor = "rgba(0, 0, 0, 0.02)";
    });
    sideStrip.addEventListener("mouseleave", () => {
      sideStrip.style.backgroundColor = "var(--background-secondary)";
    });

    // Sticky container for buttons - Floating Glassmorphism Style
    const stickyContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        position: "sticky",
        top: isDetachedWindow ? "10px" : "180px",
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        gap: "12px",
        padding: "16px 0",
        width: "100%",
        alignSelf: "center",
        zIndex: "100",
      },
    });
    sideStrip.appendChild(stickyContainer);

    // Helper for side buttons (same pattern as search tab)
    const createSideBtn = (
      icon: string,
      title: string,
      onClick: (e: Event) => void,
    ) => {
      const btn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: icon },
        attributes: { title: title },
        styles: {
          width: "24px",
          height: "24px",
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          border: "1px solid var(--border-primary)",
          borderRadius: "4px",
          backgroundColor: "var(--background-primary)",
          cursor: "pointer",
          fontSize: "14px",
          color: "var(--text-primary)",
          transition: "all 0.2s ease",
        },
        listeners: [
          {
            type: "click",
            listener: onClick,
          },
        ],
      });

      // Hover effects
      btn.addEventListener("mouseenter", () => {
        btn.style.backgroundColor = "var(--highlight-primary)";
        btn.style.color = "var(--highlight-text)";
        btn.style.borderColor = "var(--highlight-primary)";
      });
      btn.addEventListener("mouseleave", () => {
        btn.style.backgroundColor = "var(--background-primary)";
        btn.style.color = "var(--text-primary)";
        btn.style.borderColor = "var(--border-primary)";
      });

      return btn;
    };

    // 1. (+) Add Column - immediately adds a new column with inline editing
    const addColumnBtn = createSideBtn(
      "➕",
      "Add Analysis Column",
      async (e) => {
        e.stopPropagation();
        await this.addImmediateTableColumn(doc, item, container);
      },
    );
    stickyContainer.appendChild(addColumnBtn);

    // 2. (⚡) Generate All
    const generateAllBtn = createSideBtn("⚡", "Generate All Analysis", (e) => {
      e.stopPropagation();
      this.generateAllEmptyColumns(doc, item);
    });
    stickyContainer.appendChild(generateAllBtn);

    // 3. (⚙️) Settings
    const settingsBtn = createSideBtn(
      "⚙️",
      "Manage Columns & Settings",
      (e) => {
        e.stopPropagation();
        this.showTableSettingsPopover(
          doc,
          e.currentTarget as HTMLElement,
          container,
          item,
        );
      },
    );
    stickyContainer.appendChild(settingsBtn);

    // === BULK ACTIONS (Hidden by default) ===
    const bulkActionsContainer = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "table-bulk-actions" },
      styles: {
        display: "none", // Hidden by default
        flexDirection: "column",
        gap: "8px",
        marginTop: "16px",
        paddingTop: "16px",
        borderTop: "1px solid var(--border-primary)",
        width: "100%",
        alignItems: "center",
      },
    });

    // 3b. (📋) Copy Selected
    const copySelectedBtn = createSideBtn(
      "📋",
      "Copy Selected to Clipboard",
      async (e) => {
        e.stopPropagation();
        if (!currentTableData || currentTableData.selectedRowIds.size === 0)
          return;
        const rowsToCopy = currentTableData.rows.filter((r) =>
          currentTableData!.selectedRowIds.has(r.paperId),
        );
        const cols =
          currentTableConfig?.columns.filter((c) => c.visible) ||
          defaultColumns.filter((c) => c.visible);

        const references: string[] = [];
        const paperDetails: string[] = [];

        rowsToCopy.forEach((row, index) => {
          const refNum = index + 1;
          const title = row.data["title"] || "Untitled";
          const author = row.data["author"] || "Unknown";
          const year = row.data["year"] || "";
          const zoteroKey = row.data["zoteroKey"] || "";

          references.push(
            `[${refNum}]${zoteroKey ? ` ${zoteroKey} —` : ""} ${author}${year ? `, ${year}` : ""} — ${title}`,
          );

          let paperSection = `### [${refNum}] ${title}\n`;

          if (author) paperSection += `**Authors:** ${author}\n`;
          if (year) paperSection += `**Year:** ${year}\n`;
          if (row.data["zoteroKey"])
            paperSection += `**Zotero Key:** ${row.data["zoteroKey"]}\n`;
          if (row.data["DOI"]) paperSection += `**DOI:** ${row.data["DOI"]}\n`;

          cols.forEach((col) => {
            if (
              ["title", "author", "year", "zoteroKey", "DOI"].includes(col.id)
            )
              return;
            const value = row.data[col.id];
            if (value) {
              paperSection += `**${col.name}:** ${value}\n`;
            }
          });

          paperDetails.push(paperSection);
        });

        const textContent = [
          "## References",
          ...references,
          "",
          "## Detailed Data",
          "",
          ...paperDetails,
        ].join("\n");

        new ztoolkit.Clipboard().addText(textContent, "text/unicode").copy();
        const progressWindow = new Zotero.ProgressWindow({
          closeOnClick: true,
        });
        progressWindow.changeHeadline("Copied");
        progressWindow.addDescription(
          `${rowsToCopy.length} paper(s) copied as structured markdown`,
        );
        progressWindow.show();
        progressWindow.startCloseTimer(2000);
      },
    );
    bulkActionsContainer.appendChild(copySelectedBtn);

    // 4. (💾) Save Selected
    const saveSelectedBtn = createSideBtn(
      "💾",
      "Save Selected as Notes",
      async (e) => {
        e.stopPropagation();
        if (!currentTableData || currentTableData.selectedRowIds.size === 0)
          return;
        if (
          doc.defaultView?.confirm(
            `Save ${currentTableData.selectedRowIds.size} selected rows as notes?`,
          )
        ) {
          // Filter rows
          const rowsToSave = currentTableData.rows.filter((r) =>
            currentTableData!.selectedRowIds.has(r.paperId),
          );
          const cols = currentTableConfig?.columns || defaultColumns;
          let saved = 0;
          for (const row of rowsToSave) {
            if (await this.saveRowAsNote(row, cols)) saved++;
          }
          doc.defaultView?.alert(`Saved ${saved} notes.`);
        }
      },
    );
    bulkActionsContainer.appendChild(saveSelectedBtn);

    // Tag Selected (bulk tag generation)
    const tagSelectedBtn = createSideBtn(
      "🏷️",
      "Generate Tags for Selected",
      async (e) => {
        e.stopPropagation();
        if (!currentTableData || currentTableData.selectedRowIds.size === 0)
          return;
        const selectedRows = currentTableData.rows.filter((r) =>
          currentTableData!.selectedRowIds.has(r.paperId),
        );
        const btn = e.currentTarget as HTMLElement;
        const originalText = btn.innerText;
        btn.innerText = "⏳";
        btn.style.cursor = "wait";

        let successCount = 0;
        let errorCount = 0;

        for (const row of selectedRows) {
          try {
            await this.generateTagsForItemSilent(row);
            successCount++;
          } catch (err) {
            errorCount++;
            Zotero.debug(
              `[seerai] Error generating tags for ${row.paperId}: ${err}`,
            );
          }
        }

        btn.innerText = "✓";
        btn.style.color = "#4CAF50";
        btn.title = `Tagged ${successCount} items${errorCount > 0 ? `, ${errorCount} errors` : ""}`;
        setTimeout(() => {
          btn.innerText = originalText;
          btn.style.color = "";
          btn.title = "Generate Tags for Selected";
        }, 3000);
        btn.style.cursor = "pointer";
      },
    );
    bulkActionsContainer.appendChild(tagSelectedBtn);

    // 5. (⚡) Generate/Regenerate Selected
    const genSelectedBtn = createSideBtn(
      "⚡",
      "Generate/Regenerate Selected",
      (e) => {
        e.stopPropagation();
        if (!currentTableData || currentTableData.selectedRowIds.size === 0)
          return;
        // Logic will be handled in generateAllEmptyColumns by checking selection
        this.generateAllEmptyColumns(doc, item);
      },
    );
    bulkActionsContainer.appendChild(genSelectedBtn);

    // 5b. (🔄) Regenerate Previously Generated
    const regenSelectedBtn = createSideBtn(
      "🔄",
      "Regenerate Previously Generated",
      (e) => {
        e.stopPropagation();
        if (!currentTableData || currentTableData.selectedRowIds.size === 0)
          return;
        this.regenerateSelectedColumns(doc, item);
      },
    );
    bulkActionsContainer.appendChild(regenSelectedBtn);

    // 5c. (📁) Add to Folder
    const addToFolderBtn = createSideBtn(
      "📁",
      "Add Selected to Folder",
      async (e) => {
        e.stopPropagation();
        if (!currentTableData || currentTableData.selectedRowIds.size === 0)
          return;
        await this.showAddToFolderPopup(doc, e.currentTarget as HTMLElement);
      },
    );
    bulkActionsContainer.appendChild(addToFolderBtn);

    // 6. (🗑️) Trash (Remove from Table)
    const trashSelectedBtn = createSideBtn(
      "🗑️",
      "Remove Selected from Table",
      async (e) => {
        e.stopPropagation();
        if (!currentTableData || currentTableData.selectedRowIds.size === 0)
          return;
        if (
          doc.defaultView?.confirm(
            `Remove ${currentTableData.selectedRowIds.size} papers from this table?`,
          )
        ) {
          if (currentTableConfig) {
            const idsToRemove = Array.from(currentTableData.selectedRowIds);
            currentTableConfig.addedPaperIds =
              currentTableConfig.addedPaperIds.filter(
                (id) => !idsToRemove.includes(id),
              );
            // Cleanup generated data
            idsToRemove.forEach((id) => {
              if (currentTableConfig!.generatedData?.[id]) {
                delete currentTableConfig!.generatedData![id];
              }
            });
            const tableStore = getTableStore();
            await tableStore.saveConfig(currentTableConfig);
            // Force immediate refresh with container validation
            await this.forceRefreshTable();
          }
        }
      },
    );
    bulkActionsContainer.appendChild(trashSelectedBtn);

    // 7. (💣) Bomb (Delete from Zotero)
    const bombSelectedBtn = createSideBtn(
      "💣",
      "Delete Selected from Zotero",
      async (e) => {
        e.stopPropagation();
        if (!currentTableData || currentTableData.selectedRowIds.size === 0)
          return;
        if (
          doc.defaultView?.confirm(
            `PERMANENTLY DELETE ${currentTableData.selectedRowIds.size} items from Zotero?\n\nThis cannot be undone!`,
          )
        ) {
          const idsToDelete = Array.from(currentTableData.selectedRowIds);
          try {
            await Zotero.Items.erase(idsToDelete);
            // Also remove from config
            if (currentTableConfig) {
              currentTableConfig.addedPaperIds =
                currentTableConfig.addedPaperIds.filter(
                  (id) => !idsToDelete.includes(id),
                );
              idsToDelete.forEach((id) => {
                if (currentTableConfig!.generatedData?.[id]) {
                  delete currentTableConfig!.generatedData![id];
                }
              });
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
            }
            // Force immediate refresh with container validation
            await this.forceRefreshTable();
          } catch (err) {
            doc.defaultView?.alert(`Error deleting items: ${err}`);
          }
        }
      },
    );
    // Style bomb button red
    bombSelectedBtn.style.color = "#c62828";
    bombSelectedBtn.style.borderColor = "#c62828";
    bulkActionsContainer.appendChild(bombSelectedBtn);

    stickyContainer.appendChild(bulkActionsContainer);

    return sideStrip;
  }

  /**
       * Create the Search tab content with Semantic Scholar integration
  
       */
  private static async createSearchTabContent(
    doc: Document,
    item: Zotero.Item,
  ): Promise<HTMLElement> {
    // Load search column configuration
    searchColumnConfig = await loadSearchColumnConfig();

    const searchContainer = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "search-tab-container" },
      styles: {
        display: "flex",
        flexDirection: "column",
        height: "100%",
        overflow: "hidden",
        gap: "8px",
        padding: "8px",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    // === SEARCH INPUT ===
    const searchInputContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "8px",
        alignItems: "center",
        flexWrap: "wrap",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    const searchInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "text",
        placeholder: "🔍 Search Semantic Scholar...",
        value: currentSearchState.query || "",
      },
      properties: { id: "semantic-scholar-search-input" },
      styles: {
        flex: "1",
        minWidth: "0",
        padding: "10px 14px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "13px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        boxSizing: "border-box",
      },
    }) as HTMLInputElement;

    const searchBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Search" },
      styles: {
        padding: "10px 20px",
        backgroundColor: "var(--highlight-primary)",
        color: "var(--highlight-text)",
        border: "none",
        borderRadius: "6px",
        fontSize: "13px",
        fontWeight: "500",
        cursor: "pointer",
        flexShrink: "0",
        whiteSpace: "nowrap",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            currentSearchState.query = searchInput.value;
            currentSearchResults = [];
            currentSearchToken = null;
            await this.performSearch(doc);
          },
        },
      ],
    });

    // Enter key triggers search
    searchInput.addEventListener("keypress", async (e: Event) => {
      const ke = e as KeyboardEvent;
      if (ke.key === "Enter") {
        currentSearchState.query = searchInput.value;
        currentSearchResults = [];
        currentSearchToken = null;
        await this.performSearch(doc);
      }
    });

    // Query syntax help tooltip
    const syntaxHelp = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "❓" },
      attributes: {
        title:
          'Syntax: "phrase" | word1+word2 | word1|word2 | -exclude | word* | word~3',
      },
      styles: {
        fontSize: "14px",
        cursor: "help",
        opacity: "0.6",
        flexShrink: "0",
      },
    });

    // Suggestions button (replaces auto-dropdown with user-triggered action)
    const suggestionsBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "💡", title: "Get suggestions" },
      styles: {
        padding: "8px 10px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "14px",
        cursor: "pointer",
        marginLeft: "4px",
        flexShrink: "0",
      },
    });

    // Suggestions dropdown container
    const suggestionsDropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "suggestions-dropdown" },
      styles: {
        display: "none",
        position: "absolute",
        top: "100%",
        left: "0",
        right: "0",
        marginTop: "4px",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
        zIndex: "9999",
        maxHeight: "250px",
        overflowY: "auto",
      },
    });

    // Close dropdown when clicking outside
    doc.addEventListener("click", (e: Event) => {
      if (
        !suggestionsDropdown.contains(e.target as Node) &&
        e.target !== suggestionsBtn
      ) {
        suggestionsDropdown.style.display = "none";
      }
    });

    // Suggestions button click handler
    suggestionsBtn.addEventListener("click", async () => {
      const query = searchInput.value.trim();

      if (query.length < 2) {
        // Show message if query too short
        suggestionsDropdown.innerHTML = "";
        const msgDiv = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "16px",
            textAlign: "center",
            color: "var(--text-secondary)",
            fontSize: "12px",
          },
        });
        msgDiv.innerHTML = "Type at least 2 characters to get suggestions";
        suggestionsDropdown.appendChild(msgDiv);
        suggestionsDropdown.style.display = "block";
        return;
      }

      // Show loading state
      suggestionsDropdown.innerHTML = "";
      const loadingDiv = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          padding: "16px",
          textAlign: "center",
          color: "var(--text-secondary)",
          fontSize: "12px",
        },
      });
      loadingDiv.innerHTML = "⏳ Loading suggestions...";
      suggestionsDropdown.appendChild(loadingDiv);
      suggestionsDropdown.style.display = "block";

      try {
        const suggestions = await semanticScholarService.autocomplete(query);
        suggestionsDropdown.innerHTML = "";

        if (suggestions.length === 0) {
          const noResults = ztoolkit.UI.createElement(doc, "div", {
            styles: {
              padding: "16px",
              textAlign: "center",
              color: "var(--text-secondary)",
              fontSize: "12px",
            },
          });
          noResults.innerHTML = `No suggestions found for "${query}"`;
          suggestionsDropdown.appendChild(noResults);
          return;
        }

        // Header
        const header = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "8px 12px",
            fontSize: "11px",
            fontWeight: "600",
            color: "var(--text-secondary)",
            borderBottom: "1px solid var(--border-primary)",
            backgroundColor: "var(--background-secondary)",
          },
        });
        header.innerHTML = `💡 ${suggestions.length} suggestion${suggestions.length > 1 ? "s" : ""} for "${query}"`;
        suggestionsDropdown.appendChild(header);

        suggestions.slice(0, 8).forEach((sugg) => {
          const item = ztoolkit.UI.createElement(doc, "div", {
            styles: {
              padding: "10px 12px",
              fontSize: "12px",
              cursor: "pointer",
              borderBottom: "1px solid var(--border-primary)",
              lineHeight: "1.4",
            },
          });
          item.innerText = sugg.title;

          item.addEventListener("mouseenter", () => {
            item.style.backgroundColor = "var(--background-secondary)";
          });
          item.addEventListener("mouseleave", () => {
            item.style.backgroundColor = "transparent";
          });
          item.addEventListener("click", async () => {
            searchInput.value = sugg.title;
            currentSearchState.query = sugg.title;
            suggestionsDropdown.style.display = "none";
            currentSearchResults = [];
            await this.performSearch(doc);
          });

          suggestionsDropdown.appendChild(item);
        });
      } catch (e) {
        Zotero.debug(`[seerai] Suggestions error: ${e}`);
        suggestionsDropdown.innerHTML = "";
        const errorDiv = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "16px",
            textAlign: "center",
            color: "var(--error-color, #d32f2f)",
            fontSize: "12px",
          },
        });
        errorDiv.innerHTML = "⚠️ Failed to load suggestions";
        suggestionsDropdown.appendChild(errorDiv);
      }
    });

    // AI Query Refiner button (🤖)
    const aiRefineBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "🤖",
        title: "AI: Refine query for Semantic Scholar",
      },
      styles: {
        padding: "8px 10px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "14px",
        cursor: "pointer",
        marginLeft: "4px",
        flexShrink: "0",
      },
    });

    // AI Refine dropdown container
    const aiRefineDropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "ai-refine-dropdown" },
      styles: {
        display: "none",
        position: "absolute",
        top: "100%",
        left: "0",
        right: "0",
        marginTop: "4px",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
        zIndex: "9999",
        maxHeight: "300px",
        overflowY: "auto",
      },
    });

    // Close AI dropdown when clicking outside
    doc.addEventListener("click", (e: Event) => {
      if (
        !aiRefineDropdown.contains(e.target as Node) &&
        e.target !== aiRefineBtn
      ) {
        aiRefineDropdown.style.display = "none";
      }
    });

    // AI Refine button click handler
    aiRefineBtn.addEventListener("click", async () => {
      const userInput = searchInput.value.trim();

      if (userInput.length < 3) {
        aiRefineDropdown.innerHTML = "";
        const msgDiv = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "16px",
            textAlign: "center",
            color: "var(--text-secondary)",
            fontSize: "12px",
          },
        });
        msgDiv.innerHTML =
          "Enter your search criteria, research question, or PICO/FINER parameters";
        aiRefineDropdown.appendChild(msgDiv);
        aiRefineDropdown.style.display = "block";
        return;
      }

      // Check for model config
      const activeModel = getActiveModelConfig();
      if (!activeModel) {
        aiRefineDropdown.innerHTML = "";
        const errorDiv = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "16px",
            textAlign: "center",
            color: "var(--error-color, #d32f2f)",
            fontSize: "12px",
          },
        });
        errorDiv.innerHTML =
          "⚠️ No AI model configured. Please add a model in Settings.";
        aiRefineDropdown.appendChild(errorDiv);
        aiRefineDropdown.style.display = "block";
        return;
      }

      // Show loading state
      aiRefineDropdown.innerHTML = "";
      const loadingDiv = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          padding: "16px",
          textAlign: "center",
          color: "var(--text-secondary)",
          fontSize: "12px",
        },
      });
      loadingDiv.innerHTML = "🤖 AI is refining your query...";
      aiRefineDropdown.appendChild(loadingDiv);
      aiRefineDropdown.style.display = "block";

      try {
        const systemPrompt = `You are a search query optimization expert for Semantic Scholar academic paper search.

Your task is to convert user input (which may be natural language questions, research objectives, PICO/FINER criteria, PRISMA requirements, or any study parameters) into optimized search queries for Semantic Scholar.

Semantic Scholar Query Syntax:
- Use + between required terms: "machine learning+healthcare" 
- Use | for OR between alternatives: "cancer|tumor|neoplasm"
- Use - to exclude terms: "diabetes -type1"
- Use quotes for exact phrases: "deep learning"
- Use * for prefix/suffix wildcard: "neuro*"
- Use ~ for proximity: "gene~3 expression" (within 3 words)

Guidelines:
1. Extract key concepts from user input
2. Include synonyms using | operator
3. Combine related required terms with +
4. Exclude irrelevant concepts if mentioned
5. Use quotes for multi-word concepts
6. Keep the query focused but comprehensive
7. Output ONLY the refined search query, nothing else

Examples:
Input: "I want papers about using AI for diagnosing kidney diseases"
Output: "artificial intelligence"|"machine learning"|"deep learning"+"kidney disease"|"renal disease"|nephropathy+diagnosis

Input: "PICO: Population=elderly patients, Intervention=exercise, Outcome=cognitive function"
Output: elderly|geriatric|"older adults"+exercise|"physical activity"+"cognitive function"|cognition|"mental performance"

Input: "Systematic review on COVID-19 vaccines effectiveness"  
Output: "COVID-19"|"SARS-CoV-2"|coronavirus+vaccine|vaccination+effectiveness|efficacy+"systematic review"|meta-analysis`;

        const messages = [
          { role: "system" as const, content: systemPrompt },
          { role: "user" as const, content: userInput },
        ];

        let refinedQuery = "";

        await openAIService.chatCompletionStream(
          messages,
          {
            onToken: (token) => {
              refinedQuery += token;
              // Update live
              aiRefineDropdown.innerHTML = "";
              const previewDiv = ztoolkit.UI.createElement(doc, "div", {
                styles: { padding: "12px" },
              });
              previewDiv.innerHTML = `
                            <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 8px;">🤖 AI Refined Query:</div>
                            <div style="font-family: monospace; font-size: 12px; padding: 8px; background: var(--background-secondary); border-radius: 4px; word-break: break-word;">${refinedQuery}</div>
                        `;
              aiRefineDropdown.appendChild(previewDiv);
            },
            onComplete: (content) => {
              refinedQuery = content.trim();
              // Show final result with action buttons
              aiRefineDropdown.innerHTML = "";

              const resultDiv = ztoolkit.UI.createElement(doc, "div", {
                styles: { padding: "12px" },
              });

              const headerDiv = ztoolkit.UI.createElement(doc, "div", {
                styles: {
                  fontSize: "11px",
                  color: "var(--text-secondary)",
                  marginBottom: "8px",
                },
              });
              headerDiv.innerHTML = "🤖 AI Refined Query:";
              resultDiv.appendChild(headerDiv);

              const queryDiv = ztoolkit.UI.createElement(doc, "div", {
                styles: {
                  fontFamily: "monospace",
                  fontSize: "12px",
                  padding: "8px",
                  backgroundColor: "var(--background-secondary)",
                  borderRadius: "4px",
                  wordBreak: "break-word",
                  marginBottom: "12px",
                },
              });
              queryDiv.innerText = refinedQuery;
              resultDiv.appendChild(queryDiv);

              // Action buttons
              const actionsDiv = ztoolkit.UI.createElement(doc, "div", {
                styles: {
                  display: "flex",
                  gap: "8px",
                },
              });

              const useBtn = ztoolkit.UI.createElement(doc, "button", {
                namespace: "html",
                properties: { innerText: "✓ Use & Search" },
                styles: {
                  flex: "1",
                  padding: "8px 12px",
                  backgroundColor: "#1976d2",
                  color: "#fff",
                  border: "none",
                  borderRadius: "4px",
                  fontSize: "12px",
                  cursor: "pointer",
                },
              });
              useBtn.addEventListener("click", async () => {
                searchInput.value = refinedQuery;
                currentSearchState.query = refinedQuery;
                aiRefineDropdown.style.display = "none";
                currentSearchResults = [];
                await this.performSearch(doc);
              });

              const copyBtn = ztoolkit.UI.createElement(doc, "button", {
                namespace: "html",
                properties: { innerText: "📋 Copy" },
                styles: {
                  padding: "8px 12px",
                  backgroundColor: "var(--background-secondary)",
                  color: "var(--text-primary)",
                  border: "1px solid var(--border-primary)",
                  borderRadius: "4px",
                  fontSize: "12px",
                  cursor: "pointer",
                },
              });
              copyBtn.addEventListener("click", () => {
                new ztoolkit.Clipboard()
                  .addText(refinedQuery, "text/unicode")
                  .copy();
                copyBtn.innerText = "✓ Copied!";
                setTimeout(() => {
                  copyBtn.innerText = "📋 Copy";
                }, 1500);
              });

              actionsDiv.appendChild(useBtn);
              actionsDiv.appendChild(copyBtn);
              resultDiv.appendChild(actionsDiv);
              aiRefineDropdown.appendChild(resultDiv);
            },
            onError: (error) => {
              aiRefineDropdown.innerHTML = "";
              const errorDiv = ztoolkit.UI.createElement(doc, "div", {
                styles: {
                  padding: "16px",
                  textAlign: "center",
                  color: "var(--error-color, #d32f2f)",
                  fontSize: "12px",
                },
              });
              errorDiv.innerHTML = `⚠️ AI Error: ${error.message}`;
              aiRefineDropdown.appendChild(errorDiv);
            },
          },
          {
            apiURL: activeModel.apiURL,
            apiKey: activeModel.apiKey,
            model: activeModel.model,
          },
        );
      } catch (e) {
        Zotero.debug(`[seerai] AI Refine error: ${e}`);
        aiRefineDropdown.innerHTML = "";
        const errorDiv = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "16px",
            textAlign: "center",
            color: "var(--error-color, #d32f2f)",
            fontSize: "12px",
          },
        });
        errorDiv.innerHTML = "⚠️ Failed to refine query";
        aiRefineDropdown.appendChild(errorDiv);
      }
    });

    // === PAST SEARCHES BUTTON AND DROPDOWN ===
    const pastSearchesBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📜", title: "Past Searches" },
      styles: {
        padding: "8px 10px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "14px",
        cursor: "pointer",
        marginLeft: "4px",
        flexShrink: "0",
      },
    });

    const pastSearchesDropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "past-searches-dropdown" },
      styles: {
        display: "none",
        position: "absolute",
        top: "100%",
        left: "0",
        right: "0",
        marginTop: "4px",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
        zIndex: "9999",
        maxHeight: "350px",
        overflowY: "auto",
      },
    });

    // Close dropdown when clicking outside
    doc.addEventListener("click", (e: Event) => {
      if (
        !pastSearchesDropdown.contains(e.target as Node) &&
        e.target !== pastSearchesBtn
      ) {
        pastSearchesDropdown.style.display = "none";
      }
    });

    // Past Searches button click handler
    pastSearchesBtn.addEventListener("click", async () => {
      if (pastSearchesDropdown.style.display === "block") {
        pastSearchesDropdown.style.display = "none";
        return;
      }

      pastSearchesDropdown.innerHTML = "";
      const history = await getSearchHistory();

      if (history.length === 0) {
        const emptyMsg = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "24px",
            textAlign: "center",
            color: "var(--text-secondary)",
            fontSize: "12px",
          },
        });
        emptyMsg.textContent = "📭 No past searches yet";
        const br1 = doc.createElementNS(HTML_NS, "br");
        const br2 = doc.createElementNS(HTML_NS, "br");
        emptyMsg.appendChild(br1);
        emptyMsg.appendChild(br2);
        emptyMsg.appendChild(
          doc.createTextNode(
            "Your search history will appear here after you perform searches.",
          ),
        );
        pastSearchesDropdown.appendChild(emptyMsg);
      } else {
        // Header
        const header = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "10px 12px",
            fontSize: "11px",
            fontWeight: "600",
            color: "var(--text-secondary)",
            borderBottom: "1px solid var(--border-primary)",
            backgroundColor: "var(--background-secondary)",
          },
        });
        header.innerHTML = `📜 ${history.length} Past Searches`;
        pastSearchesDropdown.appendChild(header);

        // History entries
        history.forEach((entry) => {
          const item = ztoolkit.UI.createElement(doc, "div", {
            styles: {
              padding: "10px 12px",
              borderBottom: "1px solid var(--border-primary)",
              cursor: "pointer",
              display: "flex",
              alignItems: "center",
              gap: "8px",
            },
          });

          // Entry content
          const content = ztoolkit.UI.createElement(doc, "div", {
            styles: { flex: "1", minWidth: "0" },
          });

          // Query (truncated)
          const queryEl = ztoolkit.UI.createElement(doc, "div", {
            styles: {
              fontSize: "12px",
              fontWeight: "500",
              color: "var(--text-primary)",
              whiteSpace: "nowrap",
              overflow: "hidden",
              textOverflow: "ellipsis",
            },
          });
          queryEl.innerText =
            entry.query.length > 50
              ? entry.query.slice(0, 50) + "..."
              : entry.query;
          content.appendChild(queryEl);

          // Meta info
          const metaEl = ztoolkit.UI.createElement(doc, "div", {
            styles: {
              fontSize: "10px",
              color: "var(--text-secondary)",
              marginTop: "2px",
            },
          });
          const savedDate = new Date(entry.savedAt);
          const dateStr = this.formatRelativeTime(savedDate);
          metaEl.innerText = `${entry.results.length} results • ${dateStr}`;
          content.appendChild(metaEl);

          item.appendChild(content);

          // Delete button
          const deleteBtn = ztoolkit.UI.createElement(doc, "button", {
            namespace: "html",
            properties: { innerText: "🗑️", title: "Delete this search" },
            styles: {
              padding: "4px 6px",
              backgroundColor: "transparent",
              color: "var(--text-secondary)",
              border: "none",
              borderRadius: "4px",
              fontSize: "12px",
              cursor: "pointer",
              opacity: "0.6",
            },
          });
          deleteBtn.addEventListener("mouseenter", () => {
            deleteBtn.style.opacity = "1";
            deleteBtn.style.backgroundColor = "#ffebee";
          });
          deleteBtn.addEventListener("mouseleave", () => {
            deleteBtn.style.opacity = "0.6";
            deleteBtn.style.backgroundColor = "transparent";
          });
          deleteBtn.addEventListener("click", async (e) => {
            e.stopPropagation();
            await deleteSearchHistoryEntry(entry.id);
            item.remove();
            // Update header count
            const newHistory = await getSearchHistory();
            if (newHistory.length === 0) {
              pastSearchesDropdown.innerHTML = "";
              const emptyMsg = ztoolkit.UI.createElement(doc, "div", {
                styles: {
                  padding: "24px",
                  textAlign: "center",
                  color: "var(--text-secondary)",
                  fontSize: "12px",
                },
              });
              emptyMsg.innerHTML = "📭 No past searches";
              pastSearchesDropdown.appendChild(emptyMsg);
            } else {
              const headerEl =
                pastSearchesDropdown.querySelector("div:first-child");
              if (headerEl)
                headerEl.innerHTML = `📜 ${newHistory.length} Past Searches`;
            }
          });
          item.appendChild(deleteBtn);

          // Click to restore
          item.addEventListener("mouseenter", () => {
            item.style.backgroundColor = "var(--background-secondary)";
          });
          item.addEventListener("mouseleave", () => {
            item.style.backgroundColor = "transparent";
          });
          item.addEventListener("click", () => {
            this.restoreSearchFromHistory(entry, doc, searchInput);
            pastSearchesDropdown.style.display = "none";
          });

          pastSearchesDropdown.appendChild(item);
        });
      }

      pastSearchesDropdown.style.display = "block";
    });

    // Make input container relative for dropdown positioning
    searchInputContainer.style.position = "relative";
    searchInputContainer.appendChild(searchInput);
    searchInputContainer.appendChild(suggestionsBtn);
    searchInputContainer.appendChild(aiRefineBtn);
    searchInputContainer.appendChild(pastSearchesBtn);
    searchInputContainer.appendChild(syntaxHelp);
    searchInputContainer.appendChild(searchBtn);
    searchInputContainer.appendChild(suggestionsDropdown);
    searchInputContainer.appendChild(aiRefineDropdown);
    searchInputContainer.appendChild(pastSearchesDropdown);

    // === FILTERS (shown first) ===
    const filtersContainer = this.createSearchFilters(doc);
    searchContainer.appendChild(filtersContainer);

    // === SEARCH INPUT (below filters) ===
    searchContainer.appendChild(searchInputContainer);

    // === RESULTS AREA ===
    const resultsArea = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "semantic-scholar-results" },
      styles: {
        flex: "1",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-primary)",
        minWidth: "0",
        minHeight: "0",
        boxSizing: "border-box",
      },
    });

    // Render current results or empty state
    this.renderSearchResults(doc, resultsArea, item);
    searchContainer.appendChild(resultsArea);

    return searchContainer;
  }

  /**
   * Create the advanced search filters UI
   */
  private static createSearchFilters(doc: Document): HTMLElement {
    const container = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "search-filters-container" },
      styles: {
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        overflow: "hidden",
        marginBottom: "4px",
        minWidth: "0",
        boxSizing: "border-box",
      },
    });

    // Header (non-collapsible, always expanded)
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "none", // Hidden - no header needed
      },
    });

    // Filters body (always visible, compact)
    const filtersBody = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "search-filters-body" },
      styles: {
        display: "block",
        padding: "8px",
        backgroundColor: "var(--background-primary)",
      },
    });

    // === PRESET ROW ===
    const presetRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "8px",
        alignItems: "center",
        marginBottom: "12px",
        paddingBottom: "12px",
        borderBottom: "1px solid var(--border-primary)",
        flexWrap: "wrap",
        minWidth: "0",
      },
    });

    const presetLabel = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "📁 Presets:" },
      styles: {
        fontSize: "11px",
        fontWeight: "500",
        color: "var(--text-secondary)",
      },
    });
    presetRow.appendChild(presetLabel);

    // Preset dropdown
    const presetSelect = ztoolkit.UI.createElement(doc, "select", {
      properties: { id: "preset-select" },
      styles: {
        flex: "1",
        minWidth: "0",
        padding: "4px 8px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "11px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        boxSizing: "border-box",
      },
      listeners: [
        {
          type: "change",
          listener: () => {
            const selectedName = (presetSelect as HTMLSelectElement).value;
            if (!selectedName) return;

            const presets = getFilterPresets();
            const preset = presets.find((p) => p.name === selectedName);
            if (preset) {
              // Apply preset filters
              Object.assign(currentSearchState, preset.filters);
              // Re-render filters to show updated values
              const parent = container.parentElement;
              if (parent) {
                // Get references before removing
                const searchInputContainer = parent.querySelector(
                  "div[style*='position: relative']",
                );
                const resultsArea = parent.querySelector(
                  "#semantic-scholar-results",
                );

                container.remove();
                const newFilters = Assistant.createSearchFilters(doc);

                // Insert in correct order: filters, search input, results
                if (resultsArea) {
                  parent.insertBefore(newFilters, resultsArea);
                  if (searchInputContainer) {
                    parent.insertBefore(searchInputContainer, resultsArea);
                  }
                }

                // Select the preset in new dropdown
                const newSelect = newFilters.querySelector(
                  "#preset-select",
                ) as HTMLSelectElement;
                if (newSelect) newSelect.value = selectedName;
              }
            }
          },
        },
      ],
    }) as HTMLSelectElement;

    // Populate dropdown
    const defaultOpt = ztoolkit.UI.createElement(doc, "option", {
      attributes: { value: "" },
      properties: { innerText: "-- Select preset --" },
    });
    presetSelect.appendChild(defaultOpt);

    getFilterPresets().forEach((preset) => {
      const opt = ztoolkit.UI.createElement(doc, "option", {
        attributes: { value: preset.name },
        properties: { innerText: preset.name },
      });
      presetSelect.appendChild(opt);
    });

    presetRow.appendChild(presetSelect);

    // Save button
    const saveBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "💾 Save" },
      styles: {
        padding: "4px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "11px",
        backgroundColor: "#e3f2fd",
        color: "#1976d2",
        cursor: "pointer",
        fontWeight: "500",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            const name = getNextPresetName();
            addFilterPreset(name, currentSearchState);

            // Add new option to dropdown
            const newOpt = ztoolkit.UI.createElement(doc, "option", {
              attributes: { value: name },
              properties: { innerText: name },
            });
            presetSelect.appendChild(newOpt);
            presetSelect.value = name;

            Zotero.debug(`[seerai] Saved filter preset: ${name}`);
          },
        },
      ],
    });
    presetRow.appendChild(saveBtn);

    // Rename button
    const renameBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✏️" },
      attributes: { title: "Rename preset" },
      styles: {
        padding: "4px 8px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "11px",
        backgroundColor: "#fff3e0",
        color: "#e65100",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            const selectedName = presetSelect.value;
            if (!selectedName) return;

            const newName = (doc.defaultView as Window).prompt(
              "Enter new preset name:",
              selectedName,
            );
            if (!newName || newName === selectedName) return;

            // Rename in storage
            const presets = getFilterPresets();
            const preset = presets.find((p) => p.name === selectedName);
            if (preset) {
              preset.name = newName;
              saveFilterPresets(presets);
            }

            // Update dropdown option
            const opt = presetSelect.querySelector(
              `option[value="${selectedName}"]`,
            ) as HTMLOptionElement;
            if (opt) {
              opt.value = newName;
              opt.textContent = newName;
            }
            presetSelect.value = newName;

            Zotero.debug(
              `[seerai] Renamed preset: ${selectedName} -> ${newName}`,
            );
          },
        },
      ],
    });
    presetRow.appendChild(renameBtn);

    // Delete button
    const deleteBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "🗑️" },
      attributes: { title: "Delete preset" },
      styles: {
        padding: "4px 8px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "11px",
        backgroundColor: "#ffebee",
        color: "#c62828",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            const selectedName = presetSelect.value;
            if (!selectedName) return;

            deleteFilterPreset(selectedName);

            // Remove option from dropdown
            const opt = presetSelect.querySelector(
              `option[value="${selectedName}"]`,
            );
            if (opt) opt.remove();
            presetSelect.value = "";

            Zotero.debug(`[seerai] Deleted filter preset: ${selectedName}`);
          },
        },
      ],
    });
    presetRow.appendChild(deleteBtn);

    // Toggle button for advanced filters
    const toggleBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "⚙️ Advanced" },
      attributes: { title: "Show/hide advanced filters" },
      styles: {
        padding: "4px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "11px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
        marginLeft: "4px",
        display: "flex",
        alignItems: "center",
        gap: "4px",
      },
    });
    presetRow.appendChild(toggleBtn);

    filtersBody.appendChild(presetRow);

    // Collapsible container for advanced filters
    const advancedFiltersContainer = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "advanced-search-filters" },
      styles: {
        display: "none", // Hidden by default
        borderTop: "1px solid var(--border-primary)",
        marginTop: "8px",
        paddingTop: "8px",
      },
    });

    toggleBtn.addEventListener("click", () => {
      const isHidden = advancedFiltersContainer.style.display === "none";
      advancedFiltersContainer.style.display = isHidden ? "block" : "none";
      toggleBtn.style.backgroundColor = isHidden
        ? "var(--highlight-primary)"
        : "var(--background-secondary)";
      toggleBtn.style.color = isHidden
        ? "var(--highlight-text)"
        : "var(--text-primary)";
    });

    const gridStyle = {
      display: "grid",
      gridTemplateColumns: "1fr 1fr",
      gap: "6px",
      marginBottom: "6px",
    };

    const labelStyle = {
      fontSize: "10px",
      fontWeight: "500",
      color: "var(--text-secondary)",
      marginBottom: "2px",
      display: "block",
    };

    const inputStyle = {
      width: "100%",
      padding: "6px 10px",
      border: "1px solid var(--border-primary)",
      borderRadius: "4px",
      fontSize: "12px",
      backgroundColor: "var(--background-primary)",
      color: "var(--text-primary)",
      boxSizing: "border-box" as const,
    };

    // Row 1: Results limit + Year range
    const row1 = ztoolkit.UI.createElement(doc, "div", { styles: gridStyle });

    // Results per page (slider)
    const limitGroup = ztoolkit.UI.createElement(doc, "div", {});
    const limitLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: {
        innerText: `Results per page: ${currentSearchState.limit}`,
      },
      styles: labelStyle,
    });
    const limitSlider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "10",
        max: "100",
        step: "10",
        value: String(currentSearchState.limit),
      },
      styles: { ...inputStyle, padding: "0", cursor: "pointer" },
      listeners: [
        {
          type: "input",
          listener: (e: Event) => {
            const target = e.target as HTMLInputElement;
            currentSearchState.limit = parseInt(target.value, 10);
            limitLabel.innerText = `Results per page: ${currentSearchState.limit}`;
          },
        },
      ],
    }) as HTMLInputElement;
    limitGroup.appendChild(limitLabel);
    limitGroup.appendChild(limitSlider);
    row1.appendChild(limitGroup);

    // Year range
    const yearGroup = ztoolkit.UI.createElement(doc, "div", {});
    const yearLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Year range" },
      styles: labelStyle,
    });
    const yearInputs = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "4px", alignItems: "center" },
    });
    const yearStart = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "number",
        placeholder: "From",
        min: "1900",
        max: "2030",
        value: currentSearchState.yearStart || "",
      },
      styles: { ...inputStyle, width: "70px" },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            currentSearchState.yearStart = (e.target as HTMLInputElement).value;
          },
        },
      ],
    }) as HTMLInputElement;
    const yearDash = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "-" },
      styles: { color: "var(--text-secondary)" },
    });
    const yearEnd = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "number",
        placeholder: "To",
        min: "1900",
        max: "2030",
        value: currentSearchState.yearEnd || "",
      },
      styles: { ...inputStyle, width: "70px" },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            currentSearchState.yearEnd = (e.target as HTMLInputElement).value;
          },
        },
      ],
    }) as HTMLInputElement;
    yearInputs.appendChild(yearStart);
    yearInputs.appendChild(yearDash);
    yearInputs.appendChild(yearEnd);
    yearGroup.appendChild(yearLabel);
    yearGroup.appendChild(yearInputs);
    row1.appendChild(yearGroup);

    advancedFiltersContainer.appendChild(row1);

    // Row 2: Checkboxes (Has PDF, Hide Library Duplicates)
    const row2 = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "16px", marginBottom: "12px" },
    });

    const hasPdfCheck = this.createFilterCheckbox(
      doc,
      "📄 Has PDF",
      currentSearchState.openAccessPdf,
      (val) => {
        currentSearchState.openAccessPdf = val;
      },
    );
    row2.appendChild(hasPdfCheck);

    const hideDupsCheck = this.createFilterCheckbox(
      doc,
      "🚫 Hide Library Duplicates",
      currentSearchState.hideLibraryDuplicates,
      (val) => {
        currentSearchState.hideLibraryDuplicates = val;
      },
    );
    row2.appendChild(hideDupsCheck);

    advancedFiltersContainer.appendChild(row2);

    // Row 3: Min Citations + Sort By
    const row3 = ztoolkit.UI.createElement(doc, "div", { styles: gridStyle });

    const minCitGroup = ztoolkit.UI.createElement(doc, "div", {});
    const minCitLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Min citations" },
      styles: labelStyle,
    });
    const minCitInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "number",
        placeholder: "0",
        min: "0",
        value: String(currentSearchState.minCitationCount || ""),
      },
      styles: inputStyle,
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            const val = parseInt((e.target as HTMLInputElement).value, 10);
            currentSearchState.minCitationCount = isNaN(val) ? undefined : val;
          },
        },
      ],
    }) as HTMLInputElement;
    minCitGroup.appendChild(minCitLabel);
    minCitGroup.appendChild(minCitInput);
    row3.appendChild(minCitGroup);

    const sortGroup = ztoolkit.UI.createElement(doc, "div", {});
    const sortLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Sort by" },
      styles: labelStyle,
    });
    const sortSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: { ...inputStyle, appearance: "auto" as const },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            currentSearchState.sortBy = (e.target as HTMLSelectElement)
              .value as SearchState["sortBy"];
          },
        },
      ],
    }) as HTMLSelectElement;

    const sortOptions = [
      { value: "relevance", label: "Relevance" },
      { value: "citationCount:desc", label: "Most Cited" },
      { value: "publicationDate:desc", label: "Newest First" },
    ];
    sortOptions.forEach((opt) => {
      const option = ztoolkit.UI.createElement(doc, "option", {
        attributes: { value: opt.value },
        properties: { innerText: opt.label },
      }) as HTMLOptionElement;
      if (opt.value === currentSearchState.sortBy) option.selected = true;
      sortSelect.appendChild(option);
    });
    sortGroup.appendChild(sortLabel);
    sortGroup.appendChild(sortSelect);
    row3.appendChild(sortGroup);

    advancedFiltersContainer.appendChild(row3);

    // Row 4: Fields of Study multi-select
    const fosGroup = ztoolkit.UI.createElement(doc, "div", {
      styles: { marginBottom: "12px" },
    });
    const fosLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Fields of Study" },
      styles: labelStyle,
    });
    const fosContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexWrap: "wrap", gap: "4px" },
    });

    const commonFields = [
      "Computer Science",
      "Medicine",
      "Biology",
      "Physics",
      "Psychology",
      "Engineering",
    ];
    commonFields.forEach((field) => {
      const isSelected = currentSearchState.fieldsOfStudy.includes(field);
      const chip = ztoolkit.UI.createElement(doc, "span", {
        properties: {
          innerText: field,
          className: `fos-chip ${isSelected ? "selected" : ""}`,
        },
        styles: {
          padding: "4px 10px",
          borderRadius: "14px",
          fontSize: "10px",
          cursor: "pointer",
          backgroundColor: isSelected ? "#1976d2" : "transparent",
          color: isSelected ? "#ffffff" : "var(--text-primary)",
          border: isSelected ? "2px solid #1976d2" : "2px solid #888888",
          fontWeight: isSelected ? "600" : "400",
          transition: "all 0.15s ease",
        },
        listeners: [
          {
            type: "click",
            listener: () => {
              const idx = currentSearchState.fieldsOfStudy.indexOf(field);
              if (idx >= 0) {
                currentSearchState.fieldsOfStudy.splice(idx, 1);
                chip.style.backgroundColor = "transparent";
                chip.style.color = "var(--text-primary)";
                chip.style.border = "2px solid #888888";
                chip.style.fontWeight = "400";
              } else {
                currentSearchState.fieldsOfStudy.push(field);
                chip.style.backgroundColor = "#1976d2";
                chip.style.color = "#ffffff";
                chip.style.border = "2px solid #1976d2";
                chip.style.fontWeight = "600";
              }
            },
          },
        ],
      });
      fosContainer.appendChild(chip);
    });

    fosGroup.appendChild(fosLabel);
    fosGroup.appendChild(fosContainer);
    advancedFiltersContainer.appendChild(fosGroup);

    // Row 5: Publication Types
    const pubTypeGroup = ztoolkit.UI.createElement(doc, "div", {
      styles: { marginBottom: "12px" },
    });
    const pubTypeLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Publication Types" },
      styles: labelStyle,
    });
    const pubTypeContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexWrap: "wrap", gap: "4px" },
    });

    const pubTypes = [
      "JournalArticle",
      "Conference",
      "Review",
      "Book",
      "Dataset",
      "ClinicalTrial",
      "MetaAnalysis",
      "Study",
    ];
    pubTypes.forEach((ptype) => {
      const isSelected = currentSearchState.publicationTypes.includes(ptype);
      const chip = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: ptype.replace(/([A-Z])/g, " $1").trim() },
        styles: {
          padding: "4px 10px",
          borderRadius: "14px",
          fontSize: "10px",
          cursor: "pointer",
          backgroundColor: isSelected ? "#1976d2" : "transparent",
          color: isSelected ? "#ffffff" : "var(--text-primary)",
          border: isSelected ? "2px solid #1976d2" : "2px solid #888888",
          fontWeight: isSelected ? "600" : "400",
          transition: "all 0.15s ease",
        },
        listeners: [
          {
            type: "click",
            listener: () => {
              const idx = currentSearchState.publicationTypes.indexOf(ptype);
              if (idx >= 0) {
                currentSearchState.publicationTypes.splice(idx, 1);
                chip.style.backgroundColor = "transparent";
                chip.style.color = "var(--text-primary)";
                chip.style.border = "2px solid #888888";
                chip.style.fontWeight = "400";
              } else {
                currentSearchState.publicationTypes.push(ptype);
                chip.style.backgroundColor = "#1976d2";
                chip.style.color = "#ffffff";
                chip.style.border = "2px solid #1976d2";
                chip.style.fontWeight = "600";
              }
            },
          },
        ],
      });
      pubTypeContainer.appendChild(chip);
    });

    pubTypeGroup.appendChild(pubTypeLabel);
    pubTypeGroup.appendChild(pubTypeContainer);
    advancedFiltersContainer.appendChild(pubTypeGroup);

    // Row 6: Venue filter
    const venueGroup = ztoolkit.UI.createElement(doc, "div", {
      styles: { marginBottom: "8px" },
    });
    const venueLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Venue (e.g., Nature, Cell, ICML)" },
      styles: labelStyle,
    });
    const venueInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "text",
        placeholder: "Comma-separated venues...",
        value: currentSearchState.venue || "",
      },
      styles: { ...inputStyle, width: "100%" },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            currentSearchState.venue =
              (e.target as HTMLInputElement).value || undefined;
          },
        },
      ],
    }) as HTMLInputElement;
    venueGroup.appendChild(venueLabel);
    venueGroup.appendChild(venueInput);
    advancedFiltersContainer.appendChild(venueGroup);

    // Row 7: Save Location dropdown
    const saveLocationGroup = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        marginBottom: "8px",
        marginTop: "8px",
        paddingTop: "8px",
        borderTop: "1px solid var(--border-primary)",
      },
    });
    const saveLocationLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "📥 Save imported papers to:" },
      styles: { ...labelStyle, fontWeight: "600" },
    });
    const saveLocationSelect = ztoolkit.UI.createElement(doc, "select", {
      properties: { id: "save-location-select" },
      styles: {
        ...inputStyle,
        width: "100%",
        appearance: "auto" as const,
        marginTop: "4px",
      },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            currentSearchState.saveLocation = (
              e.target as HTMLSelectElement
            ).value;
            Zotero.debug(
              `[seerai] Save location changed to: ${currentSearchState.saveLocation}`,
            );
          },
        },
      ],
    }) as HTMLSelectElement;

    // Populate save location dropdown with libraries and collections
    this.populateSaveLocationSelect(saveLocationSelect);

    saveLocationGroup.appendChild(saveLocationSelect);
    advancedFiltersContainer.appendChild(saveLocationGroup);

    // Row 8: AI Insights Section (Directly Visible)
    const insightsHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        marginTop: "12px",
        paddingTop: "12px",
        borderTop: "1px solid var(--border-primary)",
        marginBottom: "8px",
        fontSize: "11px",
        fontWeight: "600",
        color: "var(--text-primary)",
        display: "flex",
        alignItems: "center",
        gap: "6px",
      },
    });
    insightsHeader.innerHTML =
      "<span>💡</span><span>AI Insights Configuration</span>";
    advancedFiltersContainer.appendChild(insightsHeader);

    const insightsSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        padding: "8px",
        backgroundColor: "var(--background-secondary)",
        borderRadius: "6px",
      },
    });

    // Auto Insights Toggle
    const autoInsightsToggle = this.createFilterCheckbox(
      doc,
      "Enable Auto AI Insights",
      Zotero.Prefs.get("extensions.seerai.searchAutoAiInsights") !== false,
      (val) => {
        Zotero.Prefs.set("extensions.seerai.searchAutoAiInsights", val);
      },
    );
    insightsSection.appendChild(autoInsightsToggle);

    // Prompt configuration
    const promptContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexDirection: "column", gap: "4px" },
    });
    const promptLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Synthesis System Prompt:" },
      styles: labelStyle,
    });
    const promptArea = ztoolkit.UI.createElement(doc, "textarea", {
      properties: {
        value:
          currentSearchState.searchAiInsightsPrompt ||
          (Zotero.Prefs.get(
            "extensions.seerai.searchAiInsightsPrompt",
          ) as string),
        rows: 4,
      },
      styles: {
        ...inputStyle,
        width: "100%",
        minHeight: "80px",
        fontFamily: "inherit",
        resize: "both",
        boxSizing: "border-box",
      },
    }) as HTMLTextAreaElement;
    promptArea.addEventListener("change", () => {
      currentSearchState.searchAiInsightsPrompt = promptArea.value;
      Zotero.Prefs.set(
        "extensions.seerai.searchAiInsightsPrompt",
        promptArea.value,
      );
    });
    promptContainer.appendChild(promptLabel);
    promptContainer.appendChild(promptArea);
    insightsSection.appendChild(promptContainer);

    // Length configuration with slider
    const lengthGroup = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        alignItems: "center",
        gap: "8px",
        flexWrap: "wrap",
      },
    });
    const lengthLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Response length:" },
      styles: { ...labelStyle, marginBottom: "0" },
    });
    const currentLength =
      currentSearchState.searchAiInsightsResponseLength ||
      (Zotero.Prefs.get(
        "extensions.seerai.searchAiInsightsResponseLength",
      ) as number) ||
      500;

    const lengthValue = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: `${currentLength} chars` },
      styles: {
        fontSize: "11px",
        color: "var(--text-secondary)",
        minWidth: "70px",
      },
    });
    const lengthSlider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "100",
        max: "2000",
        step: "100",
        value: String(currentLength),
      },
      styles: { flex: "1", minWidth: "100px", cursor: "pointer" },
    }) as HTMLInputElement;
    lengthSlider.addEventListener("input", () => {
      const val = parseInt(lengthSlider.value, 10);
      lengthValue.innerText = `${val} chars`;
    });
    lengthSlider.addEventListener("change", () => {
      const val = parseInt(lengthSlider.value, 10);
      currentSearchState.searchAiInsightsResponseLength = val;
      Zotero.Prefs.set("extensions.seerai.searchAiInsightsResponseLength", val);
    });

    lengthGroup.appendChild(lengthLabel);
    lengthGroup.appendChild(lengthSlider);
    lengthGroup.appendChild(lengthValue);
    insightsSection.appendChild(lengthGroup);

    // Citation Style configuration
    const styleGroup = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        alignItems: "center",
        gap: "8px",
        marginTop: "4px",
      },
    });
    const styleLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Citation Style:" },
      styles: { ...labelStyle, marginBottom: "0" },
    });
    const styleSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        ...inputStyle,
        width: "auto",
        minWidth: "150px",
        height: "24px",
        fontSize: "11px",
        padding: "0 4px",
      },
    }) as HTMLSelectElement;

    const citationStyles = [
      { value: "numbered", label: "Numbered ([1])" },
      { value: "apa", label: "APA (Author, Year)" },
      { value: "mla", label: "MLA (Author Page)" },
      { value: "chicago", label: "Chicago (Author Year)" },
      { value: "harvard", label: "Harvard (Author Year)" },
      { value: "ieee", label: "IEEE ([1])" },
    ];

    const currentStyle =
      (Zotero.Prefs.get(
        "extensions.seerai.searchAiInsightsCitationStyle",
      ) as string) || "numbered";

    citationStyles.forEach((s) => {
      const opt = doc.createElement("option");
      opt.value = s.value;
      opt.innerText = s.label;
      if (s.value === currentStyle) opt.selected = true;
      styleSelect.appendChild(opt);
    });

    styleSelect.addEventListener("change", () => {
      Zotero.Prefs.set(
        "extensions.seerai.searchAiInsightsCitationStyle",
        styleSelect.value,
      );
      Assistant.refreshAllCitations(doc);
    });

    styleGroup.appendChild(styleLabel);
    styleGroup.appendChild(styleSelect);
    insightsSection.appendChild(styleGroup);

    advancedFiltersContainer.appendChild(insightsSection);

    filtersBody.appendChild(advancedFiltersContainer);

    container.appendChild(filtersBody);
    return container;
  }

  /**
   * Helper to create a filter checkbox
   */
  private static createFilterCheckbox(
    doc: Document,
    label: string,
    checked: boolean,
    onChange: (val: boolean) => void,
  ): HTMLElement {
    const container = ztoolkit.UI.createElement(doc, "label", {
      styles: {
        display: "flex",
        alignItems: "center",
        gap: "6px",
        fontSize: "12px",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
    });

    const checkbox = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "checkbox" },
      styles: { cursor: "pointer" },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            onChange((e.target as HTMLInputElement).checked);
          },
        },
      ],
    }) as HTMLInputElement;
    checkbox.checked = checked;

    const labelText = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: label },
    });

    container.appendChild(checkbox);
    container.appendChild(labelText);
    return container;
  }

  /**
   * Perform the search and update results
   */
  private static async performSearch(doc: Document): Promise<void> {
    if (isSearching || !currentSearchState.query.trim()) return;

    isSearching = true;
    const resultsArea = doc.getElementById("semantic-scholar-results");
    if (!resultsArea) {
      isSearching = false;
      return;
    }

    // Determine if this is a pagination request (Show More) or a fresh search
    const isPagination = currentSearchResults.length > 0;

    if (isPagination) {
      // For pagination: show loading indicator BELOW existing content
      // The loading indicator is already placed by the click handler,
      // so we don't need to do anything else here
    } else {
      // For fresh searches: clear and show loading
      // Clear cached AI insights for new query
      currentSearchState.cachedAiInsights = undefined;
      resultsArea.innerHTML = "";
      const loadingEl = ztoolkit.UI.createElement(doc, "div", {
        properties: { id: "initial-search-loading" },
        styles: {
          padding: "40px",
          textAlign: "center",
          color: "var(--text-secondary)",
        },
      });
      loadingEl.innerHTML = `<div style="font-size: 24px; margin-bottom: 8px;">⏳</div><div>Searching Semantic Scholar...</div>`;
      resultsArea.appendChild(loadingEl);
    }

    try {
      // Check for API key
      // API Key is optional now, but checked for rate limiting internally

      // Build year filter
      let yearParam: string | undefined;
      if (currentSearchState.yearStart || currentSearchState.yearEnd) {
        const start = currentSearchState.yearStart || "";
        const end = currentSearchState.yearEnd || "";
        yearParam = `${start}-${end}`;
      }

      const result = await semanticScholarService.searchPapers({
        query: currentSearchState.query,
        limit: currentSearchState.limit,
        offset: currentSearchResults.length,
        year: yearParam,
        openAccessPdf: currentSearchState.openAccessPdf || undefined,
        fieldsOfStudy:
          currentSearchState.fieldsOfStudy.length > 0
            ? currentSearchState.fieldsOfStudy
            : undefined,
        publicationTypes:
          currentSearchState.publicationTypes.length > 0
            ? currentSearchState.publicationTypes
            : undefined,
        minCitationCount: currentSearchState.minCitationCount,
        venue: currentSearchState.venue,
      });

      Zotero.debug(
        `[seerai] Search response - total: ${result.total}, data length: ${result.data?.length ?? "undefined"}`,
      );

      // Defensive check: ensure result.data exists and is an array
      if (!result.data || !Array.isArray(result.data)) {
        Zotero.debug(
          `[seerai] Invalid search response - data is ${typeof result.data}`,
        );
        throw new Error("Invalid search response from API - no results data");
      }

      // Capture total count from result
      if (currentSearchResults.length === 0) {
        totalSearchResults = result.total || 0;
      }

      // Filter library duplicates if enabled
      let papers = result.data;
      const papersBeforeFilter = papers.length;
      if (currentSearchState.hideLibraryDuplicates && papers.length > 0) {
        papers = await this.filterLibraryDuplicates(papers);
        const filtered = papersBeforeFilter - papers.length;
        if (filtered > 0) {
          Zotero.debug(
            `[seerai] Duplicate filter: ${papersBeforeFilter} → ${papers.length} (removed ${filtered} already in library)`,
          );
        }
      }

      const previousCount = currentSearchResults.length;
      currentSearchResults = [...currentSearchResults, ...papers];

      // If this is a fresh search (previousCount === 0), render everything
      // Otherwise, just append the new cards
      if (previousCount === 0) {
        this.renderSearchResults(doc, resultsArea as HTMLElement, currentItem!);

        // Auto-save to search history (only for fresh searches with results)
        if (currentSearchResults.length > 0) {
          addSearchHistoryEntry({
            id: `search_${Date.now()}`,
            query: currentSearchState.query,
            state: { ...currentSearchState },
            results: [...currentSearchResults],
            totalResults: totalSearchResults,
            searchToken: currentSearchToken,
            savedAt: new Date().toISOString(),
          });
        }
      } else {
        // Append new cards without clearing existing content
        this.appendSearchCards(
          doc,
          resultsArea as HTMLElement,
          papers,
          currentItem!,
        );
      }
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : "Search failed";
      Zotero.debug(`[seerai] Search error: ${error}`);

      if (isPagination) {
        // For pagination errors: only remove loading indicator and show inline error
        const loadingIndicator =
          resultsArea.querySelector("#show-more-loading");
        if (loadingIndicator) {
          loadingIndicator.remove();
        }
        // Re-add the Show More button so user can retry
        const existingShowMore = resultsArea.querySelector("#show-more-btn");
        if (existingShowMore) {
          existingShowMore.remove();
        }
        const errorMsg = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "12px",
            textAlign: "center",
            color: "var(--error-color, #d32f2f)",
            fontSize: "12px",
          },
        });
        errorMsg.innerHTML = `⚠️ ${errorMessage}. <button id="retry-show-more" style="margin-left: 8px; cursor: pointer; padding: 4px 8px; border: 1px solid var(--border-primary); border-radius: 4px; background: var(--background-secondary);">Retry</button>`;
        resultsArea.appendChild(errorMsg);
        // Add retry handler
        const retryBtn = errorMsg.querySelector("#retry-show-more");
        if (retryBtn) {
          retryBtn.addEventListener("click", async () => {
            errorMsg.remove();
            await this.performSearch(doc);
          });
        }
      } else {
        // For fresh search errors: show full error
        resultsArea.innerHTML = "";
        const errorEl = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "40px",
            textAlign: "center",
            color: "var(--error-color, #d32f2f)",
          },
        });
        errorEl.innerHTML = `<div style="font-size: 24px; margin-bottom: 8px;">⚠️</div><div>${errorMessage}</div>`;
        resultsArea.appendChild(errorEl);
      }
    } finally {
      isSearching = false;
    }
  }

  /**
   * Filter out papers that already exist in any Zotero library (User or Group)
   */
  private static async filterLibraryDuplicates(
    papers: SemanticScholarPaper[],
  ): Promise<SemanticScholarPaper[]> {
    // Build lookup sets for DOI, PMID, and titles across ALL libraries
    const existingDOIs = new Set<string>();
    const existingPMIDs = new Set<string>();
    const existingTitles = new Set<string>();

    const libraries = Zotero.Libraries.getAll();

    for (const lib of libraries) {
      try {
        const libraryItems = await Zotero.Items.getAll(lib.libraryID);
        for (const item of libraryItems) {
          if (!item.isRegularItem()) continue;

          const doi = item.getField("DOI") as string;
          if (doi) existingDOIs.add(doi.toLowerCase());

          const extra = item.getField("extra") as string;
          if (extra) {
            const pmidMatch = extra.match(/PMID:\s*(\d+)/i);
            if (pmidMatch) existingPMIDs.add(pmidMatch[1]);
          }

          const title = item.getField("title") as string;
          if (title) existingTitles.add(title.toLowerCase().trim());
        }
      } catch (e) {
        Zotero.debug(
          `[seerai] Error checking duplicates in library ${lib.name}: ${e}`,
        );
      }
    }

    return papers.filter((paper) => {
      // Check DOI
      if (
        paper.externalIds?.DOI &&
        existingDOIs.has(paper.externalIds.DOI.toLowerCase())
      ) {
        return false;
      }
      // Check PMID
      if (
        paper.externalIds?.PMID &&
        existingPMIDs.has(paper.externalIds.PMID)
      ) {
        return false;
      }
      // Check title (exact match, case insensitive)
      if (paper.title && existingTitles.has(paper.title.toLowerCase().trim())) {
        return false;
      }
      return true;
    });
  }

  /**
   * Render search results in the results area
   */
  private static renderSearchResults(
    doc: Document,
    container: HTMLElement,
    item: Zotero.Item,
  ): void {
    container.textContent = "";

    if (currentSearchResults.length === 0 && !currentSearchState.query) {
      // Initial empty state
      const emptyState = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          padding: "60px 20px",
          textAlign: "center",
          color: "var(--text-secondary)",
        },
      });
      emptyState.innerHTML = `
                <div style="font-size: 48px; margin-bottom: 16px;">🔍</div>
                <div style="font-size: 14px; font-weight: 500; margin-bottom: 8px;">Search Semantic Scholar</div>
                <div style="font-size: 12px;">Enter a query to find relevant papers</div>
            `;
      container.appendChild(emptyState);
      return;
    }

    if (currentSearchResults.length === 0 && currentSearchState.query) {
      // No results
      const noResults = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          padding: "40px",
          textAlign: "center",
          color: "var(--text-secondary)",
        },
      });
      noResults.innerHTML = `
                <div style="font-size: 32px; margin-bottom: 8px;">📭</div>
                <div>No papers found for "${currentSearchState.query}"</div>
            `;
      container.appendChild(noResults);
      return;
    }

    // Total count header with export button
    const countHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "8px 12px",
        backgroundColor: "var(--background-secondary)",
        borderBottom: "1px solid var(--border-primary)",
        fontSize: "12px",
        color: "var(--text-secondary)",
        fontWeight: "500",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      },
    });

    const countText = ztoolkit.UI.createElement(doc, "span", {});
    const filterNote = currentSearchState.hideLibraryDuplicates
      ? " (hiding library duplicates)"
      : "";
    countText.innerHTML = `📊 Found <strong>${totalSearchResults.toLocaleString()}</strong> papers • Showing ${currentSearchResults.length}${filterNote}`;
    countHeader.appendChild(countText);

    const headerButtons = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "8px" },
    });

    // Export BibTeX button (Keep only this button in header)
    const exportBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📥 Export BibTeX" },
      styles: {
        padding: "4px 10px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async (e: Event) => {
            e.stopPropagation();
            const btn = e.target as HTMLButtonElement;
            const originalText = btn.textContent;
            btn.textContent = "⏳ Exporting...";
            btn.disabled = true;
            try {
              await this.exportResultsAsBibtex();
              btn.textContent = "✓ Exported!";
              setTimeout(() => {
                btn.textContent = originalText;
                btn.disabled = false;
              }, 2000);
            } catch (err) {
              btn.textContent = "⚠️ Failed";
              setTimeout(() => {
                btn.textContent = originalText;
                btn.disabled = false;
              }, 2000);
            }
          },
        },
      ],
    });
    headerButtons.appendChild(exportBtn);
    countHeader.appendChild(headerButtons);
    container.appendChild(countHeader);

    // AI Insights Container
    const summaryContainer = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "search-ai-summary-container" },
      styles: {
        display: "none",
        padding: "16px",
        margin: "12px",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
        maxHeight: "350px",
        overflowY: "auto",
        position: "relative",
        userSelect: "text", // Make text selectable
        cursor: "text",
      },
    });
    container.appendChild(summaryContainer);

    // Render Unified Search Results (Table Layout with Card First Column)
    const wrapper = this.renderUnifiedSearchResults(doc, container, item);
    container.appendChild(wrapper);

    // Auto-generate AI insights if enabled in settings
    const autoInsights = Zotero.Prefs.get(
      "extensions.seerai.searchAutoAiInsights",
    ) as boolean;

    // Check for cached insights first
    if (currentSearchState.cachedAiInsights) {
      // Display cached insights immediately
      this.displayCachedInsights(doc, currentSearchState.cachedAiInsights);
    } else if (autoInsights !== false) {
      // Run asynchronously without blocking the render
      this.generateSearchInsights(doc, container).catch((err) =>
        Zotero.debug(`[seerai] Auto AI insights error: ${err}`),
      );
    }
  }

  /**
   * Generate AI insights for current search results
   */
  private static async generateSearchInsights(
    doc: Document,
    container: HTMLElement,
  ): Promise<void> {
    const summaryContainer = doc.getElementById(
      "search-ai-summary-container",
    ) as HTMLElement;
    if (!summaryContainer) return;

    if (currentSearchResults.length === 0) {
      summaryContainer.style.display = "none";
      return;
    }

    // Show loading
    summaryContainer.style.display = "block";
    summaryContainer.innerHTML = `<div style="text-align: center; color: var(--text-secondary); padding: 20px;">⏳ Analyzing ${currentSearchResults.length} papers from results...</div>`;
    summaryContainer.scrollIntoView({ behavior: "smooth", block: "start" });

    try {
      const activeModel = getActiveModelConfig();
      if (!activeModel) {
        summaryContainer.innerHTML = `<div style="color: var(--error-color, #d32f2f); padding: 10px;">⚠️ No AI model configured. Please add a model in Settings.</div>`;
        return;
      }

      // Prepare context: limited to top N papers if there are too many to avoid token limits
      const topPapers = currentSearchResults.slice(0, 15);
      const context = topPapers
        .map((p, i) => {
          return `[${i + 1}] Title: ${p.title}\nAbstract: ${p.abstract || "No abstract available"}`;
        })
        .join("\n\n");

      const systemPrompt =
        currentSearchState.searchAiInsightsPrompt ||
        (Zotero.Prefs.get(
          "extensions.seerai.searchAiInsightsPrompt",
        ) as string) ||
        `You are an expert research analyst specializing in academic literature synthesis. Your role is to provide rigorous, insightful analysis of research papers.

For the given search results:
1. **Research Landscape**: Identify the key research themes, methodological approaches, and theoretical frameworks
2. **Critical Analysis**: Highlight significant findings, notable gaps, and areas of consensus or controversy
3. **Connections**: Draw connections between papers using citation format [N] to reference specific works
4. **Implications**: Discuss practical implications and future research directions

Format in clean Markdown with clear headings. Be analytical and substantive, not just descriptive. When referencing papers, use [N] format so readers can click to navigate.`;

      const responseLength =
        currentSearchState.searchAiInsightsResponseLength ||
        (Zotero.Prefs.get(
          "extensions.seerai.searchAiInsightsResponseLength",
        ) as number) ||
        500;

      const messages = [
        { role: "system" as const, content: systemPrompt },
        {
          role: "user" as const,
          content: `Analyze these ${topPapers.length} papers based on my search query "${currentSearchState.query}". Provide in-depth insights:\n\n${context}`,
        },
      ];

      let fullSummary = "";
      await openAIService.chatCompletionStream(
        messages,
        {
          onToken: (token) => {
            fullSummary += token;
            summaryContainer.innerHTML = `
                        <div style="font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-primary); padding-bottom: 8px;">
                            <div style="display: flex; align-items: center; gap: 6px;">
                                <span style="font-size: 16px;">💡</span>
                                <span>AI Insights Summary</span>
                            </div>
                            <span style="font-size: 11px; font-weight: 400; opacity: 0.6; font-style: italic;">AI is thinking...</span>
                        </div>
                        <div class="markdown-content" style="font-size: 13px; line-height: 1.6; color: var(--text-primary);">${parseMarkdown(fullSummary)}</div>
                    `;
            // Apply citation style live during streaming if indices are already parsed
            this.applyCitationStyle(summaryContainer);
          },
          onComplete: (content) => {
            fullSummary = content;
            // Cache the insights for persistence
            currentSearchState.cachedAiInsights = fullSummary;
            // Also update the persisted search history entry
            updateSearchHistoryWithInsights(
              currentSearchState.query,
              fullSummary,
            );
            summaryContainer.innerHTML = `
                        <div style="font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-primary); padding-bottom: 8px;">
                            <div style="display: flex; align-items: center; gap: 6px;">
                                <span style="font-size: 16px;">💡</span>
                                <span>AI Insights Summary</span>
                            </div>
                            <div style="display: flex; gap: 6px;">
                                <button id="settings-summary-btn" style="padding: 4px 8px; font-size: 14px; cursor: pointer; border-radius: 4px; border: 1px solid var(--border-primary); background: var(--background-primary); color: var(--text-primary); transition: all 0.2s;" title="AI Settings">⚙️</button>
                                <button id="copy-summary-btn" style="padding: 4px 10px; font-size: 11px; cursor: pointer; border-radius: 4px; border: 1px solid var(--border-primary); background: var(--background-primary); color: var(--text-primary); transition: all 0.2s;">📋 Copy</button>
                                <button id="close-summary-btn" style="padding: 4px 10px; font-size: 11px; cursor: pointer; border-radius: 4px; border: 1px solid var(--border-primary); background: var(--background-primary); color: var(--text-primary); transition: all 0.2s;">✕ Close</button>
                            </div>
                        </div>
                        <div class="markdown-content" style="font-size: 13px; line-height: 1.6; color: var(--text-primary);">${parseMarkdown(fullSummary)}</div>
                    `;

            // Hover effects for buttons
            const btns = [
              doc.getElementById("copy-summary-btn") as HTMLElement,
              doc.getElementById("close-summary-btn") as HTMLElement,
            ];
            btns.forEach((btn) => {
              if (btn) {
                btn.addEventListener("mouseenter", () => {
                  btn.style.backgroundColor = "var(--background-secondary)";
                });
                btn.addEventListener("mouseleave", () => {
                  btn.style.backgroundColor = "var(--background-primary)";
                });
              }
            });

            // Click listeners
            doc
              .getElementById("copy-summary-btn")
              ?.addEventListener("click", () => {
                new ztoolkit.Clipboard()
                  .addText(fullSummary, "text/unicode")
                  .copy();
                const btn = doc.getElementById(
                  "copy-summary-btn",
                ) as HTMLButtonElement;
                btn.innerText = "✓ Copied!";
                setTimeout(() => (btn.innerText = "📋 Copy"), 2000);
              });

            doc
              .getElementById("close-summary-btn")
              ?.addEventListener("click", () => {
                summaryContainer.style.display = "none";
              });

            doc
              .getElementById("settings-summary-btn")
              ?.addEventListener("click", (e: Event) => {
                const btn = e.target as HTMLElement;
                this.showAiInsightSettingsPopover(doc, btn, summaryContainer);
              });

            // Attach click handlers to citation links for navigation
            this.attachCitationClickHandlers(doc, summaryContainer);

            // Append Follow-up Question UI
            this.createFollowUpUI(doc, summaryContainer);
          },
          onError: (error) => {
            Zotero.debug(`[seerai] Search insights error: ${error}`);
            summaryContainer.innerHTML = `<div style="color: var(--error-color, #d32f2f); padding: 10px;">⚠️ AI Error: ${error.message}</div>`;
          },
        },
        {
          apiURL: activeModel.apiURL,
          apiKey: activeModel.apiKey,
          model: activeModel.model,
        },
      );
    } catch (e) {
      Zotero.debug(`[seerai] Failed to generate insights: ${e}`);
      summaryContainer.innerHTML = `<div style="color: var(--error-color, #d32f2f); padding: 10px;">⚠️ Failed to generate insights: ${e}</div>`;
    }
  }

  /**
   * Display cached AI insights without regenerating
   */
  private static displayCachedInsights(
    doc: Document,
    cachedContent: string,
  ): void {
    const summaryContainer = doc.getElementById(
      "search-ai-summary-container",
    ) as HTMLElement;
    if (!summaryContainer) return;

    summaryContainer.style.display = "block";
    summaryContainer.innerHTML = `
      <div style="font-weight: 600; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-primary); padding-bottom: 8px;">
        <div style="display: flex; align-items: center; gap: 6px;">
          <span style="font-size: 16px;">💡</span>
          <span>AI Insights Summary</span>
        </div>
        <div style="display: flex; gap: 6px;">
          <button id="settings-summary-btn" style="padding: 4px 8px; font-size: 14px; cursor: pointer; border-radius: 4px; border: 1px solid var(--border-primary); background: var(--background-primary); color: var(--text-primary); transition: all 0.2s;" title="AI Settings">⚙️</button>
          <button id="copy-summary-btn" style="padding: 4px 10px; font-size: 11px; cursor: pointer; border-radius: 4px; border: 1px solid var(--border-primary); background: var(--background-primary); color: var(--text-primary); transition: all 0.2s;">📋 Copy</button>
          <button id="close-summary-btn" style="padding: 4px 10px; font-size: 11px; cursor: pointer; border-radius: 4px; border: 1px solid var(--border-primary); background: var(--background-primary); color: var(--text-primary); transition: all 0.2s;">✕ Close</button>
        </div>
      </div>
      <div class="markdown-content" style="font-size: 13px; line-height: 1.6; color: var(--text-primary);">${parseMarkdown(cachedContent)}</div>
    `;

    // Hover effects for buttons
    const btns = [
      doc.getElementById("copy-summary-btn") as HTMLElement,
      doc.getElementById("close-summary-btn") as HTMLElement,
    ];
    btns.forEach((btn) => {
      if (btn) {
        btn.addEventListener("mouseenter", () => {
          btn.style.backgroundColor = "var(--background-secondary)";
        });
        btn.addEventListener("mouseleave", () => {
          btn.style.backgroundColor = "var(--background-primary)";
        });
      }
    });

    // Click listeners
    doc.getElementById("copy-summary-btn")?.addEventListener("click", () => {
      new ztoolkit.Clipboard().addText(cachedContent, "text/unicode").copy();
      const btn = doc.getElementById("copy-summary-btn") as HTMLButtonElement;
      btn.innerText = "✓ Copied!";
      setTimeout(() => (btn.innerText = "📋 Copy"), 2000);
    });

    doc.getElementById("close-summary-btn")?.addEventListener("click", () => {
      summaryContainer.style.display = "none";
    });

    doc
      .getElementById("settings-summary-btn")
      ?.addEventListener("click", (e: Event) => {
        const btn = e.target as HTMLElement;
        this.showAiInsightSettingsPopover(doc, btn, summaryContainer);
      });

    // Attach click handlers to citation links for navigation
    this.attachCitationClickHandlers(doc, summaryContainer);

    // Append Follow-up Question UI
    this.createFollowUpUI(doc, summaryContainer);
  }

  /**
   * Create and append the Follow-up Question UI
   */
  private static createFollowUpUI(doc: Document, container: HTMLElement): void {
    const wrapper = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        marginTop: "12px",
        paddingTop: "12px",
        borderTop: "1px solid var(--border-primary)",
      },
    });

    const inputContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "8px",
        marginTop: "8px",
      },
    });

    const input = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "text",
        placeholder: "Ask a follow-up about these papers...",
      },
      styles: {
        flex: "1",
        padding: "8px 10px",
        borderRadius: "4px",
        border: "1px solid var(--border-primary)",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
      },
    }) as HTMLInputElement;

    const sendBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "➤" },
      styles: {
        padding: "8px 12px",
        borderRadius: "4px",
        border: "none",
        backgroundColor: "var(--highlight-primary)",
        color: "var(--highlight-text)",
        cursor: "pointer",
        fontWeight: "bold",
      },
    });

    // Helper to submit
    const submit = async () => {
      const question = input.value.trim();
      if (!question) return;
      input.value = "";
      input.disabled = true;
      sendBtn.disabled = true;

      await this.handleFollowUpQuestion(doc, wrapper, question);

      input.disabled = false;
      sendBtn.disabled = false;
      input.focus();
    };

    sendBtn.addEventListener("click", submit);
    input.addEventListener("keypress", (e: Event) => {
      const ke = e as KeyboardEvent;
      if (ke.key === "Enter") submit();
    });

    inputContainer.appendChild(input);
    inputContainer.appendChild(sendBtn);
    wrapper.appendChild(inputContainer);
    container.appendChild(wrapper);
  }

  /**
   * Handle follow-up question submission
   */
  private static async handleFollowUpQuestion(
    doc: Document,
    container: HTMLElement,
    question: string,
  ): Promise<void> {
    // create response area
    const responseArea = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        marginTop: "12px",
        backgroundColor: "var(--background-secondary)",
        borderRadius: "6px",
        padding: "10px",
        fontSize: "13px",
        lineHeight: "1.5",
        color: "var(--text-primary)",
      },
    });

    // Insert before the input container (last child)
    container.insertBefore(responseArea, container.lastChild);

    responseArea.innerHTML = `
      <div style="font-weight: 600; margin-bottom: 4px; font-size: 11px; color: var(--text-secondary);">You asked: "${question}"</div>
      <div class="ai-answer">Thinking...</div>
    `;

    try {
      const activeModel = getActiveModelConfig();
      if (!activeModel) {
        responseArea.innerHTML += `<div style="color: var(--error-color);">⚠️ No AI model configured.</div>`;
        return;
      }

      // Gather context from ALL current results (including Show More loaded ones)
      // Limit to first 30 to prevent context overflow, or maybe token count
      // For now, take up to 25 papers
      const contextPapers = currentSearchResults.slice(0, 25);
      const context = contextPapers
        .map(
          (p, i) =>
            `[${i + 1}] Title: ${p.title}\nAbstract: ${p.abstract || "N/A"}`,
        )
        .join("\n\n");

      const systemPrompt = `You are a helpful research assistant. Answer the user's question based ONLY on the provided research papers.
      Use [N] format to cite specific papers. Be concise and direct.`;

      const messages = [
        { role: "system" as const, content: systemPrompt },
        {
          role: "user" as const,
          content: `Context of ${contextPapers.length} papers:\n${context}\n\nQuestion: ${question}`,
        },
      ];

      let fullAnswer = "";
      const answerDiv = responseArea.querySelector(".ai-answer") as HTMLElement;

      await openAIService.chatCompletionStream(
        messages,
        {
          onToken: (token) => {
            fullAnswer += token;
            answerDiv.innerHTML = parseMarkdown(fullAnswer);
            // Apply citation style live during streaming
            Assistant.applyCitationStyle(answerDiv);
          },
          onComplete: (content) => {
            fullAnswer = content;
            answerDiv.innerHTML = parseMarkdown(fullAnswer);
            // Re-attach citation handlers for the new content
            this.attachCitationClickHandlers(doc, answerDiv);
          },
          onError: (error) => {
            answerDiv.innerHTML += `<div style="color: var(--error-color);">Error: ${error.message}</div>`;
          },
        },
        {
          apiURL: activeModel.apiURL,
          apiKey: activeModel.apiKey,
          model: activeModel.model,
        },
      );
    } catch (e) {
      Zotero.debug(`[seerai] Follow-up error: ${e}`);
      responseArea.innerHTML += `<div style="color: var(--error-color);">Failed to generate answer.</div>`;
    }
  }

  /**
   * Scroll to and highlight a search result row by its index
   * Used when clicking citation references in AI insights
   */
  private static scrollToSearchResult(doc: Document, index: number): void {
    const rows = doc.querySelectorAll("tbody tr");
    const targetRow = rows[index] as HTMLElement;
    if (!targetRow) {
      Zotero.debug(`[seerai] Citation link: Row ${index} not found`);
      return;
    }

    // Scroll into view
    targetRow.scrollIntoView({ behavior: "smooth", block: "center" });

    // Highlight effect
    const originalBg = targetRow.style.backgroundColor;
    targetRow.style.backgroundColor = "var(--highlight-primary, #fff9c4)";
    targetRow.style.transition = "background-color 0.5s ease-out";

    setTimeout(() => {
      targetRow.style.backgroundColor = originalBg;
    }, 2000);

    Zotero.debug(`[seerai] Citation link: Scrolled to paper at index ${index}`);
  }

  /**
   * Get formatted citation string based on style
   */
  private static getFormattedCitation(
    paper: SemanticScholarPaper,
    index: number,
    style: string,
  ): string {
    if (!paper) return `[${index}]`;
    const authors = paper.authors || [];
    const firstAuthor = authors[0]?.name || "Unknown";
    const year = paper.year || "n.d.";
    const title = paper.title || "Untitled";

    switch (style) {
      case "apa":
        // (Author, Year) or (Author et al., Year)
        if (authors.length > 2) return `(${firstAuthor} et al., ${year})`;
        if (authors.length === 2)
          return `(${firstAuthor} & ${authors[1].name}, ${year})`;
        return `(${firstAuthor}, ${year})`;
      case "mla":
        // (Author Page) - Page is usually unknown here, so just Author
        // Use (Author) or (Author et al.)
        if (authors.length > 2) return `(${firstAuthor} et al.)`;
        return `(${firstAuthor})`;
      case "chicago":
      case "harvard":
        // (Author Year)
        if (authors.length > 2) return `(${firstAuthor} et al. ${year})`;
        return `(${firstAuthor} ${year})`;
      case "ieee":
      case "numbered":
      default:
        return `[${index}]`;
    }
  }

  /**
   * Apply the user's selected citation style to the insight container
   */
  private static applyCitationStyle(container: HTMLElement): void {
    const style =
      (Zotero.Prefs.get(
        "extensions.seerai.searchAiInsightsCitationStyle",
      ) as string) || "numbered";

    const links = container.querySelectorAll(".citation-link");
    links.forEach((link: Element) => {
      const htmlLink = link as HTMLElement;
      const indicesStr = htmlLink.dataset.citationIndices;
      if (!indicesStr) return;

      const indices = indicesStr.split(",").map((s) => parseInt(s.trim(), 10));
      if (indices.length === 0) return;

      // For multiple citations, strictly speaking styles differ:
      // APA: (Author, Year; Author2, Year2)
      // IEEE: [1], [2] or [1, 2]

      // Simplified approach: Map each index to its formatted string and join
      // If style is numbered/ieee, we want to keep [1, 2] look if possible, or [1][2]
      if (style === "numbered" || style === "ieee") {
        // The markdown parser already produced [1,2] or [1].
        // We can trust the innerText OR rebuild it to be safe.
        // Let's rebuild to be consistent with index logic
        htmlLink.innerText = `[${indices.join(", ")}]`;
      } else {
        // Text based citation
        const citationTexts = indices.map((idx) => {
          const paper = currentSearchResults[idx - 1];
          return this.getFormattedCitation(paper, idx, style);
        });
        // Join with semicolon for text styles
        htmlLink.innerText = citationTexts.join(", ");
      }
    });
  }

  /**
   * Find all AI insight containers and refresh their citation styles
   */
  public static refreshAllCitations(doc: Document): void {
    // Search tab summary
    const summaryContainer = doc.getElementById("search-ai-summary-container");
    if (summaryContainer)
      this.applyCitationStyle(summaryContainer as HTMLElement);

    // Follow-up answer areas
    const followUpAnswers = doc.querySelectorAll(".ai-answer");
    followUpAnswers.forEach((ans: Element) =>
      this.applyCitationStyle(ans as HTMLElement),
    );
  }

  /**
   * Convert HTML content to markdown format for copy-paste
   */
  private static htmlToMarkdown(element: HTMLElement): string {
    const processNode = (node: Node): string => {
      // Use numeric constants since Node global might not be available
      // Node.TEXT_NODE = 3, Node.ELEMENT_NODE = 1
      if (node.nodeType === 3) {
        return node.textContent || "";
      }

      if (node.nodeType !== 1) {
        return "";
      }

      const el = node as HTMLElement;
      const tagName = el.tagName.toLowerCase();
      const children = Array.from(el.childNodes)
        .filter((n): n is Node => n !== null)
        .map(processNode)
        .join("");

      switch (tagName) {
        case "strong":
        case "b":
          return `**${children}**`;
        case "em":
        case "i":
          return `*${children}*`;
        case "code":
          return `\`${children}\``;
        case "pre":
          return `\`\`\`\n${children}\n\`\`\``;
        case "h1":
          return `# ${children}\n`;
        case "h2":
          return `## ${children}\n`;
        case "h3":
          return `### ${children}\n`;
        case "h4":
          return `#### ${children}\n`;
        case "h5":
          return `##### ${children}\n`;
        case "h6":
          return `###### ${children}\n`;
        case "p":
          return `${children}\n\n`;
        case "br":
          return "\n";
        case "hr":
          return "\n---\n";
        case "a": {
          const href = el.getAttribute("href") || "";
          return `[${children}](${href})`;
        }
        case "ul":
          return `${children}\n`;
        case "ol":
          return `${children}\n`;
        case "li": {
          const parent = el.parentElement;
          if (parent?.tagName.toLowerCase() === "ol") {
            const index = Array.from(parent.children).indexOf(el) + 1;
            return `${index}. ${children}\n`;
          }
          return `- ${children}\n`;
        }
        case "blockquote":
          return (
            children
              .split("\n")
              .map((line) => `> ${line}`)
              .join("\n") + "\n"
          );
        case "table":
          return this.tableToMarkdown(el);
        case "div":
        case "span":
          return children;
        default:
          return children;
      }
    };

    return processNode(element).trim();
  }

  /**
   * Convert HTML table to markdown table format
   */
  private static tableToMarkdown(table: HTMLElement): string {
    const rows = table.querySelectorAll("tr");
    if (rows.length === 0) return "";

    const result: string[] = [];
    let headerProcessed = false;

    rows.forEach((row: Element, rowIndex: number) => {
      const cells = row.querySelectorAll("th, td");
      const cellContents: string[] = [];
      cells.forEach((cell: Element) => {
        cellContents.push(
          (cell.textContent || "").trim().replace(/\|/g, "\\|"),
        );
      });

      result.push(`| ${cellContents.join(" | ")} |`);

      // Add separator after header row
      if (!headerProcessed && (row.querySelector("th") || rowIndex === 0)) {
        const separator = cellContents.map(() => "---").join(" | ");
        result.push(`| ${separator} |`);
        headerProcessed = true;
      }
    });

    return result.join("\n") + "\n";
  }

  /**
   * Handle smart copy - append references if citations are copied
   */
  private static handleSmartCopy(
    e: ClipboardEvent,
    container: HTMLElement,
  ): void {
    const doc = container.ownerDocument;
    if (!doc) return;
    const selection = doc.getSelection();
    if (!selection || selection.rangeCount === 0) return;

    const selectedText = selection.toString();
    if (!selectedText.trim()) return;

    // Find all citations within the selection
    // This is tricky because selection might be partial text.
    // We can look for data-citation-indices in the selected NODES.

    const range = selection.getRangeAt(0);
    const clone = range.cloneContents();
    const div = doc.createElement("div");
    div.appendChild(clone);

    const links = div.querySelectorAll(".citation-link");
    const indicesSet = new Set<number>();

    links.forEach((link: Element) => {
      const idxStr = (link as HTMLElement).dataset.citationIndices;
      if (idxStr) {
        idxStr
          .split(",")
          .forEach((s) => indicesSet.add(parseInt(s.trim(), 10)));
      }
    });

    if (indicesSet.size === 0) return;

    // Format references
    const indices = Array.from(indicesSet).sort((a, b) => a - b);
    const references = indices
      .map((idx) => {
        const paper = currentSearchResults[idx - 1];
        if (!paper) return null;
        // Simple reference format: [N] Title. Author. Year.
        const authors =
          paper.authors?.map((a) => a.name).join(", ") || "Unknown Author";
        return `[${idx}] ${paper.title}. ${authors}. ${paper.year || ""}.`;
      })
      .filter(Boolean);

    if (references.length > 0) {
      e.preventDefault();
      const refText = `\n\nReferences:\n${references.join("\n")}`;
      const finalPayload = selectedText + refText;
      if (e.clipboardData) {
        e.clipboardData.setData("text/plain", finalPayload);
        // Also try to set generic HTML if we want to preserve rich text?
        // For now text/plain is safer for reference appending.
      }
    }
  }

  /**
   * Attach click and hover handlers to citation links in AI insights container
   */
  private static attachCitationClickHandlers(
    doc: Document,
    container: HTMLElement,
  ): void {
    // Apply styling first
    this.applyCitationStyle(container);

    // Smart copy listener (only attach once)
    if (!container.dataset.hasCopyListener) {
      container.addEventListener("copy", (e) =>
        this.handleSmartCopy(e as ClipboardEvent, container),
      );
      container.dataset.hasCopyListener = "true";
    }

    const citationLinks = container.querySelectorAll(".citation-link");
    citationLinks.forEach((link: Element) => {
      const htmlLink = link as HTMLElement;
      const indicesStr = htmlLink.dataset.citationIndices;
      if (!indicesStr) return;

      const indices = indicesStr.split(",").map((s) => parseInt(s.trim(), 10));

      // 1. Hover effect: Show titles
      const titles = indices
        .map((idx) => {
          const paper = currentSearchResults[idx - 1];
          return paper ? `[${idx}] ${paper.title}` : `[${idx}] Paper not found`;
        })
        .join("\n");
      htmlLink.title = titles;

      // 2. Click handler
      link.addEventListener("click", (e: Event) => {
        e.stopPropagation();
        Zotero.debug(
          `[seerai] Citation clicked: ${indicesStr} (found ${indices.length} indices)`,
        );
        if (indices.length === 1) {
          Assistant.scrollToSearchResult(doc, indices[0] - 1);
        } else {
          Assistant.showCitationMenu(doc, htmlLink, indices);
        }
      });
    });
  }

  /**
   * Show a "drop up" menu for multi-citations
   * Uses a backdrop pattern for better compatibility with Zotero/XUL
   */
  private static showCitationMenu(
    doc: Document,
    anchor: HTMLElement,
    indices: number[],
  ): void {
    // Remove any existing menus
    const existingMenu = doc.getElementById("citation-menu-popover");
    if (existingMenu) existingMenu.remove();
    const existingBackdrop = doc.getElementById("citation-menu-backdrop");
    if (existingBackdrop) existingBackdrop.remove();

    // 1. Create Backdrop (for click-outside behavior)
    const backdrop = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "citation-menu-backdrop" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        right: "0",
        bottom: "0",
        zIndex: "9998", // Below menu
        backgroundColor: "transparent",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            backdrop.remove();
            const m = doc.getElementById("citation-menu-popover");
            if (m) m.remove();
          },
        },
      ],
    });

    // 2. Create Menu
    const menu = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "citation-menu-popover" },
      styles: {
        position: "fixed",
        backgroundColor: "var(--background-primary, #fff)",
        border: "1px solid var(--border-primary, #ccc)",
        borderRadius: "6px",
        boxShadow: "0 4px 12px rgba(0,0,0,0.2)",
        padding: "4px",
        zIndex: "10000",
        minWidth: "150px",
        maxWidth: "300px",
      },
      listeners: [
        { type: "click", listener: (e: Event) => e.stopPropagation() },
      ],
    });

    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        fontSize: "10px",
        fontWeight: "600",
        color: "var(--text-secondary, #666)",
        padding: "4px 8px",
        borderBottom: "1px solid var(--border-primary, #ccc)",
        marginBottom: "4px",
      },
    });
    header.innerText = "Select paper to view:";
    menu.appendChild(header);

    indices.forEach((idx) => {
      const paper = currentSearchResults[idx - 1];
      if (!paper) return;

      const item = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          padding: "6px 8px",
          fontSize: "12px",
          cursor: "pointer",
          borderRadius: "4px",
          whiteSpace: "nowrap",
          overflow: "hidden",
          textOverflow: "ellipsis",
          color: "var(--text-primary, #000)",
        },
      });
      item.innerText = `[${idx}] ${paper.title}`;
      item.title = paper.title;

      item.addEventListener("mouseenter", () => {
        item.style.backgroundColor = "var(--background-secondary, #f5f5f5)";
      });
      item.addEventListener("mouseleave", () => {
        item.style.backgroundColor = "transparent";
      });
      item.addEventListener("click", (e) => {
        e.stopPropagation();
        Assistant.scrollToSearchResult(doc, idx - 1);
        menu.remove();
        backdrop.remove();
      });

      menu.appendChild(item);
    });

    // 3. Append to DOM (Fallback to documentElement for XUL)
    if (doc.body) {
      doc.body.appendChild(backdrop);
      if (!doc.body) return;
      doc.body.appendChild(menu);
    } else {
      doc.documentElement?.appendChild(backdrop);
      doc.documentElement?.appendChild(menu);
    }

    // 4. Position Logic
    const rect = anchor.getBoundingClientRect();
    const menuRect = menu.getBoundingClientRect();
    const view = doc.defaultView;
    if (!view) return;

    // Default: Position above
    let top = rect.top - menuRect.height - 8;

    // If not enough space above, flip to below
    if (top < 0) {
      top = rect.bottom + 8;
    }

    let left = rect.left;
    // Prevent going off-screen right
    if (left + menuRect.width > view.innerWidth) {
      left = view.innerWidth - menuRect.width - 8;
    }
    // Prevent going off-screen left
    if (left < 0) left = 8;

    menu.style.top = `${top}px`;
    menu.style.left = `${left}px`;
  }

  /**
   * Show settings popover for Search AI Insights
   */
  private static showAiInsightSettingsPopover(
    doc: Document,
    anchor: HTMLElement,
    container: HTMLElement,
  ): void {
    Zotero.debug("[seerai] showAiInsightSettingsPopover called");
    const existing = doc.getElementById("search-settings-popover");
    if (existing) existing.remove();
    const existingBackdrop = doc.getElementById("search-settings-backdrop");
    if (existingBackdrop) existingBackdrop.remove();

    // Backdrop
    const backdrop = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "search-settings-backdrop" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        right: "0",
        bottom: "0",
        zIndex: "9998",
        backgroundColor: "transparent",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            backdrop.remove();
            const p = doc.getElementById("search-settings-popover");
            if (p) p.remove();
          },
        },
      ],
    });

    // Popover
    const popover = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "search-settings-popover" },
      styles: {
        position: "fixed",
        backgroundColor: "var(--background-primary, #fff)",
        width: "280px",
        borderRadius: "8px",
        boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
        border: "1px solid var(--border-primary, #ccc)",
        padding: "16px",
        display: "flex",
        flexDirection: "column",
        gap: "12px",
        zIndex: "9999",
      },
      listeners: [
        { type: "click", listener: (e: Event) => e.stopPropagation() },
      ],
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "AI Insight Configuration" },
      styles: {
        fontSize: "14px",
        fontWeight: "600",
        borderBottom: "1px solid var(--border-primary)",
        paddingBottom: "8px",
        color: "var(--text-primary)",
      },
    });
    popover.appendChild(header);

    // 1. Response Length
    const lengthSection = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexDirection: "column", gap: "8px" },
    });

    // Get current value
    const rawLen = Zotero.Prefs.get(
      "extensions.seerai.searchAiInsightsResponseLength",
    ) as number;
    // Treat 0 as Limitless (mapped to 2000 for slider), otherwise fallback to 500
    const currentLen = rawLen === 0 ? 2000 : (rawLen ?? 500);
    const lenText = currentLen >= 2000 ? "Limitless" : `${currentLen} words`;

    const lenLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: `Max Response Length: ${lenText}` },
      styles: {
        fontSize: "12px",
        fontWeight: "600",
        color: "var(--text-primary)",
      },
    });
    lengthSection.appendChild(lenLabel);

    const sliderContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", alignItems: "center", gap: "10px" },
    });
    const slider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "100",
        max: "2000",
        step: "100",
        value: String(currentLen),
      },
      styles: { flex: "1" },
    }) as HTMLInputElement;

    sliderContainer.appendChild(slider);
    lengthSection.appendChild(sliderContainer);

    slider.addEventListener("input", () => {
      const val = parseInt(slider.value);
      if (val >= 2000) {
        lenLabel.innerText = `Max Response Length: Limitless`;
      } else {
        lenLabel.innerText = `Max Response Length: ${val} words`;
      }
    });
    slider.addEventListener("change", () => {
      let val = parseInt(slider.value);
      // If maxed out, treat as Limitless (0)
      if (val >= 2000) val = 0;

      Zotero.Prefs.set("extensions.seerai.searchAiInsightsResponseLength", val);
      currentSearchState.searchAiInsightsResponseLength = val; // Update runtime state
    });

    popover.appendChild(lengthSection);

    // 2. Citation Style (Advanced Configuration)
    const advancedSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        marginTop: "8px",
        paddingTop: "8px",
        borderTop: "1px solid var(--border-primary)",
      },
    });

    const styleLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "Citation Style" },
      styles: {
        fontSize: "12px",
        fontWeight: "600",
        color: "var(--text-primary)",
      },
    });
    advancedSection.appendChild(styleLabel);

    const styleSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        width: "100%",
        padding: "6px",
        borderRadius: "4px",
        border: "1px solid var(--border-primary)",
        backgroundColor: "var(--background-secondary)",
        fontSize: "12px",
        color: "var(--text-primary)",
      },
    }) as HTMLSelectElement;

    // Options
    const styles = [
      { value: "numbered", label: "Numbered ([1])" },
      { value: "apa", label: "APA (Author, Year)" },
      { value: "mla", label: "MLA (Author Page)" },
      { value: "chicago", label: "Chicago (Author Year)" },
      { value: "harvard", label: "Harvard (Author Year)" },
      { value: "ieee", label: "IEEE ([1])" },
    ];

    const currentStyle =
      (Zotero.Prefs.get(
        "extensions.seerai.searchAiInsightsCitationStyle",
      ) as string) || "numbered";

    styles.forEach((s) => {
      const opt = doc.createElement("option");
      opt.value = s.value;
      opt.innerText = s.label;
      if (s.value === currentStyle) opt.selected = true;
      styleSelect.appendChild(opt);
    });

    styleSelect.addEventListener("change", () => {
      Zotero.Prefs.set(
        "extensions.seerai.searchAiInsightsCitationStyle",
        styleSelect.value,
      );
      Assistant.refreshAllCitations(doc);
    });

    advancedSection.appendChild(styleSelect);

    // Append sections
    popover.appendChild(advancedSection);

    // Position
    const rect = anchor.getBoundingClientRect();
    const view = doc.defaultView;
    if (!view) return;

    let top = rect.bottom + 8;
    let left = rect.right - 280; // Align right edge

    // Prevent off-screen
    if (left < 10) left = 10;
    if (top + 400 > view.innerHeight) top = rect.top - 400; // Flip up if needed

    popover.style.top = `${top}px`;
    popover.style.left = `${left}px`;

    // Append to DOM
    if (doc.body) {
      doc.body.appendChild(backdrop);
      doc.body.appendChild(popover);
    } else {
      doc.documentElement?.appendChild(backdrop);
      doc.documentElement?.appendChild(popover);
    }
  }

  /**
   * Batch check Unpaywall for papers without PDFs
   * Updates badges in the DOM as results come in
   */
  private static async batchCheckUnpaywall(
    doc: Document,
    papers: SemanticScholarPaper[],
  ): Promise<void> {
    // Filter papers that need checking (no openAccessPdf but have DOI)
    const papersToCheck = papers.filter(
      (p) =>
        !p.openAccessPdf &&
        p.externalIds?.DOI &&
        !unpaywallPdfCache.has(p.paperId),
    );

    if (papersToCheck.length === 0) return;

    Zotero.debug(
      `[seerai] Batch checking Unpaywall for ${papersToCheck.length} papers`,
    );

    // Check in parallel (UnpaywallService handles batching internally)
    const dois = papersToCheck.map((p) => p.externalIds!.DOI!);
    const results = await unpaywallService.checkMultipleDois(dois);

    // Update cache and UI for each result
    papersToCheck.forEach((paper) => {
      const doi = paper.externalIds!.DOI!;
      const pdfUrl = results.get(doi.toLowerCase().trim());

      if (pdfUrl) {
        unpaywallPdfCache.set(paper.paperId, pdfUrl);
      }

      // Find and update the badge in the DOM
      // Cards are identified by paper title text content
      const cards = doc.querySelectorAll(".search-result-card");
      for (const card of cards) {
        const titleEl = card.querySelector('div[style*="font-weight: 600"]');
        if (titleEl && titleEl.textContent === paper.title) {
          const badge = card.querySelector('span[style*="Checking"]');
          if (badge && badge instanceof HTMLElement) {
            if (pdfUrl) {
              badge.innerText = "🔗 PDF (Unpaywall)";
              badge.style.backgroundColor = "#e3f2fd";
              badge.style.color = "#1976d2";
              badge.style.cursor = "pointer";
              badge.title = pdfUrl;
              badge.onclick = (e: Event) => {
                e.stopPropagation();
                Zotero.launchURL(pdfUrl);
              };
            } else {
              badge.innerText = "📭 No PDF";
              badge.style.backgroundColor = "#fafafa";
              badge.style.color = "#9e9e9e";
            }
          }
          break;
        }
      }
    });
  }

  /**
   * Append new search result cards to the unified table
   */
  private static appendSearchCards(
    doc: Document,
    container: HTMLElement,
    newPapers: SemanticScholarPaper[],
    item: Zotero.Item,
  ): void {
    // Find and remove the loading indicator if present
    const loadingIndicator = container.querySelector("#show-more-loading");
    if (loadingIndicator) {
      loadingIndicator.remove();
    }

    // Find the table body in the container
    const tbody = container.querySelector("tbody");
    if (tbody) {
      newPapers.forEach((paper) => {
        const tr = this.createUnifiedResultRow(
          doc,
          paper,
          item,
          searchColumnConfig.columns,
        );
        tbody.appendChild(tr);
      });
    }

    // Re-append the "Show More" button at the bottom of the scrollable area
    // We look for the scrollable table container
    const tableContainer = container.querySelector(
      "div[style*='overflow: auto']",
    );
    if (tableContainer) {
      // Create new Show More button
      const showMoreBtn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: "📥 Show More", id: "show-more-btn" },
        styles: {
          display: "block",
          width: "calc(100% - 24px)",
          margin: "12px",
          padding: "12px",
          backgroundColor: "var(--background-secondary)",
          color: "var(--text-primary)",
          border: "1px solid var(--border-primary)",
          borderRadius: "6px",
          fontSize: "13px",
          cursor: "pointer",
        },
        listeners: [
          {
            type: "click",
            listener: async () => {
              // Replace button with loading indicator
              const loadingDiv = ztoolkit.UI.createElement(doc, "div", {
                properties: { id: "show-more-loading" },
                styles: {
                  textAlign: "center",
                  padding: "16px",
                  color: "var(--text-secondary)",
                  fontSize: "12px",
                },
              });
              loadingDiv.innerHTML = "⏳ Loading more papers...";
              showMoreBtn.replaceWith(loadingDiv);

              await this.performSearch(doc);
            },
          },
        ],
      });
      tableContainer.appendChild(showMoreBtn);
    }
  }
  /**
   * Create a paper result card
   */
  private static createSearchResultCard(
    doc: Document,
    paper: SemanticScholarPaper,
    item: Zotero.Item,
    isTableCell: boolean = false,
  ): HTMLElement {
    const card = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "search-result-card" },
      styles: {
        padding: "12px",
        borderBottom: "1px solid var(--border-primary)",
        cursor: "pointer",
      },
    });

    // Header: Title + PDF indicator
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "flex-start",
        marginBottom: "6px",
      },
    });

    const title = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: paper.title },
      styles: {
        fontSize: "13px",
        fontWeight: "600",
        color: "var(--text-primary)",
        flex: "1",
        lineHeight: "1.3",
        wordBreak: "break-word", // FORCE WORD BREAK
        minWidth: "0", // ALLOW SHRINKING
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            if (paper.url) {
              Zotero.launchURL(paper.url);
            }
          },
        },
      ],
    });
    // Add hover effect for title
    title.addEventListener("mouseenter", () => {
      title.style.textDecoration = "underline";
      title.style.color = "var(--highlight-primary)";
    });
    title.addEventListener("mouseleave", () => {
      title.style.textDecoration = "none";
      title.style.color = "var(--text-primary)";
    });
    header.appendChild(title);

    // PDF badge removed - PDF access available via action buttons below

    card.appendChild(header);

    // Meta: Authors (clickable), Year, Venue
    const meta = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        fontSize: "11px",
        color: "var(--text-secondary)",
        marginBottom: "6px",
        display: "flex",
        flexWrap: "wrap",
        alignItems: "center",
        gap: "2px",
      },
    });

    // Clickable author links
    const displayedAuthors = paper.authors?.slice(0, 3) || [];
    displayedAuthors.forEach((author, idx) => {
      const authorLink = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: author.name },
        styles: {
          color: "var(--highlight-primary)",
          cursor: "pointer",
          textDecoration: "underline",
        },
        listeners: [
          {
            type: "click",
            listener: async (e: Event) => {
              e.stopPropagation();
              await this.showAuthorModal(doc, author.authorId, author.name);
            },
          },
        ],
      });
      meta.appendChild(authorLink);
      if (idx < displayedAuthors.length - 1) {
        const comma = ztoolkit.UI.createElement(doc, "span", {
          properties: { innerText: ", " },
        });
        meta.appendChild(comma);
      }
    });

    if (paper.authors?.length > 3) {
      const more = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: ` +${paper.authors.length - 3} more` },
      });
      meta.appendChild(more);
    }

    const yearVenue = paper.year ? ` • ${paper.year}` : "";
    const venueText = paper.venue
      ? ` • ${paper.venue.slice(0, 30)}${paper.venue.length > 30 ? "..." : ""}`
      : "";
    const extraMeta = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: yearVenue + venueText },
    });
    meta.appendChild(extraMeta);
    card.appendChild(meta);

    // Citation count
    const citationBadge = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        fontSize: "11px",
        color: "var(--text-secondary)",
        marginBottom: "6px",
      },
    });
    citationBadge.innerHTML = `📈 <strong>${paper.citationCount.toLocaleString()}</strong> citations`;
    card.appendChild(citationBadge);

    // Abstract preview (TLDR or truncated abstract)
    const abstractText = paper.tldr?.text || paper.abstract;
    if (abstractText) {
      const abstractEl = ztoolkit.UI.createElement(doc, "div", {
        properties: {
          innerText:
            abstractText.slice(0, 200) +
            (abstractText.length > 200 ? "..." : ""),
        },
        styles: {
          fontSize: "11px",
          color: "var(--text-secondary)",
          lineHeight: "1.4",
          marginBottom: "8px",
          wordBreak: "break-word",
          minWidth: "0",
        },
      });
      card.appendChild(abstractEl);
    }

    // Action buttons
    const actions = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "8px",
        marginTop: "8px",
        flexWrap: "wrap", // Allow buttons to wrap
      },
    });

    const actionBtnStyle = {
      padding: "6px 10px",
      fontSize: "11px",
      border: "1px solid var(--border-primary)",
      borderRadius: "4px",
      backgroundColor: "var(--background-secondary)",
      color: "var(--text-primary)",
      cursor: "pointer",
    };

    // Add to Zotero button with full PDF discovery integration
    const addToZoteroBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "➕ Add to Zotero" },
      styles: {
        ...actionBtnStyle,
        backgroundColor: "var(--highlight-primary)",
        color: "var(--highlight-text)",
        border: "none",
      },
      listeners: [
        {
          type: "click",
          listener: async (e: Event) => {
            e.stopPropagation();
            const btn = e.target as HTMLButtonElement;
            const originalText = btn.textContent;

            // Show importing status
            btn.textContent = "⬇️ Importing...";
            btn.disabled = true;
            btn.style.backgroundColor = "#e3f2fd";
            btn.style.color = "#1976d2";

            try {
              // Create the Zotero item with PDF discovery
              const result = await this.addPaperToZoteroWithPdfDiscovery(
                paper,
                btn,
              );

              if (result.item) {
                if (result.pdfAttached) {
                  // Show imported status with green styling when PDF attached
                  btn.textContent = "✅ Imported";
                  btn.style.backgroundColor = "#e8f5e9";
                  btn.style.color = "#2e7d32";
                } else if (result.sourceUrl) {
                  // No PDF but item created - show Source-Link option
                  btn.textContent = "🔗 Source-Link";
                  btn.style.backgroundColor = "#e8eaf6";
                  btn.style.color = "#3f51b5";
                  btn.disabled = false;
                  const createdItem = result.item; // Capture for use in nested handlers
                  btn.onclick = (clickEvent: Event) => {
                    clickEvent.stopPropagation();
                    if (result.sourceUrl) Zotero.launchURL(result.sourceUrl);
                    // After opening, replace button with Attach + Retry container
                    const container = doc.createElement("span");
                    container.style.display = "inline-flex";
                    container.style.gap = "6px";

                    // Attach button
                    const attachBtn = ztoolkit.UI.createElement(doc, "button", {
                      namespace: "html",
                      properties: { innerText: "⬇️ Attach" },
                      styles: {
                        padding: "4px 8px",
                        fontSize: "10px",
                        border: "1px solid var(--border-primary)",
                        borderRadius: "4px",
                        backgroundColor: "#e3f2fd",
                        color: "#1976d2",
                        cursor: "pointer",
                      },
                      listeners: [
                        {
                          type: "click",
                          listener: async (attachEvent: Event) => {
                            attachEvent.stopPropagation();
                            (
                              attachEvent.target as HTMLButtonElement
                            ).textContent = "📥 Attaching...";
                            (attachEvent.target as HTMLButtonElement).disabled =
                              true;
                            try {
                              // Use Zotero's file picker to attach a PDF
                              const fp = new (
                                Zotero.getMainWindow() as any
                              ).FilePicker();
                              fp.init(
                                Zotero.getMainWindow(),
                                "Select PDF to attach",
                                fp.modeOpen,
                              );
                              fp.appendFilter("PDF Files", "*.pdf");
                              const fpResult = await fp.show();
                              if (fpResult === fp.returnOK && fp.file) {
                                await Zotero.Attachments.importFromFile({
                                  file: fp.file,
                                  parentItemID: createdItem.id,
                                });
                                container.innerHTML = `<span style="color: #2e7d32; font-size: 11px;">✅ Imported</span>`;
                              } else {
                                (
                                  attachEvent.target as HTMLButtonElement
                                ).textContent = "⬇️ Attach";
                                (
                                  attachEvent.target as HTMLButtonElement
                                ).disabled = false;
                              }
                            } catch (err) {
                              Zotero.debug(`[seerai] Attach error: ${err}`);
                              (
                                attachEvent.target as HTMLButtonElement
                              ).textContent = "⬇️ Attach";
                              (
                                attachEvent.target as HTMLButtonElement
                              ).disabled = false;
                            }
                          },
                        },
                      ],
                    });

                    // Retry button
                    const retryBtn = ztoolkit.UI.createElement(doc, "button", {
                      namespace: "html",
                      properties: { innerText: "🔁 Retry" },
                      styles: {
                        padding: "4px 8px",
                        fontSize: "10px",
                        border: "1px solid var(--border-primary)",
                        borderRadius: "4px",
                        backgroundColor: "#fafafa",
                        color: "#757575",
                        cursor: "pointer",
                      },
                      listeners: [
                        {
                          type: "click",
                          listener: async (retryEvent: Event) => {
                            retryEvent.stopPropagation();
                            (
                              retryEvent.target as HTMLButtonElement
                            ).textContent = "⏳ Searching...";
                            (retryEvent.target as HTMLButtonElement).disabled =
                              true;
                            // Clear caches and retry PDF discovery
                            const zoteroCacheKey =
                              paper.externalIds?.DOI ||
                              paper.externalIds?.ArXiv ||
                              paper.externalIds?.PMID ||
                              paper.title?.slice(0, 50) ||
                              "";
                            if (zoteroCacheKey)
                              zoteroFindPdfCache.delete(zoteroCacheKey);
                            if (paper.externalIds?.DOI) {
                              unpaywallService.clearCacheForDoi(
                                paper.externalIds.DOI,
                              );
                            }
                            // Try to find and attach PDF again
                            try {
                              const pdfUrl = await this.tryFindPdfForItem(
                                paper,
                                createdItem,
                              );
                              if (pdfUrl) {
                                container.innerHTML = `<span style="color: #2e7d32; font-size: 11px;">✅ Imported</span>`;
                              } else {
                                (
                                  retryEvent.target as HTMLButtonElement
                                ).textContent = "🔁 Retry";
                                (
                                  retryEvent.target as HTMLButtonElement
                                ).disabled = false;
                              }
                            } catch (err) {
                              (
                                retryEvent.target as HTMLButtonElement
                              ).textContent = "🔁 Retry";
                              (
                                retryEvent.target as HTMLButtonElement
                              ).disabled = false;
                            }
                          },
                        },
                      ],
                    });

                    container.appendChild(attachBtn);
                    container.appendChild(retryBtn);
                    btn.replaceWith(container);
                  };
                } else {
                  // No PDF and no source URL - show retry
                  btn.textContent = "🔁 Retry";
                  btn.style.backgroundColor = "#fafafa";
                  btn.style.color = "#757575";
                  btn.disabled = false;
                }
              } else {
                btn.textContent = "⚠️ Failed";
                btn.style.backgroundColor = "#ffebee";
                btn.style.color = "#c62828";
                setTimeout(() => {
                  btn.textContent = originalText || "➕ Add to Zotero";
                  btn.style.backgroundColor = "var(--highlight-primary)";
                  btn.style.color = "var(--highlight-text)";
                  btn.disabled = false;
                }, 2000);
              }
            } catch (error) {
              Zotero.debug(`[seerai] Add to Zotero error: ${error}`);
              btn.textContent = "⚠️ Error";
              btn.style.backgroundColor = "#ffebee";
              btn.style.color = "#c62828";
              setTimeout(() => {
                btn.textContent = originalText || "➕ Add to Zotero";
                btn.style.backgroundColor = "var(--highlight-primary)";
                btn.style.color = "var(--highlight-text)";
                btn.disabled = false;
              }, 2000);
            }
          },
        },
      ],
    });
    actions.appendChild(addToZoteroBtn);

    // Add to Table button with full PDF discovery integration
    const addToTableBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📊 Add to Table" },
      styles: actionBtnStyle,
      listeners: [
        {
          type: "click",
          listener: async (e: Event) => {
            e.stopPropagation();
            const btn = e.target as HTMLButtonElement;
            const originalText = btn.textContent;

            // Show importing status immediately
            btn.textContent = "⬇️ Importing...";
            btn.disabled = true;
            btn.style.backgroundColor = "#e3f2fd";
            btn.style.color = "#1976d2";

            try {
              const result = await this.addPaperToZoteroWithPdfDiscovery(
                paper,
                btn,
              );
              if (result.item && currentTableConfig) {
                if (
                  !currentTableConfig.addedPaperIds.includes(result.item.id)
                ) {
                  currentTableConfig.addedPaperIds.push(result.item.id);
                  const tableStore = getTableStore();
                  await tableStore.saveConfig(currentTableConfig);
                }
              }

              if (result.item) {
                if (result.pdfAttached) {
                  // Show imported status with green styling when PDF attached
                  btn.textContent = "✅ Imported";
                  btn.style.backgroundColor = "#e8f5e9";
                  btn.style.color = "#2e7d32";
                } else if (result.sourceUrl) {
                  // No PDF but item created - show Source-Link option
                  btn.textContent = "🔗 Source-Link";
                  btn.style.backgroundColor = "#e8eaf6";
                  btn.style.color = "#3f51b5";
                  btn.disabled = false;
                  const createdItem = result.item; // Capture for use in nested handlers
                  btn.onclick = (clickEvent: Event) => {
                    clickEvent.stopPropagation();
                    if (result.sourceUrl) Zotero.launchURL(result.sourceUrl);
                    // After opening, replace button with Attach + Retry container
                    const container = doc.createElement("span");
                    container.style.display = "inline-flex";
                    container.style.gap = "6px";

                    // Attach button
                    const attachBtn = ztoolkit.UI.createElement(doc, "button", {
                      namespace: "html",
                      properties: { innerText: "⬇️ Attach" },
                      styles: {
                        padding: "4px 8px",
                        fontSize: "10px",
                        border: "1px solid var(--border-primary)",
                        borderRadius: "4px",
                        backgroundColor: "#e3f2fd",
                        color: "#1976d2",
                        cursor: "pointer",
                      },
                      listeners: [
                        {
                          type: "click",
                          listener: async (attachEvent: Event) => {
                            attachEvent.stopPropagation();
                            (
                              attachEvent.target as HTMLButtonElement
                            ).textContent = "📥 Attaching...";
                            (attachEvent.target as HTMLButtonElement).disabled =
                              true;
                            try {
                              const fp = new (
                                Zotero.getMainWindow() as any
                              ).FilePicker();
                              fp.init(
                                Zotero.getMainWindow(),
                                "Select PDF to attach",
                                fp.modeOpen,
                              );
                              fp.appendFilter("PDF Files", "*.pdf");
                              const fpResult = await fp.show();
                              if (fpResult === fp.returnOK && fp.file) {
                                await Zotero.Attachments.importFromFile({
                                  file: fp.file,
                                  parentItemID: createdItem.id,
                                });
                                container.innerHTML = `<span style="color: #2e7d32; font-size: 11px;">✅ Imported</span>`;
                              } else {
                                (
                                  attachEvent.target as HTMLButtonElement
                                ).textContent = "⬇️ Attach";
                                (
                                  attachEvent.target as HTMLButtonElement
                                ).disabled = false;
                              }
                            } catch (err) {
                              Zotero.debug(`[seerai] Attach error: ${err}`);
                              (
                                attachEvent.target as HTMLButtonElement
                              ).textContent = "⬇️ Attach";
                              (
                                attachEvent.target as HTMLButtonElement
                              ).disabled = false;
                            }
                          },
                        },
                      ],
                    });

                    // Retry button
                    const retryBtn = ztoolkit.UI.createElement(doc, "button", {
                      namespace: "html",
                      properties: { innerText: "🔁 Retry" },
                      styles: {
                        padding: "4px 8px",
                        fontSize: "10px",
                        border: "1px solid var(--border-primary)",
                        borderRadius: "4px",
                        backgroundColor: "#fafafa",
                        color: "#757575",
                        cursor: "pointer",
                      },
                      listeners: [
                        {
                          type: "click",
                          listener: async (retryEvent: Event) => {
                            retryEvent.stopPropagation();
                            (
                              retryEvent.target as HTMLButtonElement
                            ).textContent = "⏳ Searching...";
                            (retryEvent.target as HTMLButtonElement).disabled =
                              true;
                            const zoteroCacheKey =
                              paper.externalIds?.DOI ||
                              paper.externalIds?.ArXiv ||
                              paper.externalIds?.PMID ||
                              paper.title?.slice(0, 50) ||
                              "";
                            if (zoteroCacheKey)
                              zoteroFindPdfCache.delete(zoteroCacheKey);
                            if (paper.externalIds?.DOI) {
                              unpaywallService.clearCacheForDoi(
                                paper.externalIds.DOI,
                              );
                            }
                            try {
                              const pdfUrl = await this.tryFindPdfForItem(
                                paper,
                                createdItem,
                              );
                              if (pdfUrl) {
                                container.innerHTML = `<span style="color: #2e7d32; font-size: 11px;">✅ Imported</span>`;
                              } else {
                                (
                                  retryEvent.target as HTMLButtonElement
                                ).textContent = "🔁 Retry";
                                (
                                  retryEvent.target as HTMLButtonElement
                                ).disabled = false;
                              }
                            } catch (err) {
                              (
                                retryEvent.target as HTMLButtonElement
                              ).textContent = "🔁 Retry";
                              (
                                retryEvent.target as HTMLButtonElement
                              ).disabled = false;
                            }
                          },
                        },
                      ],
                    });

                    container.appendChild(attachBtn);
                    container.appendChild(retryBtn);
                    btn.replaceWith(container);
                  };
                } else {
                  // No PDF and no source URL - show retry
                  btn.textContent = "🔁 Retry";
                  btn.style.backgroundColor = "#fafafa";
                  btn.style.color = "#757575";
                  btn.disabled = false;
                }
              } else {
                btn.textContent = "⚠️ Failed";
                btn.style.backgroundColor = "#ffebee";
                btn.style.color = "#c62828";
                setTimeout(() => {
                  btn.textContent = originalText || "📊 Add to Table";
                  btn.style.backgroundColor = "var(--background-secondary)";
                  btn.style.color = "var(--text-primary)";
                  btn.disabled = false;
                }, 2000);
              }
            } catch (error) {
              Zotero.debug(`[seerai] Add to Table error: ${error}`);
              btn.textContent = "⚠️ Error";
              btn.style.backgroundColor = "#ffebee";
              btn.style.color = "#c62828";
              setTimeout(() => {
                btn.textContent = originalText || "📊 Add to Table";
                btn.style.backgroundColor = "var(--background-secondary)";
                btn.style.color = "var(--text-primary)";
                btn.disabled = false;
              }, 2000);
            }
          },
        },
      ],
    });
    actions.appendChild(addToTableBtn);

    // Open in browser button
    const openBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "🔗 Open" },
      styles: actionBtnStyle,
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            Zotero.launchURL(paper.url);
          },
        },
      ],
    });
    actions.appendChild(openBtn);

    // PDF Download button (only if open access PDF available)
    if (paper.openAccessPdf?.url) {
      const pdfBtn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: "🔗 PDF" },
        styles: {
          ...actionBtnStyle,
          backgroundColor: "#e3f2fd",
          color: "#1976d2",
          border: "1px solid #90caf9",
        },
        listeners: [
          {
            type: "click",
            listener: (e: Event) => {
              e.stopPropagation();
              Zotero.launchURL(paper.openAccessPdf!.url);
            },
          },
        ],
      });
      actions.appendChild(pdfBtn);
    } else {
      // No Semantic Scholar PDF - create PDF Discovery button with state-based handling
      Zotero.debug(
        `[seerai] Creating PDF discovery button for paper: ${paper.title.slice(0, 50)}...`,
      );

      // State to track what the button should do when clicked
      let buttonState:
        | "initial"
        | "searching"
        | "retry"
        | "pdf"
        | "page"
        | "source" = "initial";
      let pdfUrl: string | null = null;
      const pageUrl: string | null = null;
      let sourceUrl: string | null = null;

      const pdfDiscoveryBtn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: "🔍 Find PDF" },
        styles: {
          ...actionBtnStyle,
          backgroundColor: "#e3f2fd",
          color: "#1976d2",
          border: "1px solid #90caf9",
        },
        listeners: [
          {
            type: "click",
            listener: (e: Event) => {
              e.stopPropagation();
              Zotero.debug(`[seerai] Button clicked, state: ${buttonState}`);

              if (buttonState === "initial") {
                runPdfDiscovery();
              } else if (buttonState === "pdf" && pdfUrl) {
                Zotero.launchURL(pdfUrl);
              } else if (buttonState === "page" && pageUrl) {
                Zotero.launchURL(pageUrl);
              } else if (buttonState === "source" && sourceUrl) {
                // Open source link in browser
                Zotero.launchURL(sourceUrl);
                // Change to retry state after opening
                buttonState = "retry";
                pdfDiscoveryBtn.textContent = "🔁 Retry";
                pdfDiscoveryBtn.style.backgroundColor = "#fafafa";
                pdfDiscoveryBtn.style.color = "#757575";
                pdfDiscoveryBtn.style.border = "1px solid #e0e0e0";
              } else if (buttonState === "retry") {
                Zotero.debug(
                  `[seerai] Retry clicked for: ${paper.title.slice(0, 50)}...`,
                );
                // Clear caches for this paper so retry makes fresh API calls
                const zoteroCacheKey =
                  paper.externalIds?.DOI ||
                  paper.externalIds?.ArXiv ||
                  paper.externalIds?.PMID ||
                  paper.title?.slice(0, 50) ||
                  "";
                if (zoteroCacheKey) zoteroFindPdfCache.delete(zoteroCacheKey);
                if (paper.externalIds?.DOI) {
                  unpaywallService.clearCacheForDoi(paper.externalIds.DOI);
                }
                firecrawlService.clearPdfCacheForPaper(
                  paper.title,
                  paper.authors?.map((a) => a.name),
                  paper.externalIds?.DOI,
                );
                runPdfDiscovery();
              }
              // Do nothing if still searching
            },
          },
        ],
      });
      actions.appendChild(pdfDiscoveryBtn);

      // PDF discovery function that updates the button state and appearance
      const runPdfDiscovery = async () => {
        Zotero.debug(
          `[seerai] runPdfDiscovery started for: ${paper.title.slice(0, 50)}...`,
        );
        buttonState = "searching";
        pdfDiscoveryBtn.textContent = "📚 Zotero Lookup...";
        pdfDiscoveryBtn.style.backgroundColor = "#e3f2fd";
        pdfDiscoveryBtn.style.color = "#1976d2";
        pdfDiscoveryBtn.style.border = "1px solid #90caf9";
        pdfDiscoveryBtn.disabled = true;

        try {
          // Step 1: Try Zotero's Find Full Text resolver
          const zoteroResult = await findPdfViaZotero(
            paper.externalIds?.DOI,
            paper.externalIds?.ArXiv,
            paper.externalIds?.PMID,
            paper.title,
            paper.url,
          );
          if (zoteroResult) {
            Zotero.debug(`[seerai] Zotero found PDF: ${zoteroResult}`);
            buttonState = "pdf";
            pdfUrl = zoteroResult;
            pdfDiscoveryBtn.textContent = "📚 Zotero PDF";
            pdfDiscoveryBtn.style.backgroundColor = "#e8f5e9";
            pdfDiscoveryBtn.style.color = "#2e7d32";
            pdfDiscoveryBtn.style.border = "1px solid #a5d6a7";
            pdfDiscoveryBtn.disabled = false;
            return;
          }

          // Step 2: Try arXiv if ArXiv ID available (100% reliable)
          if (paper.externalIds?.ArXiv) {
            pdfDiscoveryBtn.textContent = "📄 arXiv...";
            const arxivResult = await findPdfViaArxiv(paper.externalIds.ArXiv);
            if (arxivResult) {
              Zotero.debug(`[seerai] arXiv found PDF: ${arxivResult}`);
              buttonState = "pdf";
              pdfUrl = arxivResult;
              pdfDiscoveryBtn.textContent = "📄 arXiv PDF";
              pdfDiscoveryBtn.style.backgroundColor = "#e8f5e9";
              pdfDiscoveryBtn.style.color = "#2e7d32";
              pdfDiscoveryBtn.style.border = "1px solid #a5d6a7";
              pdfDiscoveryBtn.disabled = false;
              return;
            }
          }

          // Step 3: Try PubMed Central if PMID available
          if (paper.externalIds?.PMID) {
            pdfDiscoveryBtn.textContent = "🏥 PMC...";
            const pmcResult = await findPdfViaPmc(paper.externalIds.PMID);
            if (pmcResult) {
              Zotero.debug(`[seerai] PMC found PDF: ${pmcResult}`);
              buttonState = "pdf";
              pdfUrl = pmcResult;
              pdfDiscoveryBtn.textContent = "🏥 PMC PDF";
              pdfDiscoveryBtn.style.backgroundColor = "#e8f5e9";
              pdfDiscoveryBtn.style.color = "#2e7d32";
              pdfDiscoveryBtn.style.border = "1px solid #a5d6a7";
              pdfDiscoveryBtn.disabled = false;
              return;
            }
          }

          // Step 4: Try bioRxiv/medRxiv if DOI starts with 10.1101
          if (paper.externalIds?.DOI?.startsWith("10.1101/")) {
            pdfDiscoveryBtn.textContent = "🧬 bioRxiv...";
            const biorxivResult = await findPdfViaBiorxiv(
              paper.externalIds.DOI,
            );
            if (biorxivResult) {
              Zotero.debug(`[seerai] bioRxiv found PDF: ${biorxivResult}`);
              buttonState = "pdf";
              pdfUrl = biorxivResult;
              pdfDiscoveryBtn.textContent = "🧬 bioRxiv PDF";
              pdfDiscoveryBtn.style.backgroundColor = "#e8f5e9";
              pdfDiscoveryBtn.style.color = "#2e7d32";
              pdfDiscoveryBtn.style.border = "1px solid #a5d6a7";
              pdfDiscoveryBtn.disabled = false;
              return;
            }
          }

          // Step 5: Try Unpaywall if DOI available
          if (paper.externalIds?.DOI) {
            pdfDiscoveryBtn.textContent = "🔍 Unpaywall...";
            Zotero.debug(
              `[seerai] Trying Unpaywall for DOI: ${paper.externalIds.DOI}`,
            );
            const unpaywallResult = await unpaywallService.getPdfUrl(
              paper.externalIds.DOI,
            );
            if (unpaywallResult) {
              Zotero.debug(`[seerai] Unpaywall found PDF: ${unpaywallResult}`);
              unpaywallPdfCache.set(paper.paperId, unpaywallResult);
              buttonState = "pdf";
              pdfUrl = unpaywallResult;
              pdfDiscoveryBtn.textContent = "📄 Unpaywall PDF";
              pdfDiscoveryBtn.style.backgroundColor = "#e8f5e9";
              pdfDiscoveryBtn.style.color = "#2e7d32";
              pdfDiscoveryBtn.style.border = "1px solid #a5d6a7";
              pdfDiscoveryBtn.disabled = false;
              return;
            }
          }

          // Step 6: Try Europe PMC as final fallback
          pdfDiscoveryBtn.textContent = "🇪🇺 EuropePMC...";
          const epmcResult = await findPdfViaEuropePmc(
            paper.externalIds?.DOI,
            paper.externalIds?.PMID,
          );
          if (epmcResult) {
            Zotero.debug(`[seerai] EuropePMC found PDF: ${epmcResult}`);
            buttonState = "pdf";
            pdfUrl = epmcResult;
            pdfDiscoveryBtn.textContent = "🇪🇺 EuropePMC PDF";
            pdfDiscoveryBtn.style.backgroundColor = "#e8f5e9";
            pdfDiscoveryBtn.style.color = "#2e7d32";
            pdfDiscoveryBtn.style.border = "1px solid #a5d6a7";
            pdfDiscoveryBtn.disabled = false;
            return;
          }

          // Step 7: Try Firecrawl if configured (commented out)
          /*
                              if (firecrawlService.isConfigured()) {
                                  Zotero.debug(`[seerai] Trying Firecrawl for: ${paper.title.slice(0, 50)}...`);
                                  pdfDiscoveryBtn.textContent = "🔥 Searching...";
                                  pdfDiscoveryBtn.style.backgroundColor = "#fff8e1";
                                  pdfDiscoveryBtn.style.color = "#ff8f00";
                                  pdfDiscoveryBtn.style.border = "1px solid #ffcc80";
          
                                  const firecrawlResult = await firecrawlService.searchForPdf(
                                      paper.title,
                                      paper.authors?.map(a => a.name),
                                      paper.externalIds?.DOI
                                  );
          
                                  Zotero.debug(`[seerai] Firecrawl result: ${JSON.stringify(firecrawlResult)}`);
          
                                  if (firecrawlResult.status === 'pdf_found' && firecrawlResult.pdfUrl) {
                                      firecrawlPdfCache.set(paper.paperId, firecrawlResult);
                                      buttonState = 'pdf';
                                      pdfUrl = firecrawlResult.pdfUrl;
                                      pdfDiscoveryBtn.textContent = "🔥 Fire PDF";
                                      pdfDiscoveryBtn.style.backgroundColor = "#fff3e0";
                                      pdfDiscoveryBtn.style.color = "#e65100";
                                      pdfDiscoveryBtn.style.border = "1px solid #ffcc80";
                                      pdfDiscoveryBtn.disabled = false;
                                      return;
                                  } else if (firecrawlResult.status === 'page_found' && firecrawlResult.pageUrl) {
                                      firecrawlPdfCache.set(paper.paperId, firecrawlResult);
                                      buttonState = 'page';
                                      pageUrl = firecrawlResult.pageUrl;
                                      pdfDiscoveryBtn.textContent = "🔗 Fire-page";
                                      pdfDiscoveryBtn.style.backgroundColor = "#e8eaf6";
                                      pdfDiscoveryBtn.style.color = "#3f51b5";
                                      pdfDiscoveryBtn.style.border = "1px solid #9fa8da";
                                      pdfDiscoveryBtn.disabled = false;
                                      return;
                                  }
                              }
                              */

          // Step 7: Show Source-Link if identifiers available, otherwise Retry
          Zotero.debug(`[seerai] All methods failed, checking for source link`);
          const sourceLink = getSourceLinkForPaper(
            paper.externalIds?.DOI,
            paper.externalIds?.ArXiv,
            paper.externalIds?.PMID,
            undefined,
            paper.url,
          );

          if (sourceLink) {
            buttonState = "source";
            sourceUrl = sourceLink;
            pdfDiscoveryBtn.textContent = "🔗 Source-Link";
            pdfDiscoveryBtn.style.backgroundColor = "#e8eaf6";
            pdfDiscoveryBtn.style.color = "#3f51b5";
            pdfDiscoveryBtn.style.border = "1px solid #9fa8da";
            pdfDiscoveryBtn.disabled = false;
          } else {
            buttonState = "retry";
            pdfDiscoveryBtn.textContent = "🔁 Retry";
            pdfDiscoveryBtn.style.backgroundColor = "#fafafa";
            pdfDiscoveryBtn.style.color = "#757575";
            pdfDiscoveryBtn.style.border = "1px solid #e0e0e0";
            pdfDiscoveryBtn.disabled = false;
          }
        } catch (error) {
          Zotero.debug(
            `[seerai] PDF discovery error for ${paper.paperId}: ${error}`,
          );
          buttonState = "retry";
          pdfDiscoveryBtn.textContent = "🔁 Retry";
          pdfDiscoveryBtn.style.backgroundColor = "#fafafa";
          pdfDiscoveryBtn.style.color = "#757575";
          pdfDiscoveryBtn.style.border = "1px solid #e0e0e0";
          pdfDiscoveryBtn.disabled = false;
        }
      };

      // Removed automatic discovery trigger to improve performance
      // Discovery is now initiated only by user click
    }

    // Find Similar button (recommendations)

    const similarBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "🔮 Similar" },
      styles: actionBtnStyle,
      listeners: [
        {
          type: "click",
          listener: async (e: Event) => {
            e.stopPropagation();
            const btn = e.target as HTMLButtonElement;
            btn.textContent = "Loading...";
            btn.disabled = true;
            try {
              // Get recommendations based on this paper
              const recommendations =
                await semanticScholarService.getRecommendations([
                  paper.paperId,
                ]);
              if (recommendations.length > 0) {
                // Replace current results with recommendations
                currentSearchResults = recommendations;
                totalSearchResults = recommendations.length;
                currentSearchState.query = `Similar to: ${paper.title.slice(0, 50)}...`;
                const resultsArea = doc.getElementById(
                  "semantic-scholar-results",
                );
                if (resultsArea) {
                  this.renderSearchResults(
                    doc,
                    resultsArea as HTMLElement,
                    currentItem!,
                  );
                }
              } else {
                btn.textContent = "No similar";
              }
            } catch (error) {
              Zotero.debug(`[seerai] Recommendations error: ${error}`);
              btn.textContent = "Error";
            }
          },
        },
      ],
    });
    actions.appendChild(similarBtn);

    card.appendChild(actions);

    return card;
  }

  /**
   * Show author details modal
   */
  private static async showAuthorModal(
    doc: Document,
    authorId: string,
    authorName: string,
  ): Promise<void> {
    // Create modal overlay
    const overlay = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "author-modal-overlay" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        right: "0",
        bottom: "0",
        backgroundColor: "rgba(0,0,0,0.5)",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        zIndex: "9999",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            if (e.target === overlay) overlay.remove();
          },
        },
      ],
    });

    // Modal content
    const modal = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        backgroundColor: "var(--background-primary)",
        borderRadius: "8px",
        padding: "16px",
        minWidth: "300px",
        maxWidth: "400px",
        maxHeight: "70vh",
        overflowY: "auto",
        boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
      },
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        marginBottom: "12px",
      },
    });
    const title = ztoolkit.UI.createElement(doc, "h3", {
      properties: { innerText: `👤 ${authorName}` },
      styles: { margin: "0", fontSize: "14px", fontWeight: "600" },
    });
    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        border: "none",
        background: "transparent",
        fontSize: "16px",
        cursor: "pointer",
      },
      listeners: [{ type: "click", listener: () => overlay.remove() }],
    });
    header.appendChild(title);
    header.appendChild(closeBtn);
    modal.appendChild(header);

    // Loading state
    const loadingEl = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "Loading author details..." },
      styles: { color: "var(--text-secondary)", fontSize: "12px" },
    });
    modal.appendChild(loadingEl);

    overlay.appendChild(modal);
    if (doc.body) {
      doc.body.appendChild(overlay);
    } else {
      doc.documentElement?.appendChild(overlay);
    }

    try {
      const authors = await semanticScholarService.getAuthorsBatch([authorId]);
      if (authors.length === 0) {
        loadingEl.textContent = "Author not found";
        return;
      }

      const author = authors[0];
      loadingEl.remove();

      // Stats
      const stats = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          display: "grid",
          gridTemplateColumns: "1fr 1fr 1fr",
          gap: "8px",
          marginBottom: "12px",
        },
      });

      const statItems = [
        { label: "h-Index", value: author.hIndex ?? "N/A" },
        {
          label: "Papers",
          value: author.paperCount?.toLocaleString() ?? "N/A",
        },
        {
          label: "Citations",
          value: author.citationCount?.toLocaleString() ?? "N/A",
        },
      ];

      statItems.forEach((stat) => {
        const statEl = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            textAlign: "center",
            padding: "8px",
            backgroundColor: "var(--background-secondary)",
            borderRadius: "4px",
          },
        });
        statEl.innerHTML = `<div style="font-size: 16px; font-weight: 600;">${stat.value}</div><div style="font-size: 10px; color: var(--text-secondary);">${stat.label}</div>`;
        stats.appendChild(statEl);
      });
      modal.appendChild(stats);

      // Recent papers
      if (author.papers && author.papers.length > 0) {
        const papersLabel = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: "📑 Recent Papers" },
          styles: { fontSize: "12px", fontWeight: "500", marginBottom: "8px" },
        });
        modal.appendChild(papersLabel);

        author.papers.slice(0, 5).forEach((paper) => {
          const paperEl = ztoolkit.UI.createElement(doc, "div", {
            properties: {
              innerText: `${paper.year ? `[${paper.year}] ` : ""}${paper.title}`,
            },
            styles: {
              fontSize: "11px",
              padding: "6px",
              marginBottom: "4px",
              backgroundColor: "var(--background-secondary)",
              borderRadius: "4px",
              cursor: "pointer",
            },
            listeners: [
              {
                type: "click",
                listener: () => {
                  Zotero.launchURL(
                    `https://www.semanticscholar.org/paper/${paper.paperId}`,
                  );
                },
              },
            ],
          });
          modal.appendChild(paperEl);
        });
      }

      // Semantic Scholar link
      if (author.url) {
        const linkBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "🔗 View on Semantic Scholar" },
          styles: {
            width: "100%",
            marginTop: "12px",
            padding: "8px",
            backgroundColor: "var(--highlight-primary)",
            color: "var(--highlight-text)",
            border: "none",
            borderRadius: "4px",
            fontSize: "12px",
            cursor: "pointer",
          },
          listeners: [
            {
              type: "click",
              listener: () => Zotero.launchURL(author.url!),
            },
          ],
        });
        modal.appendChild(linkBtn);
      }
    } catch (error) {
      loadingEl.textContent = `Error: ${error instanceof Error ? error.message : "Failed to load"}`;
      Zotero.debug(`[seerai] Author modal error: ${error}`);
    }
  }

  /**
   * Add a Semantic Scholar paper to Zotero library
   *
   * Follows Zotero 7 API patterns:
   * 1. Create item with new Zotero.Item(type)
   * 2. Set libraryID
   * 3. Populate fields with setField()
   * 4. First saveTx() to generate item ID
   * 5. Add to collection if configured
   * 6. Second saveTx() to persist collection relationship
   * 7. Attach PDF if available
   */
  private static async addPaperToZotero(
    paper: SemanticScholarPaper,
  ): Promise<Zotero.Item | null> {
    try {
      Zotero.debug(`[seerai] Adding paper to Zotero: ${paper.title}`);

      // Determine item type based on publication types
      type ZoteroItemType = "journalArticle" | "conferencePaper" | "book";
      let itemType: ZoteroItemType = "journalArticle";
      if (paper.publicationTypes) {
        if (paper.publicationTypes.includes("Conference")) {
          itemType = "conferencePaper";
        } else if (paper.publicationTypes.includes("Book")) {
          itemType = "book";
        }
      }

      // 1. Create the item
      const newItem = new Zotero.Item(itemType);

      // 2. Determine library ownership based on selected save location
      const saveLocation = currentSearchState.saveLocation || "user";
      let targetLibraryId = Zotero.Libraries.userLibraryID;
      let targetCollectionId: number | null = null;

      if (saveLocation === "user") {
        // Default: user library
        targetLibraryId = Zotero.Libraries.userLibraryID;
      } else if (saveLocation.startsWith("lib_")) {
        // Group library
        targetLibraryId = parseInt(saveLocation.replace("lib_", ""), 10);
      } else if (saveLocation.startsWith("col_")) {
        // Collection - need to find its library ID
        targetCollectionId = parseInt(saveLocation.replace("col_", ""), 10);
        try {
          const collection = Zotero.Collections.get(targetCollectionId);
          if (collection) {
            targetLibraryId = collection.libraryID;
          }
        } catch (e) {
          Zotero.debug(
            `[seerai] Error getting collection ${targetCollectionId}: ${e}`,
          );
        }
      }

      newItem.libraryID = targetLibraryId;

      // 3. Populate metadata fields
      newItem.setField("title", paper.title);

      if (paper.abstract) {
        newItem.setField("abstractNote", paper.abstract);
      }
      if (paper.year) {
        newItem.setField("date", String(paper.year));
      }
      if (paper.venue) {
        // Use appropriate field based on item type
        if (itemType === "conferencePaper") {
          newItem.setField("proceedingsTitle", paper.venue);
        } else {
          newItem.setField("publicationTitle", paper.venue);
        }
      }
      if (paper.externalIds?.DOI) {
        newItem.setField("DOI", paper.externalIds.DOI);
      }
      if (paper.url) {
        newItem.setField("url", paper.url);
      }

      // Add authors/creators
      if (paper.authors && paper.authors.length > 0) {
        const creators = paper.authors.slice(0, 20).map((author) => {
          const nameParts = author.name.trim().split(" ");
          const lastName = nameParts.pop() || author.name;
          const firstName = nameParts.join(" ");
          return {
            firstName,
            lastName,
            creatorType: "author" as const,
          };
        });
        newItem.setCreators(creators);
      }

      // 4. First save to generate item ID (required before collection assignment)
      await newItem.saveTx();
      Zotero.debug(
        `[seerai] Item saved with ID: ${newItem.id} to library ${targetLibraryId}`,
      );

      // 5. Add to collection if one was selected
      if (targetCollectionId !== null) {
        try {
          newItem.addToCollection(targetCollectionId);
          await newItem.saveTx(); // Second save to persist collection relationship
          Zotero.debug(
            `[seerai] Item added to collection ${targetCollectionId}`,
          );
        } catch (colError) {
          Zotero.debug(`[seerai] Error adding to collection: ${colError}`);
        }
      }

      // 6. Attach PDF if open access URL is available, otherwise try Find Full Text
      if (paper.openAccessPdf?.url) {
        try {
          Zotero.debug(
            `[seerai] Downloading PDF from: ${paper.openAccessPdf.url}`,
          );

          // Use Zotero's built-in attachment import from URL
          await Zotero.Attachments.importFromURL({
            url: paper.openAccessPdf.url,
            parentItemID: newItem.id,
            title: `${paper.title}.pdf`,
            contentType: "application/pdf",
          });

          Zotero.debug(`[seerai] PDF attached successfully`);
        } catch (pdfError) {
          // PDF download failure - try Find Full Text as fallback
          Zotero.debug(
            `[seerai] PDF download failed, trying Find Full Text: ${pdfError}`,
          );
          try {
            await (Zotero.Attachments as any).addAvailablePDF(newItem);
            Zotero.debug(`[seerai] Find Full Text initiated`);
          } catch (findError) {
            Zotero.debug(
              `[seerai] Find Full Text failed (non-fatal): ${findError}`,
            );
          }
        }
      } else {
        // No Semantic Scholar PDF - check Unpaywall cache first
        const cachedUnpaywallUrl = unpaywallPdfCache.get(paper.paperId);
        if (cachedUnpaywallUrl) {
          try {
            Zotero.debug(`[seerai] Using Unpaywall PDF: ${cachedUnpaywallUrl}`);
            await Zotero.Attachments.importFromURL({
              url: cachedUnpaywallUrl,
              parentItemID: newItem.id,
              title: `${paper.title}.pdf`,
              contentType: "application/pdf",
            });
            Zotero.debug(`[seerai] Unpaywall PDF attached successfully`);
          } catch (pdfError) {
            // Unpaywall download failed - try Find Full Text
            Zotero.debug(
              `[seerai] Unpaywall PDF download failed, trying Find Full Text: ${pdfError}`,
            );
            try {
              await (Zotero.Attachments as any).addAvailablePDF(newItem);
              Zotero.debug(`[seerai] Find Full Text initiated`);
            } catch (findError) {
              Zotero.debug(
                `[seerai] Find Full Text failed (non-fatal): ${findError}`,
              );
            }
          }
        } else {
          // No cached Unpaywall PDF - check Firecrawl cache
          const cachedFirecrawlResult = firecrawlPdfCache.get(paper.paperId);
          if (cachedFirecrawlResult?.pdfUrl) {
            try {
              Zotero.debug(
                `[seerai] Using Firecrawl PDF: ${cachedFirecrawlResult.pdfUrl}`,
              );
              await Zotero.Attachments.importFromURL({
                url: cachedFirecrawlResult.pdfUrl,
                parentItemID: newItem.id,
                title: `${paper.title}.pdf`,
                contentType: "application/pdf",
              });
              Zotero.debug(`[seerai] Firecrawl PDF attached successfully`);
            } catch (pdfError) {
              // Firecrawl download failed - try Find Full Text
              Zotero.debug(
                `[seerai] Firecrawl PDF download failed, trying Find Full Text: ${pdfError}`,
              );
              try {
                await (Zotero.Attachments as any).addAvailablePDF(newItem);
                Zotero.debug(`[seerai] Find Full Text initiated`);
              } catch (findError) {
                Zotero.debug(
                  `[seerai] Find Full Text failed (non-fatal): ${findError}`,
                );
              }
            }
          } else {
            // No cached PDF - trigger Zotero's "Find Full Text"
            try {
              Zotero.debug(
                `[seerai] No PDF available, initiating Find Full Text...`,
              );
              await (Zotero.Attachments as any).addAvailablePDF(newItem);
              Zotero.debug(`[seerai] Find Full Text initiated`);
            } catch (findError) {
              // Find Full Text failure is non-fatal
              Zotero.debug(
                `[seerai] Find Full Text failed (non-fatal): ${findError}`,
              );
            }
          }
        }
      }

      return newItem;
    } catch (error) {
      Zotero.debug(`[seerai] Error adding paper to Zotero: ${error}`);
      return null;
    }
  }

  /**
   * Add paper to Zotero with full PDF discovery pipeline (steps 1-6)
   * Uses the same discovery process as the search card PDF button
   * Returns both the item and whether PDF was successfully attached
   */
  public static async addPaperToZoteroWithPdfDiscovery(
    paper: SemanticScholarPaper,
    statusBtn?: HTMLButtonElement,
    targetColId?: number,
    waitForPdf: boolean = true,
    triggerOcr: boolean = false,
  ): Promise<{
    item: Zotero.Item | null;
    pdfAttached: boolean;
    sourceUrl?: string;
  }> {
    try {
      Zotero.debug(
        `[seerai] Adding paper to Zotero with PDF discovery: ${paper.title}`,
      );

      // Determine item type based on publication types
      type ZoteroItemType = "journalArticle" | "conferencePaper" | "book";
      let itemType: ZoteroItemType = "journalArticle";
      if (paper.publicationTypes) {
        if (paper.publicationTypes.includes("Conference")) {
          itemType = "conferencePaper";
        } else if (paper.publicationTypes.includes("Book")) {
          itemType = "book";
        }
      }

      // 1. Determine library ownership and collection
      const saveLocation = currentSearchState.saveLocation || "user";
      let targetLibraryId = Zotero.Libraries.userLibraryID;
      let targetCollectionId: number | null = targetColId || null;

      // If a collection ID was explicitly provided, use its library
      if (targetCollectionId) {
        try {
          const collection = Zotero.Collections.get(targetCollectionId);
          if (collection) {
            targetLibraryId = collection.libraryID;
          }
        } catch (e) {
          Zotero.debug(
            `[seerai] Error getting library for collection ${targetCollectionId}: ${e}`,
          );
        }
      } else if (saveLocation === "user") {
        targetLibraryId = Zotero.Libraries.userLibraryID;
      } else if (saveLocation.startsWith("lib_")) {
        targetLibraryId = parseInt(saveLocation.replace("lib_", ""), 10);
      } else if (saveLocation.startsWith("col_")) {
        targetCollectionId = parseInt(saveLocation.replace("col_", ""), 10);
        try {
          const collection = Zotero.Collections.get(targetCollectionId);
          if (collection) {
            targetLibraryId = collection.libraryID;
          }
        } catch (e) {
          Zotero.debug(
            `[seerai] Error getting collection ${targetCollectionId}: ${e}`,
          );
        }
      }

      // 2. Create the item
      const newItem = new Zotero.Item(itemType);
      newItem.libraryID = targetLibraryId;

      // 3. Populate metadata fields
      newItem.setField("title", paper.title);

      if (paper.abstract) {
        newItem.setField("abstractNote", paper.abstract);
      }
      if (paper.year) {
        newItem.setField("date", String(paper.year));
      }
      if (paper.venue) {
        if (itemType === "conferencePaper") {
          newItem.setField("proceedingsTitle", paper.venue);
        } else {
          newItem.setField("publicationTitle", paper.venue);
        }
      }
      if (paper.externalIds?.DOI) {
        newItem.setField("DOI", paper.externalIds.DOI);
      }
      if (paper.url) {
        newItem.setField("url", paper.url);
      }

      // Add authors/creators
      if (paper.authors && paper.authors.length > 0) {
        const creators = paper.authors.slice(0, 20).map((author) => {
          const nameParts = author.name.trim().split(" ");
          const lastName = nameParts.pop() || author.name;
          const firstName = nameParts.join(" ");
          return {
            firstName,
            lastName,
            creatorType: "author" as const,
          };
        });
        newItem.setCreators(creators);
      }

      // 4. Save to generate item ID
      await newItem.saveTx();
      Zotero.debug(
        `[seerai] Item saved with ID: ${newItem.id} to library ${targetLibraryId}`,
      );

      // 5. Add to collection if one was selected
      if (targetCollectionId !== null) {
        try {
          newItem.addToCollection(targetCollectionId);
          await newItem.saveTx();
          Zotero.debug(
            `[seerai] Item added to collection ${targetCollectionId}`,
          );
        } catch (colError) {
          Zotero.debug(`[seerai] Error adding to collection: ${colError}`);
        }
      }

      // Helper for status updates
      const updateStatus = (text: string) => {
        if (statusBtn) {
          statusBtn.textContent = text;
        }
      };

      // 6. Define PDF discovery logic
      const performDiscovery = async (): Promise<boolean> => {
        let pdfAttached = false;

        // If Semantic Scholar open access PDF is available, use it directly
        if (paper.openAccessPdf?.url) {
          updateStatus("📥 Attaching PDF...");
          try {
            await Zotero.Attachments.importFromURL({
              url: paper.openAccessPdf.url,
              parentItemID: newItem.id,
              title: `${paper.title}.pdf`,
              contentType: "application/pdf",
            });
            Zotero.debug(`[seerai] Semantic Scholar PDF attached`);
            pdfAttached = true;
          } catch (pdfError) {
            Zotero.debug(`[seerai] SS PDF attach failed: ${pdfError}`);
          }
        }

        // Run PDF discovery pipeline if no PDF attached yet
        if (!pdfAttached) {
          // Use unified PDF discovery pipeline
          pdfAttached = await findAndAttachPdfForItem(newItem, updateStatus);
        }

        // Trigger OCR if requested and PDF attached
        if (triggerOcr && pdfAttached) {
          const ocrService = Assistant.getOcrService();
          const pdf = ocrService.getFirstPdfAttachment(newItem);
          if (pdf) {
            Zotero.debug(
              `[seerai] Triggering background OCR for imported paper ${newItem.id}`,
            );
            // We don't await this if it's already in a background task,
            // but if we are waiting for PDF, we might want to wait for OCR too?
            // User said "until all series of import paper completed", which might include OCR.
            // For now, let's await it so the agent knows when it's FULLY done if waitForPdf is true.
            await ocrService.convertToMarkdown(pdf, { showProgress: false });
          }
        }

        Zotero.debug(
          `[seerai] Paper import discovery complete for ${newItem.id}, success: ${pdfAttached}`,
        );
        return pdfAttached;
      };

      // Handle backgrounding vs waiting
      if (waitForPdf) {
        const attached = await performDiscovery();
        return { item: newItem, pdfAttached: attached };
      } else {
        // Run in background
        performDiscovery().catch((e) =>
          Zotero.debug(`[seerai] Background PDF discovery failed: ${e}`),
        );
        return { item: newItem, pdfAttached: false };
      }
    } catch (error) {
      Zotero.debug(`[seerai] Error adding paper with PDF discovery: ${error}`);
      return { item: null, pdfAttached: false };
    }
  }

  /**
   * Try to find and attach PDF for an already-created Zotero item
   * Used for retry functionality after Source-Link is clicked
   */
  private static async tryFindPdfForItem(
    paper: SemanticScholarPaper,
    item: Zotero.Item,
  ): Promise<boolean> {
    try {
      Zotero.debug(
        `[seerai] Retrying PDF discovery for item ${item.id}: ${paper.title.slice(0, 50)}...`,
      );

      // Step 1: Try Zotero's Find Full Text resolver
      const zoteroResult = await findPdfViaZotero(
        paper.externalIds?.DOI,
        paper.externalIds?.ArXiv,
        paper.externalIds?.PMID,
        paper.title,
        paper.url,
      );
      if (zoteroResult) {
        await Zotero.Attachments.importFromURL({
          url: zoteroResult,
          parentItemID: item.id,
          title: `${paper.title}.pdf`,
          contentType: "application/pdf",
        });
        return true;
      }

      // Step 2: Try arXiv
      if (paper.externalIds?.ArXiv) {
        const arxivResult = await findPdfViaArxiv(paper.externalIds.ArXiv);
        if (arxivResult) {
          await Zotero.Attachments.importFromURL({
            url: arxivResult,
            parentItemID: item.id,
            title: `${paper.title}.pdf`,
            contentType: "application/pdf",
          });
          return true;
        }
      }

      // Step 3: Try PMC
      if (paper.externalIds?.PMID) {
        const pmcResult = await findPdfViaPmc(paper.externalIds.PMID);
        if (pmcResult) {
          await Zotero.Attachments.importFromURL({
            url: pmcResult,
            parentItemID: item.id,
            title: `${paper.title}.pdf`,
            contentType: "application/pdf",
          });
          return true;
        }
      }

      // Step 4: Try bioRxiv
      if (paper.externalIds?.DOI?.startsWith("10.1101/")) {
        const biorxivResult = await findPdfViaBiorxiv(paper.externalIds.DOI);
        if (biorxivResult) {
          await Zotero.Attachments.importFromURL({
            url: biorxivResult,
            parentItemID: item.id,
            title: `${paper.title}.pdf`,
            contentType: "application/pdf",
          });
          return true;
        }
      }

      // Step 5: Try Unpaywall
      if (paper.externalIds?.DOI) {
        const unpaywallResult = await unpaywallService.getPdfUrl(
          paper.externalIds.DOI,
        );
        if (unpaywallResult) {
          await Zotero.Attachments.importFromURL({
            url: unpaywallResult,
            parentItemID: item.id,
            title: `${paper.title}.pdf`,
            contentType: "application/pdf",
          });
          return true;
        }
      }

      // Step 6: Try Europe PMC
      const epmcResult = await findPdfViaEuropePmc(
        paper.externalIds?.DOI,
        paper.externalIds?.PMID,
      );
      if (epmcResult) {
        await Zotero.Attachments.importFromURL({
          url: epmcResult,
          parentItemID: item.id,
          title: `${paper.title}.pdf`,
          contentType: "application/pdf",
        });
        return true;
      }

      // Final fallback: Zotero Find Full Text
      try {
        await (Zotero.Attachments as any).addAvailablePDF(item);
      } catch (err) {
        // Non-fatal
      }

      return false;
    } catch (error) {
      Zotero.debug(`[seerai] Retry PDF discovery error: ${error}`);
      return false;
    }
  }

  /**
   * Convert a Semantic Scholar paper to BibTeX format
   */
  private static paperToBibtex(paper: SemanticScholarPaper): string {
    // Generate a unique cite key: firstAuthorLastName + year + firstTitleWord
    let citeKey = "unknown";
    if (paper.authors && paper.authors.length > 0) {
      const firstAuthor = paper.authors[0].name.trim();
      const lastName = firstAuthor.split(" ").pop() || "unknown";
      const year = paper.year || "nd";
      const titleWord =
        paper.title.split(/\s+/)[0]?.replace(/[^a-zA-Z]/g, "") || "paper";
      citeKey = `${lastName.toLowerCase()}${year}${titleWord.toLowerCase()}`;
    }

    // Determine entry type
    let entryType = "@article";
    if (paper.publicationTypes) {
      if (paper.publicationTypes.includes("Conference")) {
        entryType = "@inproceedings";
      } else if (paper.publicationTypes.includes("Book")) {
        entryType = "@book";
      }
    }

    // Format authors: "LastName, FirstName and LastName, FirstName"
    let authorsStr = "";
    if (paper.authors && paper.authors.length > 0) {
      authorsStr = paper.authors
        .map((a) => {
          const parts = a.name.trim().split(" ");
          const lastName = parts.pop() || "";
          const firstName = parts.join(" ");
          return firstName ? `${lastName}, ${firstName}` : lastName;
        })
        .join(" and ");
    }

    // Escape special BibTeX characters
    const escapeLatex = (str: string): string => {
      return str
        .replace(/\\/g, "\\textbackslash{}")
        .replace(/&/g, "\\&")
        .replace(/%/g, "\\%")
        .replace(/\$/g, "\\$")
        .replace(/#/g, "\\#")
        .replace(/_/g, "\\_")
        .replace(/\{/g, "\\{")
        .replace(/\}/g, "\\}")
        .replace(/~/g, "\\textasciitilde{}")
        .replace(/\^/g, "\\textasciicircum{}");
    };

    // Build the BibTeX entry
    const lines: string[] = [];
    lines.push(`${entryType}{${citeKey},`);
    lines.push(`  title = {${escapeLatex(paper.title)}},`);

    if (authorsStr) {
      lines.push(`  author = {${escapeLatex(authorsStr)}},`);
    }
    if (paper.year) {
      lines.push(`  year = {${paper.year}},`);
    }
    if (paper.venue) {
      const venueField =
        entryType === "@inproceedings" ? "booktitle" : "journal";
      lines.push(`  ${venueField} = {${escapeLatex(paper.venue)}},`);
    }
    if (paper.externalIds?.DOI) {
      lines.push(`  doi = {${paper.externalIds.DOI}},`);
    }
    if (paper.url) {
      lines.push(`  url = {${paper.url}},`);
    }
    if (paper.abstract) {
      lines.push(`  abstract = {${escapeLatex(paper.abstract)}},`);
    }

    // Remove trailing comma from last field
    if (lines.length > 1) {
      lines[lines.length - 1] = lines[lines.length - 1].replace(/,$/, "");
    }
    lines.push("}");

    return lines.join("\n");
  }

  /**
   * Export all current search results as a BibTeX file
   */
  private static async exportResultsAsBibtex(): Promise<void> {
    if (currentSearchResults.length === 0) {
      Zotero.debug("[seerai] No search results to export");
      return;
    }

    try {
      // Generate BibTeX for all papers
      const bibtexEntries = currentSearchResults.map((paper) =>
        this.paperToBibtex(paper),
      );
      const bibtexContent = bibtexEntries.join("\n\n");

      // Create file picker for save location using ztoolkit
      const defaultFileName = `semantic_scholar_export_${new Date().toISOString().slice(0, 10)}.bib`;

      const filePath = await new ztoolkit.FilePicker(
        "Export BibTeX",
        "save",
        [["BibTeX Files", "*.bib"]],
        defaultFileName,
      ).open();

      if (filePath) {
        // Write the file using Zotero.File
        await Zotero.File.putContentsAsync(filePath, bibtexContent);
        Zotero.debug(
          `[seerai] Exported ${currentSearchResults.length} papers to BibTeX: ${filePath}`,
        );
      }
    } catch (error) {
      Zotero.debug(`[seerai] Error exporting BibTeX: ${error}`);
    }
  }

  /**
   * Create the table toolbar with filter, add papers, generate, export buttons
   */
  private static createTableToolbar(
    doc: Document,
    item: Zotero.Item,
  ): HTMLElement {
    const toolbar = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "table-toolbar" },
      styles: {
        display: "flex",
        gap: "8px",
        padding: "8px",
        backgroundColor: "var(--background-secondary)",
        borderBottom: "1px solid var(--border-primary)",
        alignItems: "center",
        flexWrap: "wrap",
        flexShrink: "0",
        width: "100%",
        minWidth: "0",
        boxSizing: "border-box",
      },
    });

    // Workspace Title (Persistent & Editable)
    const titleContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        alignItems: "center",
        gap: "4px",
        marginRight: "8px",
        padding: "2px 8px",
        borderRadius: "4px",
        border: "1px solid transparent",
        cursor: "pointer",
        transition: "all 0.2s",
      },
      listeners: [
        {
          type: "mouseenter",
          listener: () => {
            if (!titleContainer.classList.contains("editing")) {
              titleContainer.style.backgroundColor =
                "var(--background-secondary)";
              titleContainer.style.border = "1px solid var(--border-primary)";
            }
          },
        },
        {
          type: "mouseleave",
          listener: () => {
            if (!titleContainer.classList.contains("editing")) {
              titleContainer.style.backgroundColor = "transparent";
              titleContainer.style.border = "1px solid transparent";
            }
          },
        },
        {
          type: "click",
          listener: () => {
            if (titleContainer.classList.contains("editing")) return;

            const nameLabel = titleContainer.querySelector(
              ".workspace-name-label",
            ) as HTMLElement;
            const currentName =
              currentTableConfig?.name || "Untitled Workspace";

            // Switch to edit mode
            titleContainer.classList.add("editing");
            titleContainer.innerHTML = "";

            const input = ztoolkit.UI.createElement(doc, "input", {
              attributes: { type: "text", value: currentName },
              styles: {
                fontSize: "12px",
                fontWeight: "600",
                padding: "2px 4px",
                border: "1px solid var(--highlight-primary)",
                borderRadius: "2px",
                outline: "none",
                width: "150px",
                color: "var(--text-primary)",
                backgroundColor: "var(--background-primary)",
              },
            }) as HTMLInputElement;

            const saveName = async () => {
              const newName = input.value.trim() || "Untitled Workspace";
              if (currentTableConfig) {
                currentTableConfig.name = newName;
                const tableStore = getTableStore();
                await tableStore.saveConfig(currentTableConfig);
              }
              // Re-render title
              renderTitle();
              titleContainer.classList.remove("editing");
            };

            input.addEventListener("blur", saveName);
            input.addEventListener("keypress", (e: Event) => {
              const ke = e as KeyboardEvent;
              if (ke.key === "Enter") {
                input.blur();
              }
            });

            titleContainer.appendChild(input);
            input.focus();
            input.select();
          },
        },
      ],
    });

    const renderTitle = () => {
      titleContainer.innerHTML = "";
      const prefix = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: "WORKSPACE:" },
        styles: {
          fontSize: "10px",
          color: "var(--text-secondary)",
          fontWeight: "600",
          letterSpacing: "0.5px",
        },
      });

      const name = ztoolkit.UI.createElement(doc, "span", {
        properties: {
          className: "workspace-name-label",
          innerText: currentTableConfig?.name || "Untitled Workspace",
        },
        styles: {
          fontSize: "12px",
          fontWeight: "600",
          color: "var(--text-primary)",
        },
      });

      const editIcon = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: "✎" },
        styles: {
          fontSize: "10px",
          color: "var(--text-tertiary)",
          opacity: "0.5",
        },
      });

      titleContainer.appendChild(prefix);
      titleContainer.appendChild(name);
      titleContainer.appendChild(editIcon);
    };

    renderTitle();
    toolbar.appendChild(titleContainer);

    // Library/Collection filter dropdown
    const filterContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        alignItems: "center",
        gap: "4px",
      },
    });

    const filterLabel = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "📁" },
      styles: { fontSize: "12px" },
    });
    filterContainer.appendChild(filterLabel);

    const filterSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        padding: "6px 8px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "12px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        minWidth: "120px",
      },
      listeners: [
        {
          type: "change",
          listener: async (e: Event) => {
            const select = e.target as HTMLSelectElement;
            const value = select.value;
            if (currentTableConfig) {
              if (value === "all") {
                currentTableConfig.filterLibraryId = null;
                currentTableConfig.filterCollectionId = null;
              } else if (value.startsWith("lib_")) {
                currentTableConfig.filterLibraryId = parseInt(
                  value.replace("lib_", ""),
                  10,
                );
                currentTableConfig.filterCollectionId = null;
              } else if (value.startsWith("col_")) {
                currentTableConfig.filterCollectionId = parseInt(
                  value.replace("col_", ""),
                  10,
                );
              }
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
            }
          },
        },
      ],
    }) as HTMLSelectElement;

    // Populate filter options
    this.populateFilterSelect(filterSelect);
    filterContainer.appendChild(filterSelect);
    toolbar.appendChild(filterContainer);

    // Search input
    const searchInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "text", placeholder: "🔍 Filter table..." },
      properties: { className: "table-search-input" },
      styles: {
        flex: "1",
        minWidth: "60px",
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "12px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        outline: "none",
        boxSizing: "border-box",
      },
      listeners: [
        {
          type: "input",
          listener: (e: Event) => {
            const target = e.target as HTMLInputElement;
            if (currentTableConfig) {
              currentTableConfig.filterQuery = target.value;
              // Persist filter query to table config so it's restored on reload/page switch
              getTableStore().saveConfig(currentTableConfig);
            }
            this.debounceTableRefresh(doc, item);
          },
        },
      ],
    }) as HTMLInputElement;

    if (currentTableConfig?.filterQuery) {
      searchInput.value = currentTableConfig.filterQuery;
    }
    toolbar.appendChild(searchInput);

    // Search Help Icon
    const searchHelp = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "❗" },
      attributes: {
        title:
          'Search Logic Supported:\n• AND, OR, NOT\n• ( ) Grouping\n• "Exact Phrase"\nExample: "Climate Change" AND (Policy OR Mitigation)',
      },
      styles: {
        fontSize: "14px",
        cursor: "help",
        marginLeft: "4px",
        marginRight: "4px",
        opacity: "0.7",
      },
    });
    toolbar.appendChild(searchHelp);

    // Add Papers button
    const addPapersBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        className: "table-btn table-btn-primary",
        innerText: "➕ Add Papers",
      },
      styles: {
        padding: "6px 12px",
        fontSize: "11px",
        border: "none",
        borderRadius: "4px",
        backgroundColor: "var(--highlight-primary)",
        color: "var(--highlight-text)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        gap: "4px",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            this.showTablePaperPicker(doc, item);
          },
        },
      ],
    });
    toolbar.appendChild(addPapersBtn);

    // Extract All button (for OCR)
    const extractBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        className: "table-btn extract-all-btn",
        innerText: "📄 Extract All",
      },
      attributes: { id: "extract-all-btn" },
      styles: {
        padding: "6px 12px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.extractAllEmptyPDFs(doc, item);
          },
        },
      ],
    });
    toolbar.appendChild(extractBtn);

    // Search all PDF button (find PDFs for items without attachments)
    // Show progress if batch is active
    const searchPdfBtnText = pdfSearchBatchActive
      ? `🔍 ${pdfSearchBatchStats.completed}/${pdfSearchBatchStats.total}`
      : "🔍 Search all PDF";
    const searchPdfBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        className: "table-btn search-pdf-all-btn",
        innerText: searchPdfBtnText,
      },
      attributes: { id: "search-pdf-all-btn" },
      styles: {
        padding: "6px 12px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: pdfSearchBatchActive ? "wait" : "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            if (pdfSearchBatchActive) return; // Don't start new search while one is running
            await this.searchAllPdfsInTable(doc, searchPdfBtn);
          },
        },
      ],
    });
    if (pdfSearchBatchActive) {
      (searchPdfBtn as HTMLButtonElement).disabled = true;
    }
    toolbar.appendChild(searchPdfBtn);

    // Export button
    const exportBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { className: "table-btn", innerText: "📤" },
      attributes: { title: "Export to CSV" },
      styles: {
        padding: "6px 10px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.exportTableToCSV();
          },
        },
      ],
    });
    toolbar.appendChild(exportBtn);

    // Save as Notes button (Data Traceability)
    const saveAsNotesBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { className: "table-btn", innerText: "📋 Notes" },
      attributes: { title: "Save table data as notes attached to each paper" },
      styles: {
        padding: "6px 12px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const rowCount = currentTableConfig?.addedPaperIds?.length || 0;
            if (rowCount === 0) {
              doc.defaultView?.alert("No papers in table to save as notes.");
              return;
            }
            const confirmed = doc.defaultView?.confirm(
              `Save table data for ${rowCount} paper(s) as notes?\n\nThis will create/update a "📊 Tables" note attached to each paper.`,
            );
            if (confirmed) {
              await this.saveAllRowsAsNotes(doc);
            }
          },
        },
      ],
    });
    toolbar.appendChild(saveAsNotesBtn);

    // Save button
    const saveBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { className: "table-btn", innerText: "💾" },
      attributes: { title: "Save workspace to history" },
      styles: {
        padding: "6px 10px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.saveWorkspaceToHistory(doc);
          },
        },
      ],
    });
    toolbar.appendChild(saveBtn);

    // Copy All Data button
    const copyAllBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { className: "table-btn", innerText: "📋" },
      attributes: { title: "Copy all table data to clipboard" },
      styles: {
        padding: "6px 10px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.copyAllTableData(doc);
          },
        },
      ],
    });
    toolbar.appendChild(copyAllBtn);

    // Export to Workspace button
    const exportWsBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { className: "table-btn", innerText: "📁 Workspace" },
      attributes: { title: "Export table to workspace" },
      styles: {
        padding: "6px 10px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.showExportToWorkspacePopup(doc, exportWsBtn);
          },
        },
      ],
    });
    toolbar.appendChild(exportWsBtn);

    // History button
    const historyBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { className: "table-btn", innerText: "📜" },
      attributes: { title: "Load from history" },
      styles: {
        padding: "6px 10px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            this.showWorkspacePicker(doc, item);
          },
        },
      ],
    });
    toolbar.appendChild(historyBtn);

    // Start Fresh button
    const startFreshBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { className: "table-btn", innerText: "🔄 New" },
      attributes: { title: "Start fresh workspace" },
      styles: {
        padding: "6px 10px",
        fontSize: "11px",
        border: "1px solid #cc6666",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "#cc6666",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.startFreshWorkspace(doc, item);
          },
        },
      ],
    });
    toolbar.appendChild(startFreshBtn);

    // === PAGINATION CONTROLS ===
    const paginationContainer = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "pagination-container" },
      styles: {
        display: "flex",
        alignItems: "center",
        gap: "8px",
        marginLeft: "auto", // Push to right
        padding: "2px 8px",
        borderLeft: "1px solid var(--border-primary)",
      },
    });

    // Page Size Selector
    const pageSizeLabel = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "Lines:" },
      styles: { fontSize: "11px", color: "var(--text-secondary)" },
    });
    paginationContainer.appendChild(pageSizeLabel);

    const pageSizeSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        padding: "2px 4px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "3px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
      },
      listeners: [
        {
          type: "change",
          listener: async (e: Event) => {
            const select = e.target as HTMLSelectElement;
            const newSize = parseInt(select.value, 10);
            if (currentTableConfig) {
              currentTableConfig.pageSize = newSize;
              currentTableConfig.currentPage = 1; // Reset to page 1 on size change
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
              await this.forceRefreshTable();
            }
          },
        },
      ],
    }) as HTMLSelectElement;

    [25, 50, 100, 200, 500, 1000].forEach((size) => {
      const opt = ztoolkit.UI.createElement(doc, "option", {
        properties: {
          value: String(size),
          innerText: String(size),
          selected: currentTableConfig?.pageSize === size,
        },
      });
      pageSizeSelect.appendChild(opt);
    });
    paginationContainer.appendChild(pageSizeSelect);

    // Page info
    const currentPage = currentTableConfig?.currentPage || 1;
    const pageSize = currentTableConfig?.pageSize || 25;
    const totalItems = currentTableConfig?.addedPaperIds?.length || 0;
    const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));

    // Prev Button
    const prevBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "◀", disabled: currentPage <= 1 },
      styles: {
        background: "none",
        border: "none",
        cursor: currentPage <= 1 ? "default" : "pointer",
        padding: "2px 4px",
        opacity: currentPage <= 1 ? "0.3" : "0.7",
        fontSize: "12px",
        color: "var(--text-primary)",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.handleTablePagination(-1);
          },
        },
      ],
    });
    paginationContainer.appendChild(prevBtn);

    // Page Indicator
    const pageIndicator = ztoolkit.UI.createElement(doc, "span", {
      properties: {
        id: "table-page-indicator",
        innerText: `${currentPage} / ${totalPages}`,
      },
      styles: {
        fontSize: "11px",
        fontWeight: "600",
        minWidth: "40px",
        textAlign: "center",
      },
    });
    paginationContainer.appendChild(pageIndicator);

    // Next Button
    const nextBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "▶", disabled: currentPage >= totalPages },
      styles: {
        background: "none",
        border: "none",
        cursor: currentPage >= totalPages ? "default" : "pointer",
        padding: "2px 4px",
        opacity: currentPage >= totalPages ? "0.3" : "0.7",
        fontSize: "12px",
        color: "var(--text-primary)",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.handleTablePagination(1);
          },
        },
      ],
    });
    paginationContainer.appendChild(nextBtn);

    toolbar.appendChild(paginationContainer);

    return toolbar;
  }

  /**
   * Populate the library/collection filter dropdown
   */
  private static populateFilterSelect(select: HTMLSelectElement): void {
    const doc = select.ownerDocument;
    if (!doc) return;
    // All libraries option
    const allOption = doc.createElement("option");
    allOption.value = "all";
    allOption.textContent = "All Libraries";
    select.appendChild(allOption);

    try {
      const libraries = Zotero.Libraries.getAll();
      for (const library of libraries) {
        // Library option
        const libOption = doc.createElement("option");
        libOption.value = `lib_${library.libraryID}`;
        libOption.textContent = `📚 ${library.name}`;
        if (
          currentTableConfig?.filterLibraryId === library.libraryID &&
          !currentTableConfig?.filterCollectionId
        ) {
          libOption.selected = true;
        }
        select.appendChild(libOption);

        // Get collections for this library (recursive)
        const collections = Zotero.Collections.getByLibrary(
          library.libraryID,
          true,
        );

        // Build hierarchy
        const colMap = new Map<number, Zotero.Collection>();
        const childrenMap = new Map<number, Zotero.Collection[]>();
        const rootCols: Zotero.Collection[] = [];

        // First pass: map all collections
        collections.forEach((col) => {
          colMap.set(col.id, col);
          if (!childrenMap.has(col.id)) {
            childrenMap.set(col.id, []);
          }
        });

        // Second pass: organize into tree
        collections.forEach((col) => {
          if (col.parentID && colMap.has(col.parentID)) {
            if (!childrenMap.has(col.parentID)) {
              childrenMap.set(col.parentID, []);
            }
            childrenMap.get(col.parentID)?.push(col);
          } else {
            rootCols.push(col);
          }
        });

        // Sort by name
        const sortCols = (a: Zotero.Collection, b: Zotero.Collection) =>
          a.name.localeCompare(b.name);

        rootCols.sort(sortCols);
        childrenMap.forEach((children) => children.sort(sortCols));

        // Recursive function to render options
        const renderCollections = (
          cols: Zotero.Collection[],
          level: number,
        ) => {
          for (const col of cols) {
            const colOption = doc.createElement("option");
            colOption.value = `col_${col.id}`;
            // Add indentation based on level
            const prefix = "  ".repeat(level + 1);
            colOption.textContent = `${prefix}📁 ${col.name}`;

            if (currentTableConfig?.filterCollectionId === col.id) {
              colOption.selected = true;
            }
            select.appendChild(colOption);

            // Render children
            const children = childrenMap.get(col.id);
            if (children && children.length > 0) {
              renderCollections(children, level + 1);
            }
          }
        };

        // Start rendering from roots
        renderCollections(rootCols, 0);
      }
    } catch (e) {
      Zotero.debug(`[seerai] Error populating filter select: ${e}`);
    }
  }

  /**
   * Populate the save location dropdown for search imports
   */
  private static populateSaveLocationSelect(select: HTMLSelectElement): void {
    const doc = select.ownerDocument;
    if (!doc) return;

    try {
      const libraries = Zotero.Libraries.getAll();
      for (const library of libraries) {
        // Library option
        const libOption = doc.createElement("option");
        // Use 'user' for user library (matches default), 'lib_ID' for group libraries
        libOption.value =
          library.libraryID === Zotero.Libraries.userLibraryID
            ? "user"
            : `lib_${library.libraryID}`;
        libOption.textContent = `📚 ${library.name}`;
        if (currentSearchState.saveLocation === libOption.value) {
          libOption.selected = true;
        }
        select.appendChild(libOption);

        // Get collections for this library
        const collections = Zotero.Collections.getByLibrary(
          library.libraryID,
          true,
        );

        // Build hierarchy
        const colMap = new Map<number, Zotero.Collection>();
        const childrenMap = new Map<number, Zotero.Collection[]>();
        const rootCols: Zotero.Collection[] = [];

        // First pass: map all collections
        collections.forEach((col) => {
          colMap.set(col.id, col);
          if (!childrenMap.has(col.id)) {
            childrenMap.set(col.id, []);
          }
        });

        // Second pass: organize into tree
        collections.forEach((col) => {
          if (col.parentID && colMap.has(col.parentID)) {
            if (!childrenMap.has(col.parentID)) {
              childrenMap.set(col.parentID, []);
            }
            childrenMap.get(col.parentID)?.push(col);
          } else {
            rootCols.push(col);
          }
        });

        // Sort by name
        const sortCols = (a: Zotero.Collection, b: Zotero.Collection) =>
          a.name.localeCompare(b.name);

        rootCols.sort(sortCols);
        childrenMap.forEach((children) => children.sort(sortCols));

        // Recursive function to render options
        const renderCollections = (
          cols: Zotero.Collection[],
          level: number,
        ) => {
          for (const col of cols) {
            const colOption = doc.createElement("option");
            colOption.value = `col_${col.id}`;
            // Add indentation based on level
            const prefix = "  ".repeat(level + 1); // +1 for initial indent under library
            colOption.textContent = `${prefix}📁 ${col.name}`;

            if (currentSearchState.saveLocation === colOption.value) {
              colOption.selected = true;
            }
            select.appendChild(colOption);

            // Render children
            const children = childrenMap.get(col.id);
            if (children && children.length > 0) {
              renderCollections(children, level + 1);
            }
          }
        };

        // Start rendering from roots
        renderCollections(rootCols, 0);
      }
    } catch (e) {
      Zotero.debug(`[seerai] Error populating save location select: ${e}`);
    }
  }

  /**
   * Show a popup to select a folder to add selected items to
   */
  private static async showAddToFolderPopup(
    doc: Document,
    anchorBtn: HTMLElement,
  ): Promise<void> {
    // Remove existing popup if any
    const existing = doc.getElementById("add-to-folder-popup");
    if (existing) {
      existing.remove();
      return;
    }

    if (!currentTableData || currentTableData.selectedRowIds.size === 0) return;

    // Create popup
    const popup = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "add-to-folder-popup" },
      styles: {
        position: "fixed",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
        padding: "12px",
        zIndex: "10000",
        minWidth: "200px",
        maxHeight: "300px",
        overflow: "auto",
      },
    });

    // Position popup near button
    const rect = anchorBtn.getBoundingClientRect();
    popup.style.top = `${rect.top}px`;
    popup.style.left = `${rect.left - 210}px`;

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        fontSize: "12px",
        fontWeight: "600",
        marginBottom: "8px",
        color: "var(--text-primary)",
      },
    });
    header.textContent = `Add ${currentTableData.selectedRowIds.size} item(s) to:`;
    popup.appendChild(header);

    // Collection select dropdown
    const select = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        width: "100%",
        padding: "8px",
        borderRadius: "4px",
        border: "1px solid var(--border-primary)",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
        fontSize: "12px",
        marginBottom: "8px",
      },
    }) as HTMLSelectElement;

    // Populate with collections
    try {
      const libraries = Zotero.Libraries.getAll();
      for (const library of libraries) {
        // Get collections for this library
        const collections = Zotero.Collections.getByLibrary(
          library.libraryID,
          true,
        );

        // Build hierarchy
        const colMap = new Map<number, Zotero.Collection>();
        const childrenMap = new Map<number, Zotero.Collection[]>();
        const rootCols: Zotero.Collection[] = [];

        collections.forEach((col) => {
          colMap.set(col.id, col);
          if (!childrenMap.has(col.id)) {
            childrenMap.set(col.id, []);
          }
        });

        collections.forEach((col) => {
          if (col.parentID && colMap.has(col.parentID)) {
            if (!childrenMap.has(col.parentID)) {
              childrenMap.set(col.parentID, []);
            }
            childrenMap.get(col.parentID)?.push(col);
          } else {
            rootCols.push(col);
          }
        });

        const sortCols = (a: Zotero.Collection, b: Zotero.Collection) =>
          a.name.localeCompare(b.name);
        rootCols.sort(sortCols);
        childrenMap.forEach((children) => children.sort(sortCols));

        // Library header (as disabled option)
        const libHeader = doc.createElement("option");
        libHeader.value = "";
        libHeader.textContent = `📚 ${library.name}`;
        libHeader.disabled = true;
        libHeader.style.fontWeight = "bold";
        select.appendChild(libHeader);

        const renderCollections = (
          cols: Zotero.Collection[],
          level: number,
        ) => {
          for (const col of cols) {
            const colOption = doc.createElement("option");
            colOption.value = `${col.id}`;
            const prefix = "  ".repeat(level + 1);
            colOption.textContent = `${prefix}📁 ${col.name}`;
            select.appendChild(colOption);

            const children = childrenMap.get(col.id);
            if (children && children.length > 0) {
              renderCollections(children, level + 1);
            }
          }
        };

        renderCollections(rootCols, 0);
      }
    } catch (e) {
      Zotero.debug(`[seerai] Error populating folder select: ${e}`);
    }

    popup.appendChild(select);

    // Buttons row
    const btnRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "8px",
        justifyContent: "flex-end",
      },
    });

    // Cancel button
    const cancelBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Cancel" },
      styles: {
        padding: "6px 12px",
        borderRadius: "4px",
        border: "1px solid var(--border-primary)",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
        fontSize: "12px",
      },
      listeners: [{ type: "click", listener: () => popup.remove() }],
    });
    btnRow.appendChild(cancelBtn);

    // Add button
    const addBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Add" },
      styles: {
        padding: "6px 12px",
        borderRadius: "4px",
        border: "none",
        backgroundColor: "var(--highlight-primary)",
        color: "var(--highlight-text)",
        cursor: "pointer",
        fontSize: "12px",
        fontWeight: "500",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const collectionId = parseInt(select.value, 10);
            if (!collectionId) {
              popup.remove();
              return;
            }

            const selectedIds = Array.from(currentTableData!.selectedRowIds);
            let added = 0;

            for (const itemId of selectedIds) {
              try {
                const zItem = Zotero.Items.get(itemId);
                if (zItem && zItem.isRegularItem()) {
                  zItem.addToCollection(collectionId);
                  await zItem.saveTx();
                  added++;
                }
              } catch (err) {
                Zotero.debug(
                  `[seerai] Error adding item ${itemId} to collection: ${err}`,
                );
              }
            }

            // Update button to show success
            addBtn.innerText = `✓ Added ${added}`;
            addBtn.style.backgroundColor = "#4CAF50";
            setTimeout(() => popup.remove(), 1000);
          },
        },
      ],
    });
    btnRow.appendChild(addBtn);
    popup.appendChild(btnRow);

    // Append popup to document
    if (doc.body) {
      doc.body.appendChild(popup);
    } else {
      doc.documentElement?.appendChild(popup);
    }
    Zotero.debug(`[seerai] Add to folder popup appended`);

    // Track if select is being interacted with
    let selectActive = false;
    select.addEventListener("mousedown", () => {
      selectActive = true;
    });
    select.addEventListener("change", () => {
      selectActive = false;
    });
    select.addEventListener("blur", () => {
      selectActive = false;
    });

    // Close on click outside - but not when select dropdown is active
    const closeHandler = (e: Event) => {
      // If select dropdown is being used, don't close
      if (selectActive) {
        return;
      }
      const target = e.target as Node;
      // Don't close if clicking inside popup
      if (popup.contains(target)) {
        return;
      }
      popup.remove();
      doc.removeEventListener("mousedown", closeHandler);
    };
    setTimeout(() => doc.addEventListener("mousedown", closeHandler), 100);
  }

  /**
   * Show paper picker as a beautiful inline dropdown panel
   */
  private static async showTablePaperPicker(
    doc: Document,
    item: Zotero.Item,
  ): Promise<void> {
    // Toggle existing dropdown
    const existing = doc.getElementById(
      "table-paper-picker-dropdown",
    ) as HTMLElement | null;
    if (existing) {
      // Animate out
      existing.style.opacity = "0";
      existing.style.transform = "translateY(-10px)";
      setTimeout(() => existing.remove(), 200);
      return;
    }

    // Find the toolbar and table container to position dropdown
    const tabContent = doc.getElementById("tab-content");
    if (!tabContent) return;

    // Create dropdown panel
    const dropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "table-paper-picker-dropdown" },
      styles: {
        backgroundColor: "var(--background-primary)",
        borderRadius: "8px",
        padding: "0",
        boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
        overflow: "hidden",
        border: "1px solid var(--border-primary)",
        transition: "all 0.2s ease-out",
        opacity: "0",
        transform: "translateY(-10px)",
        margin: "8px",
      },
    });

    // Header with gradient
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        background:
          "linear-gradient(135deg, var(--highlight-primary) 0%, color-mix(in srgb, var(--highlight-primary) 80%, purple) 100%)",
        padding: "12px 16px",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        gap: "12px",
      },
    });

    const headerTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "📚 Add Papers" },
      styles: {
        fontSize: "14px",
        fontWeight: "600",
        color: "var(--highlight-text)",
        textShadow: "0 1px 2px rgba(0,0,0,0.1)",
      },
    });
    header.appendChild(headerTitle);

    // Close button in header
    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "rgba(0,0,0,0.1)",
        border: "none",
        borderRadius: "50%",
        width: "24px",
        height: "24px",
        cursor: "pointer",
        color: "var(--highlight-text)",
        fontSize: "12px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        transition: "background 0.2s",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            dropdown.style.opacity = "0";
            dropdown.style.transform = "translateY(-10px)";
            setTimeout(() => {
              dropdown.remove();
              if (currentContainer && currentItem) {
                this.renderInterface(currentContainer, currentItem);
              }
            }, 200);
          },
        },
      ],
    });
    closeBtn.addEventListener("mouseenter", () => {
      closeBtn.style.background = "rgba(0,0,0,0.15)";
    });
    closeBtn.addEventListener("mouseleave", () => {
      closeBtn.style.background = "rgba(0,0,0,0.1)";
    });
    header.appendChild(closeBtn);
    dropdown.appendChild(header);

    // Content area
    const content = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "12px 16px",
        display: "flex",
        flexDirection: "column",
        gap: "10px",
      },
    });

    // Filter and search row
    const controlsRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "8px",
        alignItems: "center",
      },
    });

    const filterSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        flex: "0 0 auto",
        minWidth: "140px",
        padding: "8px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
        outline: "none",
        transition: "border-color 0.2s, box-shadow 0.2s",
      },
    }) as HTMLSelectElement;
    filterSelect.addEventListener("focus", () => {
      filterSelect.style.borderColor = "var(--highlight-primary)";
      filterSelect.style.boxShadow =
        "0 0 0 3px color-mix(in srgb, var(--highlight-primary) 20%, transparent)";
    });
    filterSelect.addEventListener("blur", () => {
      filterSelect.style.borderColor = "var(--border-primary)";
      filterSelect.style.boxShadow = "none";
    });
    this.populateFilterSelect(filterSelect);
    controlsRow.appendChild(filterSelect);

    const searchInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "text", placeholder: "🔍 Search papers..." },
      styles: {
        flex: "1",
        padding: "8px 12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        outline: "none",
        transition: "border-color 0.2s, box-shadow 0.2s",
      },
    }) as HTMLInputElement;
    searchInput.addEventListener("focus", () => {
      searchInput.style.borderColor = "var(--highlight-primary)";
      searchInput.style.boxShadow =
        "0 0 0 3px color-mix(in srgb, var(--highlight-primary) 20%, transparent)";
    });
    searchInput.addEventListener("blur", () => {
      searchInput.style.borderColor = "var(--border-primary)";
      searchInput.style.boxShadow = "none";
    });
    controlsRow.appendChild(searchInput);
    content.appendChild(controlsRow);

    // Paper list container with custom scrollbar styling
    const listContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        maxHeight: "280px",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        backgroundColor: "var(--background-secondary)",
      },
    });
    content.appendChild(listContainer);

    // State for infinite scroll and Add All
    let allFilteredItems: Zotero.Item[] = [];
    let displayedCount = 0;
    const BATCH_SIZE = 50;
    let isLoadingMore = false;

    // Render papers with beautiful styling
    const renderPaperBatch = (
      items: Zotero.Item[],
      startIndex: number,
      count: number,
    ) => {
      const endIndex = Math.min(startIndex + count, items.length);
      for (let i = startIndex; i < endIndex; i++) {
        const paperItem = items[i];
        const paperTitle =
          (paperItem.getField("title") as string) || "Untitled";
        const creators = paperItem.getCreators();
        const authorStr =
          creators.length > 0
            ? creators.map((c) => c.lastName).join(", ")
            : "Unknown";
        const year = paperItem.getField("year") || "";

        const row = ztoolkit.UI.createElement(doc, "div", {
          attributes: { "data-paper-id": String(paperItem.id) },
          styles: {
            padding: "10px 14px",
            borderBottom: "1px solid var(--border-primary)",
            cursor: "pointer",
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            transition: "background-color 0.15s, transform 0.1s",
          },
        });

        const info = ztoolkit.UI.createElement(doc, "div", {
          styles: { flex: "1", overflow: "hidden", marginRight: "10px" },
        });

        // Clickable title to open PDF
        const titleEl = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: paperTitle },
          styles: {
            fontSize: "12px",
            fontWeight: "500",
            overflow: "hidden",
            lineHeight: "1.3",
            maxHeight: "2.6em",
            color: "var(--highlight-primary)",
            cursor: "pointer",
            transition: "color 0.15s",
            wordBreak: "break-word",
          },
          listeners: [
            {
              type: "click",
              listener: async (e: Event) => {
                e.stopPropagation();
                const attachmentIds = paperItem.getAttachments();
                for (const attachId of attachmentIds) {
                  const attachment = Zotero.Items.get(attachId);
                  if (
                    attachment &&
                    attachment.isPDFAttachment &&
                    attachment.isPDFAttachment()
                  ) {
                    await Zotero.Reader.open(attachment.id);
                    return;
                  }
                }
                const zp = Zotero.getActiveZoteroPane();
                if (zp) zp.selectItem(paperItem.id);
              },
            },
          ],
        });
        titleEl.addEventListener("mouseenter", () => {
          (titleEl as HTMLElement).style.textDecoration = "underline";
        });
        titleEl.addEventListener("mouseleave", () => {
          (titleEl as HTMLElement).style.textDecoration = "none";
        });

        const metaEl = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: `${authorStr}${year ? ` • ${year}` : ""}` },
          styles: {
            fontSize: "11px",
            color: "var(--text-secondary)",
            marginTop: "2px",
          },
        });
        info.appendChild(titleEl);
        info.appendChild(metaEl);
        row.appendChild(info);

        // Add button with animation
        const addBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "+" },
          styles: {
            width: "28px",
            height: "28px",
            borderRadius: "50%",
            border: "2px solid var(--highlight-primary)",
            backgroundColor: "transparent",
            color: "var(--highlight-primary)",
            fontSize: "16px",
            fontWeight: "bold",
            cursor: "pointer",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            transition: "all 0.2s ease",
            flexShrink: "0",
          },
          listeners: [
            {
              type: "click",
              listener: async (e: Event) => {
                e.stopPropagation();
                if (currentTableConfig) {
                  if (!currentTableConfig.addedPaperIds) {
                    currentTableConfig.addedPaperIds = [];
                  }
                  if (
                    !currentTableConfig.addedPaperIds.includes(paperItem.id)
                  ) {
                    currentTableConfig.addedPaperIds.push(paperItem.id);
                    const tableStore = getTableStore();
                    await tableStore.saveConfig(currentTableConfig);

                    // Update table view immediately
                    const tableWrapper = doc.querySelector(".table-wrapper");
                    if (tableWrapper) {
                      const newData = await this.loadTableData();
                      const newTable = this.createPapersTable(doc, newData);
                      tableWrapper.innerHTML = "";
                      tableWrapper.appendChild(newTable);
                    }

                    // Animate removal
                    row.style.transform = "translateX(20px)";
                    row.style.opacity = "0";
                    setTimeout(() => {
                      row.remove();
                      const idx = allFilteredItems.findIndex(
                        (item) => item.id === paperItem.id,
                      );
                      if (idx !== -1) {
                        allFilteredItems.splice(idx, 1);
                        displayedCount--;
                      }
                    }, 150);
                  }
                }
              },
            },
          ],
        });
        addBtn.addEventListener("mouseenter", () => {
          addBtn.style.backgroundColor = "var(--highlight-primary)";
          addBtn.style.color = "var(--highlight-text)";
          addBtn.style.transform = "scale(1.1)";
        });
        addBtn.addEventListener("mouseleave", () => {
          addBtn.style.backgroundColor = "transparent";
          addBtn.style.color = "var(--highlight-primary)";
          addBtn.style.transform = "scale(1)";
        });
        row.appendChild(addBtn);

        // Row hover effect
        row.addEventListener("mouseenter", () => {
          row.style.backgroundColor = "var(--background-primary)";
        });
        row.addEventListener("mouseleave", () => {
          row.style.backgroundColor = "";
        });

        listContainer.appendChild(row);
      }
      displayedCount = endIndex;
    };

    // Infinite scroll
    const loadMorePapers = () => {
      if (isLoadingMore || displayedCount >= allFilteredItems.length) return;
      isLoadingMore = true;
      renderPaperBatch(allFilteredItems, displayedCount, BATCH_SIZE);
      isLoadingMore = false;
    };

    listContainer.addEventListener("scroll", () => {
      const scrollTop = listContainer.scrollTop;
      const scrollHeight = listContainer.scrollHeight;
      const clientHeight = listContainer.clientHeight;
      if (scrollHeight - scrollTop - clientHeight < 50) {
        loadMorePapers();
      }
    });

    // Load papers
    const loadPapers = async () => {
      listContainer.innerHTML = "";
      allFilteredItems = [];
      displayedCount = 0;
      const filterValue = filterSelect.value;
      const searchQuery = searchInput.value.toLowerCase();

      // Show loading
      const loadingEl = ztoolkit.UI.createElement(doc, "div", {
        properties: { innerText: "⏳ Loading papers..." },
        styles: {
          padding: "20px",
          textAlign: "center",
          color: "var(--text-secondary)",
        },
      });
      listContainer.appendChild(loadingEl);

      let items: Zotero.Item[] = [];
      try {
        if (filterValue === "all") {
          const libraries = Zotero.Libraries.getAll();
          for (const lib of libraries) {
            const libItems = await Zotero.Items.getAll(lib.libraryID);
            items.push(
              ...libItems.filter((i: Zotero.Item) => i.isRegularItem()),
            );
          }
        } else if (filterValue.startsWith("lib_")) {
          const libraryId = parseInt(filterValue.replace("lib_", ""), 10);
          const libItems = await Zotero.Items.getAll(libraryId);
          items = libItems.filter((i: Zotero.Item) => i.isRegularItem());
        } else if (filterValue.startsWith("col_")) {
          const collectionId = parseInt(filterValue.replace("col_", ""), 10);
          const collection = Zotero.Collections.get(collectionId);
          if (collection) {
            items = collection
              .getChildItems()
              .filter((i: Zotero.Item) => i.isRegularItem());
          }
        }

        const addedIds = new Set(currentTableConfig?.addedPaperIds || []);
        allFilteredItems = items.filter((i) => {
          if (addedIds.has(i.id)) return false;
          if (!searchQuery) return true;
          const itemTitle = (
            (i.getField("title") as string) || ""
          ).toLowerCase();
          const creators = i
            .getCreators()
            .map((c) => `${c.firstName} ${c.lastName}`.toLowerCase())
            .join(" ");
          return (
            itemTitle.includes(searchQuery) || creators.includes(searchQuery)
          );
        });

        listContainer.innerHTML = "";

        if (allFilteredItems.length === 0) {
          const emptyMsg = ztoolkit.UI.createElement(doc, "div", {
            properties: { innerText: "No papers found" },
            styles: {
              padding: "30px",
              textAlign: "center",
              color: "var(--text-tertiary)",
              fontSize: "13px",
            },
          });
          listContainer.appendChild(emptyMsg);
          return;
        }

        renderPaperBatch(allFilteredItems, 0, BATCH_SIZE);
      } catch (e) {
        listContainer.innerHTML = "";
        const errorMsg = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: `Error loading papers: ${e}` },
          styles: { padding: "20px", textAlign: "center", color: "#c62828" },
        });
        listContainer.appendChild(errorMsg);
      }
    };

    filterSelect.addEventListener("change", loadPapers);
    searchInput.addEventListener("input", () => {
      clearTimeout((searchInput as any)._debounce);
      (searchInput as any)._debounce = setTimeout(loadPapers, 300);
    });

    // Button row with gradient background
    const buttonRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        gap: "10px",
        paddingTop: "8px",
        borderTop: "1px solid var(--border-primary)",
      },
    });

    // Add All button
    const addAllBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "➕ Add All" },
      styles: {
        padding: "10px 18px",
        border: "none",
        borderRadius: "8px",
        background:
          "linear-gradient(135deg, var(--highlight-primary) 0%, color-mix(in srgb, var(--highlight-primary) 80%, purple) 100%)",
        color: "var(--highlight-text)",
        cursor: "pointer",
        fontWeight: "600",
        fontSize: "12px",
        transition: "transform 0.15s, box-shadow 0.15s",
        boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            let count = 0;
            for (const paperItem of allFilteredItems) {
              if (
                currentTableConfig &&
                !currentTableConfig.addedPaperIds.includes(paperItem.id)
              ) {
                currentTableConfig.addedPaperIds.push(paperItem.id);
                count++;
              }
            }
            // Save the config to persist changes
            if (count > 0 && currentTableConfig) {
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
            }
            listContainer.innerHTML = "";
            allFilteredItems = [];
            displayedCount = 0;
            const emptyMsg = ztoolkit.UI.createElement(doc, "div", {
              properties: { innerText: "All papers added!" },
              styles: {
                padding: "30px",
                textAlign: "center",
                color: "var(--text-secondary)",
                fontSize: "13px",
              },
            });
            listContainer.appendChild(emptyMsg);
            if (count > 0) {
              (addAllBtn as HTMLElement).innerText = `✓ Added ${count}`;
              setTimeout(() => {
                (addAllBtn as HTMLElement).innerText = "➕ Add All";
              }, 1500);
            }
          },
        },
      ],
    });
    addAllBtn.addEventListener("mouseenter", () => {
      addAllBtn.style.transform = "translateY(-2px)";
      addAllBtn.style.boxShadow = "0 4px 12px rgba(0,0,0,0.2)";
    });
    addAllBtn.addEventListener("mouseleave", () => {
      addAllBtn.style.transform = "";
      addAllBtn.style.boxShadow = "0 2px 8px rgba(0,0,0,0.15)";
    });
    buttonRow.appendChild(addAllBtn);

    // Done button
    const doneBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Done" },
      styles: {
        padding: "10px 18px",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
        fontWeight: "500",
        fontSize: "12px",
        transition: "background-color 0.15s",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            dropdown.style.opacity = "0";
            dropdown.style.transform = "translateY(-10px)";
            setTimeout(() => {
              dropdown.remove();
              if (currentContainer && currentItem) {
                this.renderInterface(currentContainer, currentItem);
              }
            }, 200);
          },
        },
      ],
    });
    doneBtn.addEventListener("mouseenter", () => {
      doneBtn.style.backgroundColor = "var(--background-primary)";
    });
    doneBtn.addEventListener("mouseleave", () => {
      doneBtn.style.backgroundColor = "var(--background-secondary)";
    });
    buttonRow.appendChild(doneBtn);
    content.appendChild(buttonRow);

    dropdown.appendChild(content);

    // Insert dropdown after the toolbar
    const toolbar = tabContent.querySelector(".table-toolbar");
    if (toolbar && toolbar.parentNode) {
      toolbar.parentNode.insertBefore(dropdown, toolbar.nextSibling);
    } else {
      tabContent.insertBefore(dropdown, tabContent.firstChild);
    }

    // Auto-focus search
    setTimeout(() => searchInput.focus(), 100);

    // Animate in
    setTimeout(() => {
      dropdown.style.opacity = "1";
      dropdown.style.transform = "translateY(0)";
    }, 10);

    // Load papers
    await loadPapers();
  }

  /**
   * Generate AI content for all empty computed columns
   */
  private static async generateAllEmptyColumns(
    doc: Document,
    item: Zotero.Item,
  ): Promise<void> {
    Zotero.debug("[seerai] Generate All clicked");

    if (!currentTableConfig) return;

    isTableGenerating = true;

    // Get visible columns and computed columns
    const columns = currentTableConfig.columns || defaultColumns;
    const visibleCols = columns.filter((col) => col.visible);
    const computedCols = visibleCols.filter((col) => col.type === "computed");

    if (computedCols.length === 0) {
      Zotero.debug("[seerai] No computed columns visible");
      return;
    }

    // Find all visible rows
    const table = doc.querySelector(".papers-table");
    if (!table) return;

    const rows = table.querySelectorAll("tr[data-paper-id]");
    if (rows.length === 0) return;

    // Get max concurrent from settings
    const maxConcurrent =
      (Zotero.Prefs.get(
        `${addon.data.config.prefsPrefix}.aiMaxConcurrent`,
      ) as number) || 5;
    Zotero.debug(`[seerai] AI Max concurrent queries: ${maxConcurrent}`);

    // Build tasks by scanning DOM
    interface GenerationTask {
      paperId: number;
      col: TableColumn;
      td: HTMLElement;
      item: Zotero.Item;
    }
    const tasks: GenerationTask[] = [];

    for (let i = 0; i < rows.length; i++) {
      const tr = rows[i] as HTMLElement;
      const paperId = parseInt(tr.getAttribute("data-paper-id") || "0", 10);
      if (!paperId) continue;

      // Get paper item
      const paperItem = Zotero.Items.get(paperId);
      if (!paperItem || !paperItem.isRegularItem()) continue;

      const existingRowData = currentTableData?.rows.find(
        (r) => r.paperId === paperId,
      );

      // Filter by selection if active
      if (
        currentTableData?.selectedRowIds.size &&
        !currentTableData.selectedRowIds.has(paperId)
      ) {
        continue;
      }

      // Check each computed column
      for (const col of computedCols) {
        // Check if cell is empty in DATA
        const val = existingRowData?.data[col.id];
        // If val is undefined or null, it's empty. If it's a string, trim it.
        // Note: For now we only fill empty cells to avoid accidental overwrite.
        if (val && val.toString().trim().length > 0) {
          // Zotero.debug(`[seerai] Skipping ${paperId}/${col.id} - content exists`);
          continue;
        }

        // Calculate correct DOM index
        // In createPapersTable:
        // Index 0: Paper (Title + Author + Year + Sources + ZoteroKey + DOI) - Combined
        // Index 1..N: Other Columns (excluding core columns)
        // Index N+1: Actions

        // Find index of this column in "otherColumns"
        const coreColumnIds = [
          "title",
          "author",
          "year",
          "sources",
          "zoteroKey",
          "DOI",
        ];
        const visibleOtherColumns = visibleCols.filter(
          (c) => !coreColumnIds.includes(c.id),
        );
        const otherColIndex = visibleOtherColumns.findIndex(
          (c) => c.id === col.id,
        );

        if (otherColIndex === -1) continue;

        // Map to DOM index (1-based because 0 is "Paper")
        const domIndex = otherColIndex + 1;

        // Target the specific TD
        if (domIndex >= tr.children.length) continue;
        const td = tr.children[domIndex] as HTMLElement;
        if (!td) continue;

        // Add to tasks
        tasks.push({
          paperId,
          col,
          td,
          item: paperItem,
        });

        // Immediate visual feedback
        td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">⏳ Generating...</span>`;
        td.style.cursor = "wait";
      }
    }

    if (tasks.length === 0) {
      Zotero.debug("[seerai] No empty cells to generate in visible rows");
      return;
    }

    Zotero.debug(`[seerai] ${tasks.length} visible cells to generate`);

    // Update button status
    const generateBtn = doc.getElementById(
      "generate-all-btn",
    ) as HTMLButtonElement | null;
    const originalBtnText = generateBtn?.innerText || "⚡ Generate All";
    let completed = 0;
    let generated = 0;
    let failed = 0;

    const updateProgress = () => {
      if (generateBtn) {
        generateBtn.innerText = `⏳ ${completed}/${tasks.length}`;
        generateBtn.disabled = true;
        generateBtn.style.cursor = "wait";
      }
    };

    let saveCounter = 0;
    const SAVE_INTERVAL = 5;

    const saveIncremental = async () => {
      saveCounter++;
      if (saveCounter % SAVE_INTERVAL === 0 && currentTableConfig) {
        const tableStore = getTableStore();
        await tableStore.saveConfig(currentTableConfig);
        Zotero.debug(`[seerai] Incremental save at ${saveCounter} completions`);
      }
    };

    // Process tasks using runConcurrentTasks
    updateProgress();
    await runConcurrentTasks<GenerationTask, string | null>({
      tasks,
      concurrency: maxConcurrent,
      maxRetries: 3,
      executor: async (task) => {
        const noteIds = task.item.getNotes();
        let content = "";

        if (noteIds.length > 0) {
          content = await this.generateColumnContent(
            task.item,
            task.col,
            noteIds,
          );
        } else {
          content = await this.generateFromPDF(task.item, task.col);
        }
        return content;
      },
      onTaskComplete: (task, content) => {
        completed++;
        if (content) {
          task.td.innerHTML = parseMarkdown(content);
          task.td.style.cursor = "pointer";
          task.td.style.backgroundColor = "";

          const row = currentTableData?.rows.find(
            (r) => r.paperId === task.paperId,
          );
          if (row) {
            row.data[task.col.id] = content;
          }

          if (currentTableConfig) {
            if (!currentTableConfig.generatedData) {
              currentTableConfig.generatedData = {};
            }
            if (!currentTableConfig.generatedData[task.paperId]) {
              currentTableConfig.generatedData[task.paperId] = {};
            }
            currentTableConfig.generatedData[task.paperId][task.col.id] =
              content;
          }

          generated++;
          saveIncremental();
        } else {
          task.td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px; font-style: italic;">Empty - no notes</span>`;
          task.td.title =
            "No notes found. Use '🔍 Extract with ocr' to create notes first.";
          task.td.style.cursor = "default";
        }
        updateProgress();
      },
      onTaskError: (task, error, index, willRetry) => {
        if (!willRetry) {
          completed++;
          failed++;
          task.td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Error</span>`;
          task.td.title = String(error);
          updateProgress();
        }
        Zotero.debug(
          `[seerai] Error generating for ${task.paperId}/${task.col.id}: ${error}${willRetry ? " (will retry)" : ""}`,
        );
      },
    });

    // Save at the end
    const tableStore = getTableStore();
    if (currentTableConfig) {
      await tableStore.saveConfig(currentTableConfig);
    }

    // Restore button
    if (generateBtn) {
      generateBtn.innerText = `✓ Done (${generated}/${tasks.length})`;
      generateBtn.disabled = false;
      generateBtn.style.cursor = "pointer";
      setTimeout(() => {
        generateBtn.innerText = originalBtnText;
      }, 2000);
    }

    isTableGenerating = false;

    if (generated > 0 || tasks.length > 0) {
      this.playNotificationSound();
    }

    Zotero.debug(
      `[seerai] Generation complete: ${generated} generated, ${failed} failed`,
    );
  }

  /**
   * Regenerate columns for selected items that already have generated content.
   * Unlike generateAllEmptyColumns, this specifically targets cells with existing content.
   */
  private static async regenerateSelectedColumns(
    doc: Document,
    item: Zotero.Item,
  ): Promise<void> {
    Zotero.debug("[seerai] Regenerate Selected clicked");

    if (!currentTableConfig) return;
    if (!currentTableData || currentTableData.selectedRowIds.size === 0) return;

    isTableGenerating = true;

    // Get visible columns and computed columns
    const columns = currentTableConfig.columns || defaultColumns;
    const visibleCols = columns.filter((col) => col.visible);
    const computedCols = visibleCols.filter((col) => col.type === "computed");

    if (computedCols.length === 0) {
      Zotero.debug("[seerai] No computed columns visible");
      return;
    }

    // Find all visible rows
    const table = doc.querySelector(".papers-table");
    if (!table) return;

    const rows = table.querySelectorAll("tr[data-paper-id]");
    if (rows.length === 0) return;

    // Get max concurrent from settings
    const maxConcurrent =
      (Zotero.Prefs.get(
        `${addon.data.config.prefsPrefix}.aiMaxConcurrent`,
      ) as number) || 5;

    // Build tasks by scanning DOM
    interface RegenerationTask {
      paperId: number;
      col: TableColumn;
      td: HTMLElement;
      item: Zotero.Item;
    }
    const tasks: RegenerationTask[] = [];

    for (let i = 0; i < rows.length; i++) {
      const tr = rows[i] as HTMLElement;
      const paperId = parseInt(tr.getAttribute("data-paper-id") || "0", 10);
      if (!paperId) continue;

      // Only process selected rows
      if (!currentTableData.selectedRowIds.has(paperId)) {
        continue;
      }

      // Get paper item
      const paperItem = Zotero.Items.get(paperId);
      if (!paperItem || !paperItem.isRegularItem()) continue;

      const existingRowData = currentTableData?.rows.find(
        (r) => r.paperId === paperId,
      );

      // Check each computed column
      for (const col of computedCols) {
        // Check if cell has existing content - ONLY regenerate cells with content
        const val = existingRowData?.data[col.id];
        if (!val || val.toString().trim().length === 0) {
          // Skip empty cells - we only want to regenerate existing content
          continue;
        }

        // Calculate correct DOM index
        const coreColumnIds = [
          "title",
          "author",
          "year",
          "sources",
          "zoteroKey",
          "DOI",
        ];
        const visibleOtherColumns = visibleCols.filter(
          (c) => !coreColumnIds.includes(c.id),
        );
        const otherColIndex = visibleOtherColumns.findIndex(
          (c) => c.id === col.id,
        );

        if (otherColIndex === -1) continue;

        const domIndex = otherColIndex + 1;
        if (domIndex >= tr.children.length) continue;
        const td = tr.children[domIndex] as HTMLElement;
        if (!td) continue;

        tasks.push({
          paperId,
          col,
          td,
          item: paperItem,
        });

        // Immediate visual feedback
        td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">🔄 Regenerating...</span>`;
        td.style.cursor = "wait";
      }
    }

    if (tasks.length === 0) {
      Zotero.debug("[seerai] No cells with existing content to regenerate");
      isTableGenerating = false;
      return;
    }

    Zotero.debug(`[seerai] ${tasks.length} cells to regenerate`);

    let completed = 0;
    let regenerated = 0;
    let failed = 0;
    let saveCounter = 0;
    const SAVE_INTERVAL = 5;

    const saveIncremental = async () => {
      saveCounter++;
      if (saveCounter % SAVE_INTERVAL === 0 && currentTableConfig) {
        const tableStore = getTableStore();
        await tableStore.saveConfig(currentTableConfig);
        Zotero.debug(`[seerai] Incremental save at ${saveCounter} completions`);
      }
    };

    // Process tasks using runConcurrentTasks
    await runConcurrentTasks<RegenerationTask, string | null>({
      tasks,
      concurrency: maxConcurrent,
      maxRetries: 3,
      executor: async (task) => {
        const noteIds = task.item.getNotes();
        let content = "";

        if (noteIds.length > 0) {
          content = await this.generateColumnContent(
            task.item,
            task.col,
            noteIds,
          );
        } else {
          content = await this.generateFromPDF(task.item, task.col);
        }
        return content;
      },
      onTaskComplete: (task, content) => {
        completed++;
        if (content) {
          task.td.innerHTML = parseMarkdown(content);
          task.td.style.cursor = "pointer";
          task.td.style.backgroundColor = "";

          const row = currentTableData?.rows.find(
            (r) => r.paperId === task.paperId,
          );
          if (row) {
            row.data[task.col.id] = content;
          }

          if (currentTableConfig) {
            if (!currentTableConfig.generatedData) {
              currentTableConfig.generatedData = {};
            }
            if (!currentTableConfig.generatedData[task.paperId]) {
              currentTableConfig.generatedData[task.paperId] = {};
            }
            currentTableConfig.generatedData[task.paperId][task.col.id] =
              content;
          }

          regenerated++;
          saveIncremental();
        } else {
          task.td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px; font-style: italic;">Failed to regenerate</span>`;
          task.td.style.cursor = "default";
        }
      },
      onTaskError: (task, error, index, willRetry) => {
        if (!willRetry) {
          completed++;
          failed++;
          task.td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Error</span>`;
          task.td.title = String(error);
        }
        Zotero.debug(
          `[seerai] Error regenerating for ${task.paperId}/${task.col.id}: ${error}${willRetry ? " (will retry)" : ""}`,
        );
      },
    });

    // Save at the end
    const tableStore = getTableStore();
    if (currentTableConfig) {
      await tableStore.saveConfig(currentTableConfig);
    }

    isTableGenerating = false;

    if (regenerated > 0 || tasks.length > 0) {
      this.playNotificationSound();
    }

    Zotero.debug(
      `[seerai] Regeneration complete: ${regenerated} regenerated, ${failed} failed`,
    );
  }

  /**
   * Extract text from all visible PDFs that don't have notes
   */
  private static async extractAllEmptyPDFs(
    doc: Document,
    item: Zotero.Item,
  ): Promise<void> {
    Zotero.debug("[seerai] Extract All clicked");

    // Find all visible rows
    const table = doc.querySelector(".papers-table");
    if (!table) return;

    const rows = table.querySelectorAll("tr[data-paper-id]");
    if (rows.length === 0) return;

    // Get max concurrent from settings (OCR-specific setting)
    const maxConcurrent =
      (Zotero.Prefs.get(
        `${addon.data.config.prefsPrefix}.datalabMaxConcurrent`,
      ) as number) || 5;
    Zotero.debug(`[seerai] OCR Max concurrent: ${maxConcurrent}`);

    // Build list of extraction tasks
    interface ExtractionTask {
      paperId: number;
      pdf: Zotero.Item;
      tds: HTMLElement[]; // Any "Click to process PDF" cells to update
      item: Zotero.Item;
    }
    const tasks: ExtractionTask[] = [];

    // Helper to check for existing notes
    const hasExistingNote = (parent: Zotero.Item): boolean => {
      return ocrService.hasExistingNote(parent);
    };

    for (let i = 0; i < rows.length; i++) {
      const tr = rows[i] as HTMLElement;
      const paperId = parseInt(tr.getAttribute("data-paper-id") || "0", 10);
      if (!paperId) continue;

      // Get paper item
      const paperItem = Zotero.Items.get(paperId);
      if (!paperItem || !paperItem.isRegularItem()) continue;

      // 1. Check if has PDF
      const pdf = ocrService.getFirstPdfAttachment(paperItem);
      if (!pdf) continue;

      // 2. Check if already has notes matching title
      if (hasExistingNote(paperItem)) continue;

      // Find cells that might show "Click to process PDF" status
      // These would be computed cells that are empty
      const tds: HTMLElement[] = [];
      if (currentTableConfig && currentTableConfig.columns) {
        currentTableConfig.columns.forEach((col, idx) => {
          if (col.type === "computed" && col.visible) {
            const cellVal =
              currentTableData?.rows.find((r) => r.paperId === paperId)?.data[
                col.id
              ] || "";
            if (!cellVal.trim()) {
              // This cell is empty, so it might show the "OCR" prompt
              // Actual index in DOM depends on visible columns
              const visibleIdx = currentTableConfig!.columns
                .filter((c) => c.visible)
                .findIndex((c) => c.id === col.id);
              if (visibleIdx !== -1 && tr.children[visibleIdx]) {
                tds.push(tr.children[visibleIdx] as HTMLElement);
              }
            }
          }
        });
      }

      tasks.push({
        paperId,
        pdf,
        tds,
        item: paperItem,
      });

      // Immediate feedback
      tds.forEach((td) => {
        td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">📄 Queued...</span>`;
        td.style.cursor = "wait";
      });
    }

    if (tasks.length === 0) {
      Zotero.debug("[seerai] No PDFs to extract");
      return;
    }

    Zotero.debug(`[seerai] ${tasks.length} PDFs to extract`);

    const extractBtn = doc.getElementById(
      "extract-all-btn",
    ) as HTMLButtonElement | null;
    const originalBtnText = extractBtn?.innerText || "📄 Extract All";
    let completed = 0;
    let success = 0;
    let failed = 0;

    const updateProgress = () => {
      if (extractBtn) {
        extractBtn.innerText = `📄 OCR ${completed}/${tasks.length}`;
        extractBtn.disabled = true;
        extractBtn.style.cursor = "wait";
      }
    };

    const processTask = async (task: ExtractionTask): Promise<void> => {
      try {
        // Update cells to "Processing"
        task.tds.forEach((td) => {
          td.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">📄 OCR Processing...</span>`;
        });

        // Run silent OCR
        await ocrService.convertToMarkdown(task.pdf, { showProgress: false });

        // Update cells to "Done"
        task.tds.forEach((td) => {
          td.innerHTML = `<span style="color: green; font-size: 11px;">✓ Note Extracted</span>`;
        });
        success++;
      } catch (e) {
        Zotero.debug(`[seerai] OCR Error for ${task.paperId}: ${e}`);
        task.tds.forEach((td) => {
          td.innerHTML = `<span style="color: #c62828; font-size: 11px;">OCR Error</span>`;
          td.title = String(e);
        });
        failed++;
      } finally {
        completed++;
        updateProgress();
      }
    };

    // Process in batches
    updateProgress();
    for (let i = 0; i < tasks.length; i += maxConcurrent) {
      const batch = tasks.slice(i, i + maxConcurrent);
      await Promise.all(batch.map(processTask));

      // Refresh logic - notes added, so we should refresh validity
      // But full refresh might kill our "Done" status
      // Maybe just refresh at very end
    }

    // Restore button
    if (extractBtn) {
      extractBtn.innerText = `✓ OCR Done (${success})`;
      extractBtn.disabled = false;
      extractBtn.style.cursor = "pointer";
      setTimeout(() => {
        extractBtn.innerText = originalBtnText;
      }, 2000);
    }

    // Refresh table to pick up new notes and show "Generate" button
    setTimeout(() => {
      this.debounceTableRefresh(doc, item);
    }, 1500);
  }

  /**
   * Search for PDFs for all table items that don't have PDF attachments
   * Processes items concurrently with timeout, retry, and progress tracking
   */
  private static async searchAllPdfsInTable(
    doc: Document,
    btn: HTMLElement,
  ): Promise<void> {
    Zotero.debug("[seerai] Search all PDF clicked");

    // Find all visible rows
    const table = doc.querySelector(".papers-table");
    if (!table) return;

    const rows = table.querySelectorAll("tr[data-paper-id]");
    if (rows.length === 0) return;

    // Build list of items without PDF
    interface SearchTask {
      paperId: number;
      item: Zotero.Item;
    }
    const tasks: SearchTask[] = [];

    for (let i = 0; i < rows.length; i++) {
      const tr = rows[i] as HTMLElement;
      const paperId = parseInt(tr.getAttribute("data-paper-id") || "0", 10);
      if (!paperId) continue;

      const item = Zotero.Items.get(paperId);
      if (!item || !item.isRegularItem()) continue;

      // Check if has PDF already
      const attachments = item.getAttachments() || [];
      const hasPdf = attachments.some((attId: number) => {
        const att = Zotero.Items.get(attId);
        return (
          att &&
          (att.attachmentContentType === "application/pdf" ||
            att.attachmentPath?.toLowerCase().endsWith(".pdf"))
        );
      });

      if (!hasPdf) {
        // Check if has identifiers for search
        const doi = item.getField("DOI") as string;
        const arxivId = extractArxivFromItem(item);
        const pmid = extractPmidFromItem(item);
        const title = item.getField("title") as string;

        // Allow search if we have IDs OR at least a title (for Firecrawl/SS fallback)
        if (doi || arxivId || pmid || title) {
          tasks.push({ paperId, item });
        }
      }
    }

    if (tasks.length === 0) {
      Zotero.debug("[seerai] No items without PDF to search");
      btn.innerText = "✓ All have PDFs";
      setTimeout(() => {
        btn.innerText = "🔍 Search all PDF";
      }, 2000);
      return;
    }

    Zotero.debug(
      `[seerai] ${tasks.length} items to search for PDFs (concurrent mode)`,
    );

    const originalBtnText = btn.innerText;
    (btn as HTMLButtonElement).disabled = true;
    btn.style.cursor = "wait";

    // Get settings for concurrency (default 5)
    const concurrency =
      (Zotero.Prefs.get(
        `${addon.data.config.prefsPrefix}.pdfSearchConcurrency`,
      ) as number) || 5;

    // Initialize batch tracking state
    pdfSearchBatchActive = true;
    pdfSearchBatchStats = {
      total: tasks.length,
      completed: 0,
      succeeded: 0,
      failed: 0,
      skipped: 0,
    };

    // Initialize individual paper states
    for (const task of tasks) {
      activePdfSearches.set(task.paperId, {
        paperId: task.paperId,
        status: "queued",
        startedAt: Date.now(),
      });
    }

    // Helper to update both tracking state AND cell UI
    const updateCellStatus = (
      paperId: number,
      status:
        | "searching"
        | "found"
        | "notfound"
        | "skipped"
        | "error"
        | "retrying",
      message?: string,
    ) => {
      // Update tracking state
      const existing = activePdfSearches.get(paperId);
      if (existing) {
        existing.status = status as PdfSearchStatus;
        existing.message = message;
      }

      // Update DOM (may not exist if tab switched)
      const tr = doc.querySelector(`tr[data-paper-id="${paperId}"]`);
      if (!tr) return;

      const computedCells = tr.querySelectorAll("td");
      computedCells.forEach((td: Element) => {
        const content = String(td.innerHTML);
        if (
          content.includes("Search PDF") ||
          content.includes("Source-Link") ||
          content.includes("Searching") ||
          content.includes("⏳") ||
          content.includes("↻")
        ) {
          const cell = td as HTMLElement;
          switch (status) {
            case "searching":
              cell.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">🔍 Searching...</span>`;
              break;
            case "retrying":
              cell.innerHTML = `<span style="color: #f57c00; font-size: 11px;">↻ Retrying...</span>`;
              break;
            case "found":
              cell.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">📄 Process PDF</span>`;
              break;
            case "skipped":
              cell.innerHTML = `<span style="color: #f57c00; font-size: 11px;">⏭ ${message || "Skipped"}</span>`;
              break;
            case "error":
              cell.innerHTML = `<span style="color: #c62828; font-size: 11px;">✗ ${message || "Error"}</span>`;
              cell.title = message || "";
              break;
            case "notfound": {
              const taskItem = tasks.find((t) => t.paperId === paperId);
              if (!taskItem) {
                cell.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px; font-style: italic;">❌ Not found</span>`;
                break;
              }

              // Resolve identifiers and source link
              const doi = taskItem.item.getField("DOI") as string;
              const arxiv = extractArxivFromItem(taskItem.item);
              const pmid = extractPmidFromItem(taskItem.item);
              const itemUrl = taskItem.item.getField("url") as string;
              const sourceLink = getSourceLinkForPaper(
                doi,
                arxiv,
                pmid,
                undefined,
                itemUrl,
              );

              if (sourceLink) {
                // Clicking this will trigger the expanded menu (Attach, Retry, Note) via the TD click listener
                cell.innerHTML = `<span class="source-link-btn" data-url="${sourceLink}" style="color: var(--highlight-primary); font-size: 11px; cursor: pointer; text-decoration: underline;">🔗 Source-Link</span>`;
              } else {
                cell.innerHTML = `<span class="attach-pdf-btn" data-item-id="${paperId}" style="color: var(--highlight-primary); font-size: 11px; cursor: pointer; text-decoration: underline;">⬇️ Attach</span>`;
              }
              break;
            }
          }
        }
      });
    };

    // Run concurrent PDF search
    const results = await runConcurrentTasks({
      tasks,
      concurrency,
      maxRetries: 3,
      retryDelayMs: 2000,

      onProgress: (stats: TaskStats) => {
        // Update batch stats for persistence across tab switches
        pdfSearchBatchStats = {
          total: stats.total,
          completed: stats.completed,
          succeeded: stats.succeeded,
          failed: stats.failed,
          skipped: stats.skipped,
        };
        btn.innerText = `🔍 ${formatTaskStats(stats)}`;
      },

      onTaskStart: (task) => {
        updateCellStatus(task.paperId, "searching");
      },

      executor: async (task) => {
        const success = await findAndAttachPdfForItem(task.item);
        return success;
      },

      onTaskComplete: async (task, success) => {
        if (success) {
          Zotero.debug(`[seerai] Found PDF for item ${task.paperId}`);
          updateCellStatus(task.paperId, "found");

          // Persist the success state
          if (currentTableConfig) {
            if (!currentTableConfig.pdfDiscoveryData)
              currentTableConfig.pdfDiscoveryData = {};
            currentTableConfig.pdfDiscoveryData[task.paperId] = {
              status: "found",
              discoveredAt: new Date().toISOString(),
            };
            const tableStore = getTableStore();
            await tableStore.saveConfig(currentTableConfig);
          }
        } else {
          updateCellStatus(task.paperId, "notfound");

          // Persist the failure/source-link state
          if (currentTableConfig) {
            const doi = task.item.getField("DOI") as string;
            const arxiv = extractArxivFromItem(task.item);
            const pmid = extractPmidFromItem(task.item);
            const itemUrl = task.item.getField("url") as string;
            const sourceLink = getSourceLinkForPaper(
              doi,
              arxiv,
              pmid,
              undefined,
              itemUrl,
            );

            if (!currentTableConfig.pdfDiscoveryData)
              currentTableConfig.pdfDiscoveryData = {};
            currentTableConfig.pdfDiscoveryData[task.paperId] = {
              status: sourceLink ? "source_link" : "not_found",
              sourceUrl: sourceLink || undefined,
              discoveredAt: new Date().toISOString(),
            };
            const tableStore = getTableStore();
            await tableStore.saveConfig(currentTableConfig);
          }
        }
      },

      onTaskError: (task, error, _index, willRetry) => {
        if (willRetry) {
          Zotero.debug(
            `[seerai] Retrying PDF search for ${task.paperId}: ${error.message}`,
          );
          updateCellStatus(task.paperId, "retrying");
        } else {
          Zotero.debug(
            `[seerai] PDF search failed for ${task.paperId}: ${error.message}`,
          );
          updateCellStatus(task.paperId, "error", error.message);
        }
      },
    });

    // Calculate final stats
    const succeeded = results.filter(
      (r) => r.status === "success" && r.result === true,
    ).length;
    const failed = results.filter((r) => r.status === "failed").length;
    const notFound = results.filter(
      (r) => r.status === "success" && r.result === false,
    ).length;

    Zotero.debug(
      `[seerai] PDF search complete: ${succeeded} found, ${notFound} not found, ${failed} failed`,
    );

    // Update batch stats
    pdfSearchBatchStats = {
      total: tasks.length,
      completed: tasks.length,
      succeeded,
      failed,
      skipped: 0,
    };

    // Clear batch active state (but keep individual states for a bit for UI restoration)
    pdfSearchBatchActive = false;

    // Clear individual states after a delay (allow UI restoration on tab switch back)
    setTimeout(() => {
      for (const task of tasks) {
        activePdfSearches.delete(task.paperId);
      }
    }, 30000); // Keep states for 30 seconds

    // Restore button with summary
    btn.innerText = `✓ ${succeeded}📄 ${failed > 0 ? `${failed}✗` : ""}`;
    (btn as HTMLButtonElement).disabled = false;
    btn.style.cursor = "pointer";
    setTimeout(() => {
      btn.innerText = originalBtnText;
    }, 4000);
  }

  /**
   * Helper: Search Semantic Scholar for a table item (fallback interaction)
   * Parses metadata from SS results (DOI, PMID, ArXiv) and uses them for full PDF discovery
   */
  private static async searchSemanticScholarForTableItem(
    item: Zotero.Item,
    btn: HTMLElement,
  ): Promise<void> {
    const originalText = btn.innerText;
    btn.innerHTML = "⏳";
    (btn as HTMLButtonElement).disabled = true;
    btn.style.cursor = "wait";

    try {
      // Get query
      const title = item.getField("title") as string;
      if (!title) {
        btn.title = "No title to search";
        btn.innerText = "❌";
        return;
      }

      // Search SS
      const results = await semanticScholarService.searchPapers({
        query: title,
        limit: 1,
      });
      if (results && results.data.length > 0) {
        const paper = results.data[0];
        let pdfAttached = false;

        // Extract identifiers from SS result
        const discoveredDoi = paper.externalIds?.DOI;
        const discoveredPmid = paper.externalIds?.PMID;
        const discoveredArxivId = paper.externalIds?.ArXiv;

        Zotero.debug(
          `[seerai] SS found identifiers - DOI: ${discoveredDoi}, PMID: ${discoveredPmid}, ArXiv: ${discoveredArxivId}`,
        );

        // Update Zotero item metadata if missing
        let metadataUpdated = false;
        if (discoveredDoi && !item.getField("DOI")) {
          item.setField("DOI", discoveredDoi);
          metadataUpdated = true;
          Zotero.debug(`[seerai] Updated item DOI: ${discoveredDoi}`);
        }
        // PMID and ArXiv are typically stored in 'extra' field
        const currentExtra = (item.getField("extra") as string) || "";
        if (discoveredPmid && !currentExtra.includes("PMID:")) {
          item.setField(
            "extra",
            currentExtra +
              (currentExtra ? "\n" : "") +
              `PMID: ${discoveredPmid}`,
          );
          metadataUpdated = true;
          Zotero.debug(`[seerai] Updated item PMID: ${discoveredPmid}`);
        }
        if (discoveredArxivId && !currentExtra.includes("arXiv:")) {
          const extra = (item.getField("extra") as string) || "";
          item.setField(
            "extra",
            extra + (extra ? "\n" : "") + `arXiv: ${discoveredArxivId}`,
          );
          metadataUpdated = true;
          Zotero.debug(`[seerai] Updated item ArXiv: ${discoveredArxivId}`);
        }
        if (metadataUpdated) {
          await item.saveTx();
          Zotero.debug(`[seerai] Saved updated metadata for item`);
        }

        // Step 1: Try SS Open Access PDF first
        if (paper.openAccessPdf?.url) {
          btn.innerText = "📥 SS PDF...";
          const attached = await downloadAndAttachPdf(
            item,
            paper.openAccessPdf.url,
          );
          if (attached) {
            pdfAttached = true;
            btn.innerText = "✓";
            btn.title = "PDF Attached from Semantic Scholar!";

            // Update cell to show "Process PDF"
            const tr = btn.closest("tr");
            if (tr) {
              const cells = tr.querySelectorAll("td");
              cells.forEach((td: HTMLElement) => {
                if (td.contains(btn)) {
                  td.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">📄 Process PDF</span>`;
                }
              });
            }
            return;
          }
        }

        // Step 2: Update table cell metadata and feedback
        // We no longer trigger the full findAndAttachPdfForItem automatically here
        // because it's redundant if the batch search just finished and failed.
        if (!pdfAttached) {
          if (metadataUpdated) {
            btn.innerText = "✓ Metadata Updated";
            btn.title =
              "Found new identifiers (DOI/PMID). Re-rendering row for better direct links...";

            // Re-render interface after a short delay to show updated buttons (Source-Link will now be better)
            setTimeout(() => {
              if (currentContainer && currentItem) {
                Assistant.renderInterface(currentContainer, currentItem);
              }
            }, 1500);
          } else {
            btn.innerText = "🔍 No PDF";
            btn.title =
              "Search finished: Metadata already correct, no OA PDF found on Semantic Scholar.";
          }
        }

        // Fallback to launching URL if all PDF attempts failed
        if (!pdfAttached && paper.url) {
          Zotero.launchURL(paper.url);
          btn.innerText = "📖";
          btn.title = "Opened Semantic Scholar page (no PDF found)";
        } else if (!pdfAttached) {
          btn.title = "No PDF found via any source";
          btn.innerText = "❌";
        }
      } else {
        btn.title = "Not found on Semantic Scholar";
        btn.innerText = "❌";
      }
    } catch (e) {
      Zotero.debug(`[seerai] SS Search error: ${e}`);
      btn.title = `Error: ${e}`;
      btn.innerText = "⚠️";
    } finally {
      if (
        btn.innerText === "⏳" ||
        btn.innerText.includes("📥") ||
        btn.innerText.includes("🔍")
      ) {
        btn.innerText = originalText;
      }
      (btn as HTMLButtonElement).disabled = false;
      btn.style.cursor = "pointer";
    }
  }

  /**
   * Helper: Interactive Firecrawl search for table item
   */
  private static async searchFirecrawlForTableItem(
    item: Zotero.Item,
    btn: HTMLElement,
  ): Promise<void> {
    const originalText = btn.innerText;
    btn.innerHTML = "⏳";
    (btn as HTMLButtonElement).disabled = true;
    btn.style.cursor = "wait";

    try {
      const title = item.getField("title") as string;
      const doi = (item.getField("DOI") as string) || undefined;
      // Get creators
      const creators = item.getCreators();
      const authors = creators.map((c) =>
        `${c.firstName || ""} ${c.lastName || ""}`.trim(),
      );

      // Search
      const result = await firecrawlService.researchSearch(title, authors, doi);

      if (
        result &&
        (result.status === "pdf_found" || result.status === "page_found")
      ) {
        const url = result.pdfUrl || result.pageUrl;
        if (url) {
          const doc = btn.ownerDocument;
          const win = doc?.defaultView;
          if (win) {
            const choice = win.confirm(
              `Firecrawl found: ${url}\n\nDo you want to attach this file?`,
            );
            if (choice) {
              btn.innerText = "📥";
              await Zotero.Attachments.importFromURL({
                url: url,
                parentItemID: item.id,
                title: "firecrawl_found.pdf",
                contentType: "application/pdf",
              });
              // Update UI to "Process PDF" on success
              const cell = btn.parentElement?.parentElement;
              if (cell) {
                cell.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">📄 Process PDF</span>`;
              }
            } else {
              // User cancelled, revert button
              btn.innerText = originalText;
            }
          }
        } else {
          btn.title = "No suitable URL found";
          btn.innerText = "❌";
        }
      } else {
        btn.title = "Not found via Firecrawl";
        btn.innerText = "❌";
      }
    } catch (e) {
      Zotero.debug(`[seerai] Firecrawl search error: ${e}`);
      btn.title = `Error: ${e}`;
      btn.innerText = "⚠️";
    } finally {
      if (btn.innerText === "⏳") btn.innerText = originalText;
      (btn as HTMLButtonElement).disabled = false;
      btn.style.cursor = "pointer";
    }
  }

  /**
   * Generate content for a single cell
   */
  private static async generateCellContent(
    doc: Document,
    row: TableRow,
    col: TableColumn,
    td: HTMLElement,
  ): Promise<void> {
    // Show loading indicator
    td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">⏳ Generating...</span>`;
    td.style.cursor = "wait";

    try {
      const item = Zotero.Items.get(row.paperId);
      if (!item) throw new Error("Item not found");

      const content = await this.generateColumnContent(item, col, row.noteIds);

      // Update cell display
      td.innerText = content || "(No content generated)";
      td.style.cursor = "default";
      td.style.backgroundColor = "";

      // Update row data
      row.data[col.id] = content;

      // Save to tableStore (persist the generated data)
      const tableStore = getTableStore();
      await tableStore.saveConfig(currentTableConfig!);
    } catch (e) {
      td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Error: ${e}</span>`;
      td.style.cursor = "pointer";
      Zotero.debug(`[seerai] Cell generation error: ${e}`);
    }
  }

  /**
   * Extract text from PDF and generate content
   */
  private static async extractPDFAndGenerate(
    doc: Document,
    row: TableRow,
    col: TableColumn,
    td: HTMLElement,
  ): Promise<void> {
    // Show loading indicator
    td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">📄 Extracting PDF...</span>`;
    td.style.cursor = "wait";

    try {
      const item = Zotero.Items.get(row.paperId);
      if (!item) throw new Error("Item not found");

      // Get PDF attachments
      const attachmentIds = item.getAttachments();
      let pdfText = "";

      for (const attId of attachmentIds) {
        const att = Zotero.Items.get(attId);
        if (att && att.attachmentContentType === "application/pdf") {
          // Try to get full-text content (Zotero indexes PDFs)
          try {
            // Try different Zotero Fulltext APIs (varies by version)
            let fullText = "";
            if ((Zotero.Fulltext as any).getItemContent) {
              const content = await (Zotero.Fulltext as any).getItemContent(
                att.id,
              );
              fullText = content?.content || "";
            } else if ((Zotero.Fulltext as any).getTextForItem) {
              fullText =
                (await (Zotero.Fulltext as any).getTextForItem(att.id)) || "";
            }
            if (fullText) {
              pdfText += fullText; // Use full context - no limit
              break;
            }
          } catch (e) {
            Zotero.debug(`[seerai] Error getting fulltext: ${e}`);
          }
        }
      }

      if (!pdfText) {
        td.innerHTML = `<span style="color: #ff9800; font-size: 11px;">PDF not indexed</span>`;
        td.style.cursor = "default";
        return;
      }

      // Now generate with PDF context
      td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">⚡ Generating...</span>`;

      const content = await this.generateColumnContentFromText(
        item,
        col,
        pdfText,
      );

      // Update cell display
      td.innerText = content || "(No content generated)";
      td.style.cursor = "default";
      td.style.backgroundColor = "";

      // Update row data
      row.data[col.id] = content;
    } catch (e) {
      td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Error: ${e}</span>`;
      td.style.cursor = "pointer";
      Zotero.debug(`[seerai] PDF extraction error: ${e}`);
    }
  }

  /**
   * Generate table data for specified items and columns (public for agent tools)
   * This runs AI analysis and saves results to the table config
   */
  public static async generateDataForTable(
    tableId: string,
    itemIds?: number[],
    columnIds?: string[],
  ): Promise<{ generatedCount: number; errors: string[] }> {
    const tableStore = getTableStore();
    const allTables = await tableStore.getAllTables();
    const table = allTables.find((t) => t.id === tableId);

    if (!table) {
      throw new Error(`Table ${tableId} not found`);
    }

    const errors: string[] = [];
    let generatedCount = 0;

    // Determine which items to process
    const items = itemIds || table.addedPaperIds || [];

    // Determine which columns to generate (AI columns only)
    const columnsToGenerate = columnIds
      ? table.columns.filter((c: TableColumn) => columnIds.includes(c.id))
      : table.columns.filter(
          (c: TableColumn) => c.type === "computed" || c.aiPrompt,
        );

    if (columnsToGenerate.length === 0 || items.length === 0) {
      return { generatedCount: 0, errors: [] };
    }

    Zotero.debug(
      `[seerai] generateDataForTable: ${items.length} items × ${columnsToGenerate.length} columns`,
    );

    // Initialize generatedData if needed
    if (!table.generatedData) {
      table.generatedData = {};
    }

    // Get max concurrent from active model settings
    const activeConfig = getActiveModelConfig();
    let maxConcurrent = 5;
    if (activeConfig?.rateLimit?.type === "concurrency") {
      maxConcurrent = activeConfig.rateLimit.value;
    } else if (activeConfig?.rateLimit?.type === "rpm") {
      maxConcurrent = Math.max(1, Math.min(10, activeConfig.rateLimit.value));
    } else if (activeConfig?.rateLimit?.type === "tpm") {
      maxConcurrent = 10;
    }

    Zotero.debug(`[seerai] Table generation max concurrent: ${maxConcurrent}`);

    // Build task list
    interface GenerationTask {
      paperId: number;
      col: TableColumn;
      item: Zotero.Item;
      noteContent: string;
    }
    const tasks: GenerationTask[] = [];

    for (const paperId of items) {
      const item = Zotero.Items.get(paperId);
      if (!item) {
        errors.push(`Item ${paperId} not found`);
        continue;
      }

      // Initialize data for this paper
      if (!table.generatedData[paperId]) {
        table.generatedData[paperId] = {};
      }

      // Get note content for this item
      let noteContent = "";
      const childNotes = item.getNotes();
      for (const noteId of childNotes) {
        const noteItem = Zotero.Items.get(noteId);
        if (noteItem) {
          const noteHTML = noteItem.getNote();
          noteContent += this.stripHtml(noteHTML) + "\n\n";
        }
      }

      // Try to get PDF text if no notes
      if (!noteContent.trim()) {
        const attachmentIds = item.getAttachments();
        for (const attId of attachmentIds) {
          const att = Zotero.Items.get(attId);
          if (att && att.attachmentContentType === "application/pdf") {
            try {
              if ((Zotero.Fulltext as any).getItemContent) {
                const content = await (Zotero.Fulltext as any).getItemContent(
                  att.id,
                );
                noteContent = content?.content || "";
              } else if ((Zotero.Fulltext as any).getTextForItem) {
                noteContent =
                  (await (Zotero.Fulltext as any).getTextForItem(att.id)) || "";
              }
              if (noteContent) break;
            } catch (e) {
              Zotero.debug(`[seerai] Error getting fulltext: ${e}`);
            }
          }
        }
      }

      if (!noteContent.trim()) {
        errors.push(`No content available for item ${paperId}`);
        continue;
      }

      // Add tasks for each column
      for (const col of columnsToGenerate) {
        const tableCol: TableColumn = {
          id: col.id,
          name: col.name,
          width: col.width || 150,
          minWidth: col.minWidth || 80,
          visible: true,
          sortable: false,
          resizable: true,
          type: col.type || "computed",
          aiPrompt: col.aiPrompt,
        };
        tasks.push({ paperId, col: tableCol, item, noteContent });
      }
    }

    await runConcurrentTasks<GenerationTask, string | null>({
      tasks,
      concurrency: maxConcurrent,
      maxRetries: 3,
      executor: async (task) => {
        return await this.generateColumnContentFromText(
          task.item,
          task.col,
          task.noteContent,
        );
      },
      onTaskComplete: (task, content) => {
        if (content) {
          table.generatedData![task.paperId]![task.col.id] = content;
          generatedCount++;
          Zotero.debug(
            `[seerai] Generated: item=${task.paperId} col=${task.col.name} length=${content.length}`,
          );
        }
      },
      onTaskError: (task, error, index, willRetry) => {
        const errorMsg = `Error generating ${task.col.name} for item ${task.paperId}: ${error.message}`;
        if (!willRetry) {
          errors.push(errorMsg);
        }
        Zotero.debug(`[seerai] ${errorMsg}${willRetry ? " (will retry)" : ""}`);
      },
    });

    // Save the updated table config
    await tableStore.saveConfig(table);
    Zotero.debug(`[seerai] Saved generatedData for table ${tableId}`);

    return { generatedCount, errors };
  }

  /**
   * Generate column content using AI
   */
  private static async generateColumnContent(
    item: Zotero.Item,
    col: TableColumn,
    noteIds: number[],
  ): Promise<string> {
    // Get note content
    let noteContent = "";
    for (const noteId of noteIds) {
      const noteItem = Zotero.Items.get(noteId);
      if (noteItem) {
        const noteHTML = noteItem.getNote();
        noteContent += this.stripHtml(noteHTML) + "\n\n";
      }
    }

    if (!noteContent.trim()) {
      return "";
    }

    // Get paper metadata
    const paperTitle = (item.getField("title") as string) || "Untitled";
    const creators = item.getCreators();
    const authors = creators
      .map((c) => `${c.firstName || ""} ${c.lastName || ""}`.trim())
      .join(", ");

    return this.generateColumnContentFromText(item, col, noteContent);
  }

  /**
   * Generate column content from text using AI
   */
  private static async generateColumnContentFromText(
    item: Zotero.Item,
    col: TableColumn,
    sourceText: string,
  ): Promise<string> {
    const paperTitle = (item.getField("title") as string) || "Untitled";
    const effectiveMaxTokens =
      col.maxTokens ?? currentTableConfig?.responseLength;

    // Build a targeted prompt using column title and description
    const lengthInstruction =
      effectiveMaxTokens === 0 || effectiveMaxTokens === undefined
        ? ""
        : `Be concise (max ${effectiveMaxTokens} words).`;

    let columnPrompt = "";
    if (col.aiPrompt) {
      // Use both column name (title) and aiPrompt (description)
      columnPrompt = `For the column "${col.name}": ${col.aiPrompt} ${lengthInstruction}`;
    } else {
      // Fallback prompts for known columns
      switch (col.id) {
        case "analysisMethodology":
          columnPrompt = `For the column "${col.name}": Identify and briefly describe the analysis methodology or research method used in this paper. ${lengthInstruction}`;
          break;
        default:
          columnPrompt = `For the column "${col.name}": Extract relevant information. ${lengthInstruction}`;
      }
    }

    const systemPrompt = `You are extracting structured information from academic papers for a research table. Be concise and factual. Return ONLY the requested information, no explanations or preamble.`;

    const userPrompt = `Paper: "${paperTitle}"

Source content:
${sourceText}

Task: ${columnPrompt}`;

    // Get active model config (same as chat uses)
    const activeModel = getActiveModelConfig();
    if (!activeModel) {
      throw new Error(
        "No active model configured. Please set up a model in settings.",
      );
    }

    const configOverride = {
      apiURL: activeModel.apiURL,
      apiKey: activeModel.apiKey,
      model: activeModel.model,
      temperature:
        col.temperature ?? currentTableConfig?.temperature ?? undefined,
      max_tokens: effectiveMaxTokens,
    };

    // Use non-streaming completion for simpler cell generation
    try {
      const messages: OpenAIMessage[] = [
        { role: "system", content: systemPrompt },
        { role: "user", content: userPrompt },
      ];

      // Use chatCompletionStream but collect the full response
      let fullResponse = "";
      await openAIService.chatCompletionStream(
        messages,
        {
          onToken: (token) => {
            fullResponse += token;
          },
          onComplete: () => {},
          onError: (err) => {
            throw err;
          },
        },
        configOverride,
      );

      return fullResponse.trim();
    } catch (e) {
      Zotero.debug(`[seerai] AI generation error: ${e}`);
      throw e;
    }
  }

  /**
   * Generate AI tags for a paper item based on its content
   */
  private static async generateTagsForItem(
    row: TableRow,
    btn: HTMLElement,
  ): Promise<void> {
    const originalText = btn.innerText;
    btn.innerText = "⏳";
    btn.style.cursor = "wait";

    try {
      const item = Zotero.Items.get(row.paperId);
      if (!item) {
        throw new Error("Item not found");
      }

      // Get source content (notes or PDF text)
      let sourceText = "";
      const noteIds = item.getNotes();

      if (noteIds.length > 0) {
        // Get content from notes
        for (const noteId of noteIds) {
          const noteItem = Zotero.Items.get(noteId);
          if (noteItem) {
            const noteHTML = noteItem.getNote();
            sourceText += this.stripHtml(noteHTML) + "\n\n";
          }
        }
      } else {
        // Try to get PDF text
        const attachments = item.getAttachments();
        for (const attId of attachments) {
          const att = Zotero.Items.get(attId);
          if (att && att.attachmentContentType === "application/pdf") {
            try {
              const path = await att.getFilePathAsync();
              if (!path) continue;
              const text = await Zotero.PDFWorker.getFullText(att.id, 10);
              const pdfText = text?.text || "";
              if (pdfText) {
                sourceText = pdfText.substring(0, 10000); // Limit text length
                break;
              }
            } catch (e) {
              Zotero.debug(`[seerai] Error extracting PDF text: ${e}`);
            }
          }
        }
      }

      // If no source text, use metadata only
      const paperTitle = (item.getField("title") as string) || "Untitled";
      const abstract = (item.getField("abstractNote") as string) || "";
      const creators = item.getCreators();
      const authors = creators
        .map((c) => `${c.firstName || ""} ${c.lastName || ""}`.trim())
        .join(", ");

      if (!sourceText.trim()) {
        sourceText = `Title: ${paperTitle}\nAuthors: ${authors}\nAbstract: ${abstract}`;
      }

      if (!sourceText.trim()) {
        btn.innerText = "❌";
        btn.title = "No content available to generate tags";
        setTimeout(() => {
          btn.innerText = originalText;
          btn.title = "Generate AI tags for this paper";
        }, 2000);
        btn.style.cursor = "pointer";
        return;
      }

      // Define the tool for structured tag generation
      const tagGenerationTool: ToolDefinition = {
        type: "function",
        function: {
          name: "generate_tags",
          description:
            "Generate structured tags for an academic paper based on its content",
          parameters: {
            type: "object",
            properties: {
              tags: {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    name: {
                      type: "string",
                      description:
                        "A concise tag (1-3 words). Must be a topic, methodology, domain, or concept. Never use author names, locations, or partial sentences.",
                    },
                    category: {
                      type: "string",
                      enum: [
                        "topic",
                        "methodology",
                        "domain",
                        "concept",
                        "application",
                      ],
                      description: "The category of this tag",
                    },
                  },
                  required: ["name", "category"],
                },
                description: "Array of 3-7 tags for the paper",
              },
            },
            required: ["tags"],
          },
        },
      };

      // Build prompts
      const systemPrompt = `You are a research librarian tagging academic papers. Generate precise, useful tags.

RULES:
- Generate 3-7 tags only
- Each tag must be 1-3 words, properly capitalized
- Tags must be topics, methodologies, domains, or concepts
- NEVER include: author names, institution names, cities, countries, or partial sentences
- NEVER include tags like "the methodology used" or "the clinical problem" - these are not tags
- Good examples: "Machine Learning", "Cardiovascular Disease", "Meta-Analysis", "Pediatrics"
- Bad examples: "Smith", "the main approach", "methodology", "San Diego"

You MUST call the generate_tags function with your response.`;

      const userPrompt = `Generate tags for this academic paper:

Title: ${paperTitle}
Authors: ${authors}

Content excerpt:
${sourceText.substring(0, 6000)}

Call the generate_tags function with an array of 3-7 high-quality tags.`;

      // Get active model config
      const activeModel = getActiveModelConfig();
      if (!activeModel) {
        throw new Error("No active model configured");
      }

      const configOverride = {
        apiURL: activeModel.apiURL,
        apiKey: activeModel.apiKey,
        model: activeModel.model,
      };

      // Generate tags using AI with function calling
      const messages: OpenAIMessage[] = [
        { role: "system", content: systemPrompt },
        { role: "user", content: userPrompt },
      ];

      let generatedTags: string[] = [];
      let functionCalled = false;

      await openAIService.chatCompletionStream(
        messages,
        {
          onToken: () => {},
          onComplete: () => {},
          onError: (err) => {
            throw err;
          },
          onToolCalls: (toolCalls) => {
            for (const toolCall of toolCalls) {
              if (toolCall.function.name === "generate_tags") {
                functionCalled = true;
                try {
                  const args = JSON.parse(toolCall.function.arguments);
                  if (args.tags && Array.isArray(args.tags)) {
                    generatedTags = args.tags
                      .map((t: { name: string }) => t.name?.trim())
                      .filter(
                        (t: string) => t && t.length > 0 && t.length < 40,
                      );
                  }
                } catch (e) {
                  Zotero.debug(
                    `[seerai] Error parsing tag function arguments: ${e}`,
                  );
                }
              }
            }
          },
        },
        configOverride,
        [tagGenerationTool],
      );

      if (!functionCalled || generatedTags.length === 0) {
        throw new Error(
          "No valid tags generated - model did not call the function",
        );
      }

      // Apply tags to item
      for (const tag of generatedTags) {
        item.addTag(tag);
      }
      item.addTag("Seerai-Tagged");
      await item.saveTx();

      // Show success
      btn.innerText = "✓";
      btn.style.color = "#4CAF50";
      btn.title = `Added ${generatedTags.length} tags: ${generatedTags.join(", ")}`;
      setTimeout(() => {
        btn.innerText = originalText;
        btn.style.color = "";
        btn.title = "Generate AI tags for this paper";
      }, 3000);

      Zotero.debug(
        `[seerai] Generated tags for item ${row.paperId}: ${generatedTags.join(", ")}`,
      );
    } catch (err) {
      Zotero.debug(`[seerai] Error generating tags: ${err}`);
      btn.innerText = "❌";
      btn.style.color = "#c62828";
      btn.title = `Error: ${err}`;
      setTimeout(() => {
        btn.innerText = originalText;
        btn.style.color = "";
        btn.title = "Generate AI tags for this paper";
      }, 2000);
    }
  }

  /**
   * Generate AI tags for a paper item silently (no UI feedback)
   * Used for bulk operations
   */
  private static async generateTagsForItemSilent(row: TableRow): Promise<void> {
    const item = Zotero.Items.get(row.paperId);
    if (!item) {
      throw new Error("Item not found");
    }

    // Get source content (notes or PDF text)
    let sourceText = "";
    const noteIds = item.getNotes();

    if (noteIds.length > 0) {
      for (const noteId of noteIds) {
        const noteItem = Zotero.Items.get(noteId);
        if (noteItem) {
          const noteHTML = noteItem.getNote();
          sourceText += this.stripHtml(noteHTML) + "\n\n";
        }
      }
    } else {
      const attachments = item.getAttachments();
      for (const attId of attachments) {
        const att = Zotero.Items.get(attId);
        if (att && att.attachmentContentType === "application/pdf") {
          try {
            const path = await att.getFilePathAsync();
            if (!path) continue;
            const text = await Zotero.PDFWorker.getFullText(att.id, 10);
            const pdfText = text?.text || "";
            if (pdfText) {
              sourceText = pdfText.substring(0, 10000);
              break;
            }
          } catch (e) {
            Zotero.debug(`[seerai] Error extracting PDF text: ${e}`);
          }
        }
      }
    }

    const paperTitle = (item.getField("title") as string) || "Untitled";
    const abstract = (item.getField("abstractNote") as string) || "";
    const creators = item.getCreators();
    const authors = creators
      .map((c) => `${c.firstName || ""} ${c.lastName || ""}`.trim())
      .join(", ");

    if (!sourceText.trim()) {
      sourceText = `Title: ${paperTitle}\nAuthors: ${authors}\nAbstract: ${abstract}`;
    }

    if (!sourceText.trim()) {
      throw new Error("No content available");
    }

    const tagGenerationTool: ToolDefinition = {
      type: "function",
      function: {
        name: "generate_tags",
        description: "Generate structured tags for an academic paper",
        parameters: {
          type: "object",
          properties: {
            tags: {
              type: "array",
              items: {
                type: "object",
                properties: {
                  name: { type: "string" },
                  category: {
                    type: "string",
                    enum: [
                      "topic",
                      "methodology",
                      "domain",
                      "concept",
                      "application",
                    ],
                  },
                },
                required: ["name", "category"],
              },
            },
          },
          required: ["tags"],
        },
      },
    };

    const systemPrompt = `You are a research librarian tagging academic papers. Generate 3-7 precise tags.
RULES: Tags must be 1-3 words. Never include author names, locations, or partial sentences.
You MUST call the generate_tags function.`;

    const userPrompt = `Generate tags for: "${paperTitle}" by ${authors}\n\n${sourceText.substring(0, 6000)}`;

    const activeModel = getActiveModelConfig();
    if (!activeModel) throw new Error("No model configured");

    const messages: OpenAIMessage[] = [
      { role: "system", content: systemPrompt },
      { role: "user", content: userPrompt },
    ];

    let generatedTags: string[] = [];
    let functionCalled = false;

    await openAIService.chatCompletionStream(
      messages,
      {
        onToken: () => {},
        onComplete: () => {},
        onError: (err) => {
          throw err;
        },
        onToolCalls: (toolCalls) => {
          for (const tc of toolCalls) {
            if (tc.function.name === "generate_tags") {
              functionCalled = true;
              try {
                const args = JSON.parse(tc.function.arguments);
                if (args.tags && Array.isArray(args.tags)) {
                  generatedTags = args.tags
                    .map((t: { name: string }) => t.name?.trim())
                    .filter((t: string) => t && t.length > 0 && t.length < 40);
                }
              } catch (e) {
                Zotero.debug(`[seerai] Error parsing tags: ${e}`);
              }
            }
          }
        },
      },
      {
        apiURL: activeModel.apiURL,
        apiKey: activeModel.apiKey,
        model: activeModel.model,
      },
      [tagGenerationTool],
    );

    if (!functionCalled || generatedTags.length === 0) {
      throw new Error("No valid tags generated");
    }

    for (const tag of generatedTags) {
      item.addTag(tag);
    }
    item.addTag("Seerai-Tagged");
    await item.saveTx();

    Zotero.debug(
      `[seerai] Generated tags for ${row.paperId}: ${generatedTags.join(", ")}`,
    );
  }

  /**
   * Show cell detail modal for viewing/generating content
   */
  private static showCellDetailModal(
    doc: Document,
    row: TableRow,
    col: TableColumn,
    currentValue: string,
  ): void {
    // Remove any existing modal
    const existing = doc.getElementById("cell-detail-modal");
    if (existing) existing.remove();

    const win = doc.defaultView;
    const isDarkMode =
      (win as any)?.matchMedia?.("(prefers-color-scheme: dark)").matches ??
      false;
    const isComputed = col.type === "computed";

    // Check for item and its notes/PDF (fresh data, not stale row data)
    const item = Zotero.Items.get(row.paperId);
    const attachments = item?.getAttachments() || [];
    const notes = item?.getNotes() || [];
    const hasNotes = notes.length > 0;
    const hasPDF = attachments.some((attId: number) => {
      const att = Zotero.Items.get(attId);
      return att && att.attachmentContentType === "application/pdf";
    });

    // Create overlay
    const overlay = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "cell-detail-modal" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        width: "100%",
        height: "100%",
        backgroundColor: "rgba(0,0,0,0.5)",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        zIndex: "10000",
      },
    });

    // Create dialog
    const dialog = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        backgroundColor: `var(--background-primary, ${isDarkMode ? "#333" : "#fafafa"})`,
        color: `var(--text-primary, ${isDarkMode ? "#eee" : "#212121"})`,
        borderRadius: "12px",
        padding: "20px",
        maxWidth: "600px",
        width: "90%",
        maxHeight: "70vh",
        display: "flex",
        flexDirection: "column",
        gap: "16px",
        boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
      },
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      },
    });
    const title = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: col.name },
      styles: { fontSize: "16px", fontWeight: "600" },
    });
    header.appendChild(title);

    const closeX = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "none",
        border: "none",
        fontSize: "18px",
        cursor: "pointer",
        color: "var(--text-secondary)",
      },
      listeners: [{ type: "click", listener: () => overlay.remove() }],
    });
    header.appendChild(closeX);
    dialog.appendChild(header);

    // Paper info
    const paperInfo = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: row.paperTitle },
      styles: {
        fontSize: "13px",
        color: "var(--text-secondary)",
        fontStyle: "italic",
      },
    });
    dialog.appendChild(paperInfo);

    // Mode toggle (Preview / Edit)
    let isEditMode = !currentValue; // Start in edit mode if empty, preview if has content

    const modeToggle = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "4px",
        marginBottom: "8px",
      },
    });

    const previewBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "👁 Preview" },
      styles: {
        padding: "6px 12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: !isEditMode
          ? "var(--highlight-primary)"
          : "var(--background-secondary)",
        color: !isEditMode ? "var(--highlight-text)" : "var(--text-primary)",
        cursor: "pointer",
        fontSize: "12px",
      },
    });

    const editBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✏️ Edit" },
      styles: {
        padding: "6px 12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: isEditMode
          ? "var(--highlight-primary)"
          : "var(--background-secondary)",
        color: isEditMode ? "var(--highlight-text)" : "var(--text-primary)",
        cursor: "pointer",
        fontSize: "12px",
      },
    });

    modeToggle.appendChild(previewBtn);
    modeToggle.appendChild(editBtn);
    dialog.appendChild(modeToggle);

    // Content container to hold either preview or textarea
    const contentContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        flex: "1",
        minHeight: "200px",
        display: "flex",
        flexDirection: "column",
      },
    });

    // Preview area (shows rendered markdown)
    const previewArea = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        flex: "1",
        minHeight: "200px",
        padding: "12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        fontSize: "13px",
        lineHeight: "1.6",
        backgroundColor: "var(--background-secondary)",
        overflowY: "auto",
        display: isEditMode ? "none" : "block",
      },
    });
    previewArea.innerHTML = currentValue
      ? parseMarkdown(currentValue)
      : '<span style="color: var(--text-tertiary); font-style: italic;">No content yet</span>';

    // Content area (editable textarea)
    const contentArea = ztoolkit.UI.createElement(doc, "textarea", {
      properties: { value: currentValue || "" },
      styles: {
        flex: "1",
        minHeight: "200px",
        padding: "12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        resize: "vertical",
        fontSize: "13px",
        lineHeight: "1.6",
        fontFamily: "inherit",
        backgroundColor: "var(--background-secondary)",
        display: isEditMode ? "block" : "none",
      },
    }) as HTMLTextAreaElement;

    contentContainer.appendChild(previewArea);
    contentContainer.appendChild(contentArea);

    // Toggle handlers
    const updateModeStyles = () => {
      previewBtn.style.backgroundColor = !isEditMode
        ? "var(--highlight-primary)"
        : "var(--background-secondary)";
      previewBtn.style.color = !isEditMode
        ? "var(--highlight-text)"
        : "var(--text-primary)";
      editBtn.style.backgroundColor = isEditMode
        ? "var(--highlight-primary)"
        : "var(--background-secondary)";
      editBtn.style.color = isEditMode
        ? "var(--highlight-text)"
        : "var(--text-primary)";
      previewArea.style.display = isEditMode ? "none" : "block";
      contentArea.style.display = isEditMode ? "block" : "none";
    };

    previewBtn.addEventListener("click", () => {
      // Update preview with current textarea content before switching
      previewArea.innerHTML = contentArea.value
        ? parseMarkdown(contentArea.value)
        : '<span style="color: var(--text-tertiary); font-style: italic;">No content yet</span>';
      isEditMode = false;
      updateModeStyles();
    });

    editBtn.addEventListener("click", () => {
      isEditMode = true;
      updateModeStyles();
      contentArea.focus();
    });
    dialog.appendChild(contentContainer);

    // Button row
    const buttonRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "8px",
        justifyContent: "flex-end",
        flexWrap: "wrap",
      },
    });

    // Generate/Search buttons (only for computed columns)
    if (isComputed) {
      // Generate button if we have notes or PDF
      if (hasNotes || hasPDF) {
        const genBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: {
            innerText: hasNotes
              ? "⚡ Generate from Notes"
              : "📄 Generate from PDF",
          },
          styles: {
            padding: "10px 16px",
            border: "none",
            borderRadius: "6px",
            backgroundColor: "var(--highlight-primary)",
            color: "var(--highlight-text)",
            cursor: "pointer",
            fontWeight: "500",
          },
          listeners: [
            {
              type: "click",
              listener: async () => {
                genBtn.innerText = "⏳ Generating...";
                (genBtn as HTMLButtonElement).disabled = true;
                try {
                  const content = hasNotes
                    ? await this.generateColumnContent(item!, col, notes)
                    : await this.generateFromPDF(item!, col);
                  contentArea.value = content || "(No content generated)";
                  previewArea.innerHTML = content
                    ? parseMarkdown(content)
                    : '<span style="color: var(--text-tertiary); font-style: italic;">No content generated</span>';
                } catch (e) {
                  contentArea.value = `Error: ${e}`;
                  previewArea.innerHTML = `<span style="color: #c62828;">Error: ${e}</span>`;
                }
                genBtn.innerText = hasNotes ? "⚡ Regenerate" : "📄 Regenerate";
                (genBtn as HTMLButtonElement).disabled = false;
              },
            },
          ],
        });
        buttonRow.appendChild(genBtn);
      }

      // Search PDF button if no PDF (even if notes exist)
      if (!hasPDF) {
        const searchPdfBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "🔍 Search PDF" },
          attributes: { title: "Search for PDF to attach" },
          styles: {
            padding: "10px 16px",
            border: hasNotes ? "1px solid var(--border-primary)" : "none",
            borderRadius: "6px",
            backgroundColor: hasNotes
              ? "var(--background-secondary)"
              : "var(--highlight-primary)",
            color: hasNotes ? "var(--text-primary)" : "var(--highlight-text)",
            cursor: "pointer",
            fontWeight: "500",
          },
          listeners: [
            {
              type: "click",
              listener: async () => {
                searchPdfBtn.innerText = "⏳ Searching...";
                (searchPdfBtn as HTMLButtonElement).disabled = true;
                try {
                  const success = await findAndAttachPdfForItem(
                    item!,
                    (step) => {
                      searchPdfBtn.innerText = `⏳ ${step}`;
                    },
                  );
                  if (success) {
                    if (!hasNotes) {
                      searchPdfBtn.replaceWith(
                        ztoolkit.UI.createElement(doc, "button", {
                          namespace: "html",
                          properties: { innerText: "⚡ Generate from PDF" },
                          styles: {
                            padding: "10px 16px",
                            border: "none",
                            borderRadius: "6px",
                            backgroundColor: "var(--highlight-primary)",
                            color: "var(--highlight-text)",
                            cursor: "pointer",
                            fontWeight: "500",
                          },
                          listeners: [
                            {
                              type: "click",
                              listener: async () => {
                                const btn = buttonRow.querySelector(
                                  "button",
                                ) as HTMLButtonElement;
                                btn.innerText = "⏳ Generating...";
                                btn.disabled = true;
                                try {
                                  const content = await this.generateFromPDF(
                                    item!,
                                    col,
                                  );
                                  contentArea.value =
                                    content || "(No content generated)";
                                  previewArea.innerHTML = content
                                    ? parseMarkdown(content)
                                    : '<span style="color: var(--text-tertiary); font-style: italic;">No content generated</span>';
                                } catch (e) {
                                  contentArea.value = `Error: ${e}`;
                                  previewArea.innerHTML = `<span style="color: #c62828;">Error: ${e}</span>`;
                                }
                                btn.innerText = "📄 Regenerate";
                                btn.disabled = false;
                              },
                            },
                          ],
                        }),
                      );
                    } else {
                      searchPdfBtn.innerText = "✓ PDF Attached";
                      searchPdfBtn.style.backgroundColor =
                        "var(--background-secondary)";
                      searchPdfBtn.style.color = "var(--text-secondary)";
                      searchPdfBtn.disabled = true;
                    }
                  } else {
                    const doi = (item!.getField("DOI") as string) || undefined;
                    const arxivId = extractArxivFromItem(item!);
                    const pmid = extractPmidFromItem(item!);
                    const itemUrl =
                      (item!.getField("url") as string) || undefined;
                    const sourceLink = getSourceLinkForPaper(
                      doi,
                      arxivId,
                      pmid,
                      undefined,
                      itemUrl,
                    );
                    if (sourceLink) {
                      searchPdfBtn.replaceWith(
                        ztoolkit.UI.createElement(doc, "a", {
                          properties: {
                            innerText: "🔗 Open Source",
                            href: sourceLink,
                            target: "_blank",
                          },
                          styles: {
                            padding: "10px 16px",
                            border: "1px solid var(--border-primary)",
                            borderRadius: "6px",
                            backgroundColor: "var(--background-secondary)",
                            color: "var(--highlight-primary)",
                            textDecoration: "none",
                            fontWeight: "500",
                            display: "inline-block",
                          },
                        }),
                      );
                    } else {
                      searchPdfBtn.innerText = "❌ Not found";
                      searchPdfBtn.style.backgroundColor =
                        "var(--background-secondary)";
                      searchPdfBtn.style.color = "var(--text-tertiary)";
                      searchPdfBtn.disabled = false;
                    }
                  }
                } catch (e) {
                  searchPdfBtn.innerText = `Error`;
                  searchPdfBtn.style.backgroundColor =
                    "var(--background-secondary)";
                  searchPdfBtn.style.color = "#c62828";
                }
              },
            },
          ],
        });
        buttonRow.appendChild(searchPdfBtn);
      }
    }

    // Copy button
    const copyBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📋 Copy" },
      styles: {
        padding: "10px 16px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            this.copyToClipboard(contentArea.value, copyBtn);
          },
        },
      ],
    });
    buttonRow.appendChild(copyBtn);

    // Save as Note button
    const saveAsNoteBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📝 Save as Note" },
      attributes: { title: "Save content as a new note" },
      styles: {
        padding: "10px 16px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const originalText = saveAsNoteBtn.innerText;
            saveAsNoteBtn.innerText = "⏳ Saving...";
            saveAsNoteBtn.style.cursor = "wait";

            try {
              const item = Zotero.Items.get(row.paperId);
              if (item) {
                const note = new Zotero.Item("note");
                note.libraryID = item.libraryID;
                note.parentID = item.id;
                const noteContent = `<h1>${col.name}</h1>\n${parseMarkdown(contentArea.value)}`;
                note.setNote(noteContent);
                await note.saveTx();

                saveAsNoteBtn.innerText = "✓ Saved";
                setTimeout(() => {
                  saveAsNoteBtn.innerText = originalText;
                  saveAsNoteBtn.style.cursor = "pointer";
                }, 2000);
              } else {
                throw new Error("Item not found");
              }
            } catch (e) {
              saveAsNoteBtn.innerText = "❌ Error";
              Zotero.debug(`[seerai] Error saving as note: ${e}`);
              setTimeout(() => {
                saveAsNoteBtn.innerText = originalText;
                saveAsNoteBtn.style.cursor = "pointer";
              }, 2000);
            }
          },
        },
      ],
    });
    buttonRow.appendChild(saveAsNoteBtn);

    // Save button
    const saveBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "💾 Save" },
      styles: {
        padding: "10px 16px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const value = contentArea.value;
            row.data[col.id] = value;

            // Also save to generatedData for persistence
            if (currentTableConfig) {
              if (!currentTableConfig.generatedData) {
                currentTableConfig.generatedData = {};
              }
              if (!currentTableConfig.generatedData[row.paperId]) {
                currentTableConfig.generatedData[row.paperId] = {};
              }
              currentTableConfig.generatedData[row.paperId][col.id] = value;

              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
            }

            overlay.remove();
            // Refresh table
            if (currentContainer && currentItem) {
              this.renderInterface(currentContainer, currentItem);
            }
          },
        },
      ],
    });
    buttonRow.appendChild(saveBtn);

    // Cancel button
    const cancelBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Cancel" },
      styles: {
        padding: "10px 16px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-primary)",
        cursor: "pointer",
      },
      listeners: [{ type: "click", listener: () => overlay.remove() }],
    });
    buttonRow.appendChild(cancelBtn);

    dialog.appendChild(buttonRow);
    overlay.appendChild(dialog);

    overlay.addEventListener("click", (e) => {
      if (e.target === overlay) overlay.remove();
    });

    if (doc.body) {
      doc.body.appendChild(overlay);
    } else {
      (doc.documentElement || doc).appendChild(overlay);
    }
  }

  /**
   * Show cell detail modal for search results (Analysis Columns)
   */
  private static showSearchCellDetailModal(
    doc: Document,
    paper: SemanticScholarPaper,
    col: SearchAnalysisColumn,
    currentValue: string,
    resultsContainer: HTMLElement,
    item: Zotero.Item,
  ): void {
    const existing = doc.getElementById("search-cell-detail-modal");
    if (existing) existing.remove();

    const win = doc.defaultView;
    const isDarkMode =
      (win as any)?.matchMedia?.("(prefers-color-scheme: dark)").matches ??
      false;

    const overlay = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "search-cell-detail-modal" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        width: "100%",
        height: "100%",
        backgroundColor: "rgba(0,0,0,0.5)",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        zIndex: "10000",
      },
    });

    const dialog = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        backgroundColor: `var(--background-primary, ${isDarkMode ? "#333" : "#fafafa"})`,
        color: `var(--text-primary, ${isDarkMode ? "#eee" : "#212121"})`,
        borderRadius: "12px",
        padding: "20px",
        maxWidth: "600px",
        width: "90%",
        maxHeight: "70vh",
        display: "flex",
        flexDirection: "column",
        gap: "16px",
        boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
      },
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      },
    });
    const title = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: col.name },
      styles: { fontSize: "16px", fontWeight: "600" },
    });
    header.appendChild(title);

    const closeX = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "none",
        border: "none",
        fontSize: "18px",
        cursor: "pointer",
        color: "var(--text-secondary)",
      },
      listeners: [{ type: "click", listener: () => overlay.remove() }],
    });
    header.appendChild(closeX);
    dialog.appendChild(header);

    // Paper info
    const paperInfo = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: paper.title },
      styles: {
        fontSize: "13px",
        color: "var(--text-secondary)",
        fontStyle: "italic",
      },
    });
    dialog.appendChild(paperInfo);

    // Mode toggle
    let isEditMode = !currentValue;
    const modeToggle = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "4px", marginBottom: "8px" },
    });

    const previewBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "👁 Preview" },
      styles: {
        padding: "6px 12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: !isEditMode
          ? "var(--highlight-primary)"
          : "var(--background-secondary)",
        color: !isEditMode ? "var(--highlight-text)" : "var(--text-primary)",
        cursor: "pointer",
        fontSize: "12px",
      },
    });

    const editBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✏️ Edit" },
      styles: {
        padding: "6px 12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: isEditMode
          ? "var(--highlight-primary)"
          : "var(--background-secondary)",
        color: isEditMode ? "var(--highlight-text)" : "var(--text-primary)",
        cursor: "pointer",
        fontSize: "12px",
      },
    });

    modeToggle.appendChild(previewBtn);
    modeToggle.appendChild(editBtn);
    dialog.appendChild(modeToggle);

    // Content container
    const contentContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        flex: "1",
        minHeight: "200px",
        display: "flex",
        flexDirection: "column",
      },
    });

    const previewArea = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        flex: "1",
        minHeight: "200px",
        padding: "12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        fontSize: "13px",
        lineHeight: "1.6",
        backgroundColor: "var(--background-secondary)",
        overflowY: "auto",
        display: isEditMode ? "none" : "block",
      },
    });
    previewArea.innerHTML = currentValue
      ? parseMarkdown(currentValue)
      : '<span style="color: var(--text-tertiary); font-style: italic;">No content yet</span>';

    const contentArea = ztoolkit.UI.createElement(doc, "textarea", {
      properties: { value: currentValue || "" },
      styles: {
        flex: "1",
        minHeight: "200px",
        padding: "12px",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        resize: "vertical",
        fontSize: "13px",
        lineHeight: "1.6",
        fontFamily: "inherit",
        backgroundColor: "var(--background-secondary)",
        display: isEditMode ? "block" : "none",
      },
    }) as HTMLTextAreaElement;

    contentContainer.appendChild(previewArea);
    contentContainer.appendChild(contentArea);
    dialog.appendChild(contentContainer);

    // Toggle handlers
    const updateModeStyles = () => {
      previewBtn.style.backgroundColor = !isEditMode
        ? "var(--highlight-primary)"
        : "var(--background-secondary)";
      previewBtn.style.color = !isEditMode
        ? "var(--highlight-text)"
        : "var(--text-primary)";
      editBtn.style.backgroundColor = isEditMode
        ? "var(--highlight-primary)"
        : "var(--background-secondary)";
      editBtn.style.color = isEditMode
        ? "var(--highlight-text)"
        : "var(--text-primary)";
      previewArea.style.display = isEditMode ? "none" : "block";
      contentArea.style.display = isEditMode ? "block" : "none";
    };

    previewBtn.addEventListener("click", () => {
      previewArea.innerHTML = contentArea.value
        ? parseMarkdown(contentArea.value)
        : '<span style="color: var(--text-tertiary); font-style: italic;">No content yet</span>';
      isEditMode = false;
      updateModeStyles();
    });

    editBtn.addEventListener("click", () => {
      isEditMode = true;
      updateModeStyles();
      contentArea.focus();
    });

    // Button row
    const buttonRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "8px",
        justifyContent: "flex-end",
      },
    });

    // Copy button
    const copyBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📋 Copy" },
      styles: {
        padding: "10px 16px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            this.copyToClipboard(contentArea.value, copyBtn);
          },
        },
      ],
    });
    buttonRow.appendChild(copyBtn);

    // Save button
    const saveBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "💾 Save" },
      styles: {
        padding: "10px 16px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const value = contentArea.value;

            // Save to generatedData
            if (!searchColumnConfig.generatedData[paper.paperId]) {
              searchColumnConfig.generatedData[paper.paperId] = {};
            }
            searchColumnConfig.generatedData[paper.paperId][col.id] = value;
            await saveSearchColumnConfig();

            overlay.remove();
            // Refresh search results to show updated content
            if (resultsContainer && item) {
              this.renderSearchResults(doc, resultsContainer, item);
            }
          },
        },
      ],
    });
    buttonRow.appendChild(saveBtn);

    // Cancel button
    const cancelBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Cancel" },
      styles: {
        padding: "10px 16px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-primary)",
        cursor: "pointer",
      },
      listeners: [{ type: "click", listener: () => overlay.remove() }],
    });
    buttonRow.appendChild(cancelBtn);

    dialog.appendChild(buttonRow);
    overlay.appendChild(dialog);

    overlay.addEventListener("click", (e) => {
      if (e.target === overlay) overlay.remove();
    });

    if (doc.body) {
      doc.body.appendChild(overlay);
    } else {
      (doc.documentElement || doc).appendChild(overlay);
    }
  }

  /**
   * Generate content from PDF using indexed text or DataLabs OCR as fallback
   * Priority: Notes → Indexed PDF text → OCR extraction
   */
  private static async generateFromPDF(
    item: Zotero.Item,
    col: TableColumn,
  ): Promise<string> {
    // Check if there's already a note with matching title (prioritize OCR notes)
    if (ocrService.hasExistingNote(item)) {
      const noteIds = item.getNotes();
      if (noteIds.length > 0) {
        Zotero.debug(`[seerai] Using existing note for item ${item.id}`);
        return this.generateColumnContent(item, col, noteIds);
      }
    }

    // Try indexed PDF text (auto-indexes if needed, skips note check since we already did it)
    const pdfText = await Assistant.getPdfTextForItem(item, 0, true, false);
    if (pdfText) {
      Zotero.debug(
        `[seerai] Using indexed PDF text for generation (${pdfText.length} chars)`,
      );
      return this.generateColumnContentFromText(item, col, pdfText);
    }

    // PDF not available or image-only - need OCR via DataLabs
    const pdf = ocrService.getFirstPdfAttachment(item);
    if (!pdf) {
      throw new Error("No PDF attachment found");
    }

    // Process PDF with DataLabs - this creates a note
    Zotero.debug("[seerai] Image-only PDF, processing with OCR...");
    await ocrService.convertToMarkdown(pdf);

    // Wait a moment for the note to be saved
    await new Promise((r) => setTimeout(r, 500));

    // Get the newly created note IDs
    const newNoteIds = item.getNotes();
    if (newNoteIds.length === 0) {
      throw new Error("DataLabs processing completed but no note was created");
    }

    Zotero.debug(
      `[seerai] DataLabs created note, now generating content with ${newNoteIds.length} notes`,
    );

    // Now generate content using the new notes
    return this.generateColumnContent(item, col, newNoteIds);
  }

  /**
   * Save current workspace to history
   */
  private static async saveWorkspaceToHistory(doc: Document): Promise<void> {
    try {
      if (!currentTableConfig) return;

      // Give the workspace a name if it doesn't have one
      const paperCount = currentTableConfig.addedPaperIds?.length || 0;
      if (
        currentTableConfig.name === "Default Table" ||
        !currentTableConfig.name
      ) {
        currentTableConfig.name = `Workspace (${paperCount} papers) - ${new Date().toLocaleDateString()}`;
      }

      const tableStore = getTableStore();
      await tableStore.saveConfig(currentTableConfig);
      Zotero.debug(`[seerai] Workspace saved with ${paperCount} papers`);
    } catch (e) {
      Zotero.debug(`[seerai] Error saving workspace: ${e}`);
    }
  }

  /**
   * Show table history/workspace picker with renaming support
   */
  private static async showWorkspacePicker(
    doc: Document,
    item: Zotero.Item,
  ): Promise<void> {
    // Toggle existing dropdown
    const existing = doc.getElementById(
      "workspace-picker-dropdown",
    ) as HTMLElement;
    if (existing) {
      existing.style.opacity = "0";
      existing.style.transform = "translateY(-10px)";
      setTimeout(() => existing.remove(), 200);
      return;
    }

    const toolbar = doc.querySelector(".table-toolbar") as HTMLElement;
    if (!toolbar || !toolbar.parentNode) return;

    // Create dropdown - match Add Papers style with inline positioning
    const dropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "workspace-picker-dropdown" },
      styles: {
        backgroundColor: "var(--background-primary)",
        borderRadius: "8px",
        padding: "0",
        boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
        overflow: "hidden",
        border: "1px solid var(--border-primary)",
        transition: "all 0.2s ease-out",
        opacity: "0",
        transform: "translateY(-10px)",
        marginTop: "8px",
        marginLeft: "8px",
        marginRight: "8px",
      },
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        background:
          "linear-gradient(135deg, var(--highlight-primary) 0%, color-mix(in srgb, var(--highlight-primary) 80%, black) 100%)",
        padding: "10px 14px",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        borderRadius: "8px 8px 0 0",
      },
    });

    const headerTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "📂 Saved Workspaces" },
      styles: {
        fontSize: "13px",
        fontWeight: "600",
        color: "var(--highlight-text)",
        textShadow: "0 1px 2px rgba(0,0,0,0.1)",
      },
    });
    header.appendChild(headerTitle);

    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "rgba(0,0,0,0.1)",
        border: "none",
        borderRadius: "50%",
        width: "22px",
        height: "22px",
        cursor: "pointer",
        color: "var(--highlight-text)",
        fontSize: "11px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            dropdown.style.opacity = "0";
            setTimeout(() => dropdown.remove(), 200);
          },
        },
      ],
    });
    header.appendChild(closeBtn);
    dropdown.appendChild(header);

    // Content
    const content = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "8px",
        display: "flex",
        flexDirection: "column",
        gap: "8px",
      },
    });

    const listContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        maxHeight: "240px",
        overflowY: "auto",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
      },
    });

    // Load history
    const tableStore = getTableStore();
    const history = await tableStore.loadHistory();

    const renderList = async () => {
      listContainer.innerHTML = "";
      if (history.entries.length === 0) {
        listContainer.appendChild(
          ztoolkit.UI.createElement(doc, "div", {
            properties: { innerText: "No saved workspaces." },
            styles: {
              padding: "16px",
              textAlign: "center",
              color: "var(--text-secondary)",
              fontSize: "12px",
            },
          }),
        );
      } else {
        for (const entry of history.entries) {
          const isActive =
            currentTableConfig && currentTableConfig.id === entry.config.id;
          const row = ztoolkit.UI.createElement(doc, "div", {
            styles: {
              padding: "8px 10px",
              borderBottom: "1px solid var(--border-primary)",
              cursor: "pointer",
              display: "flex",
              justifyContent: "space-between",
              alignItems: "center",
              backgroundColor: isActive
                ? "var(--background-hover)"
                : "transparent",
            },
          });

          // Left side: Name and meta
          const info = ztoolkit.UI.createElement(doc, "div", {
            styles: { flex: "1", overflow: "hidden", marginRight: "8px" },
            listeners: [
              {
                type: "click",
                listener: async () => {
                  // Load workspace
                  currentTableConfig = { ...entry.config };
                  entry.usedAt = new Date().toISOString();
                  await tableStore.saveHistory(history); // Update last used
                  await tableStore.saveConfig(currentTableConfig); // Set as current

                  dropdown.style.opacity = "0";
                  setTimeout(() => dropdown.remove(), 200);

                  if (currentContainer && currentItem) {
                    this.renderInterface(currentContainer, currentItem);
                  }
                },
              },
            ],
          });

          const nameEl = ztoolkit.UI.createElement(doc, "div", {
            properties: { innerText: entry.config.name || "Untitled Workpace" },
            styles: {
              fontSize: "12px",
              fontWeight: isActive ? "600" : "500",
              color: "var(--text-primary)",
            },
          });

          const metaEl = ztoolkit.UI.createElement(doc, "div", {
            properties: {
              innerText: `${new Date(entry.usedAt).toLocaleDateString()} • ${entry.config.addedPaperIds?.length || 0} papers`,
            },
            styles: {
              fontSize: "10px",
              color: "var(--text-secondary)",
              marginTop: "2px",
            },
          });

          info.appendChild(nameEl);
          info.appendChild(metaEl);
          row.appendChild(info);

          // Actions
          const actions = ztoolkit.UI.createElement(doc, "div", {
            styles: { display: "flex", gap: "4px" },
          });

          // Rename button
          const renameBtn = ztoolkit.UI.createElement(doc, "button", {
            namespace: "html",
            properties: { innerText: "✏️", title: "Rename" },
            styles: {
              background: "none",
              border: "none",
              cursor: "pointer",
              fontSize: "12px",
              opacity: "0.6",
              padding: "2px",
            },
            listeners: [
              {
                type: "click",
                listener: async (e: Event) => {
                  e.stopPropagation();
                  const newName = doc.defaultView?.prompt(
                    "Rename workspace:",
                    entry.config.name,
                  );
                  if (newName) {
                    entry.config.name = newName;
                    await tableStore.saveHistory(history);
                    if (isActive && currentTableConfig) {
                      currentTableConfig.name = newName;
                      await tableStore.saveConfig(currentTableConfig);
                    }
                    renderList(); // Re-render list
                  }
                },
              },
            ],
          });
          renameBtn.addEventListener(
            "mouseenter",
            () => (renameBtn.style.opacity = "1"),
          );
          renameBtn.addEventListener(
            "mouseleave",
            () => (renameBtn.style.opacity = "0.6"),
          );

          actions.appendChild(renameBtn);

          // Delete button
          const deleteBtn = ztoolkit.UI.createElement(doc, "button", {
            namespace: "html",
            properties: { innerText: "🗑", title: "Delete" },
            styles: {
              background: "none",
              border: "none",
              cursor: "pointer",
              fontSize: "12px",
              color: "#c62828",
              opacity: "0.6",
              padding: "2px",
            },
            listeners: [
              {
                type: "click",
                listener: async (e: Event) => {
                  e.stopPropagation();
                  if (
                    doc.defaultView?.confirm(
                      `Delete workspace "${entry.config.name}"?`,
                    )
                  ) {
                    const idx = history.entries.indexOf(entry);
                    if (idx > -1) {
                      history.entries.splice(idx, 1);
                      await tableStore.saveHistory(history);
                      // If deleted active one, maybe reset? keeping it simple for now
                      renderList();
                    }
                  }
                },
              },
            ],
          });
          deleteBtn.addEventListener(
            "mouseenter",
            () => (deleteBtn.style.opacity = "1"),
          );
          deleteBtn.addEventListener(
            "mouseleave",
            () => (deleteBtn.style.opacity = "0.6"),
          );

          actions.appendChild(deleteBtn);
          row.appendChild(actions);

          // Highlight active
          if (isActive) {
            const check = ztoolkit.UI.createElement(doc, "div", {
              properties: { innerText: "✓" },
              styles: {
                fontSize: "14px",
                color: "var(--highlight-primary)",
                marginRight: "6px",
              },
            });
            row.insertBefore(check, info);
          }

          listContainer.appendChild(row);
        }
      }
    };

    await renderList();
    content.appendChild(listContainer);
    dropdown.appendChild(content);

    // Insert after toolbar in the DOM flow (same as Add Papers dropdown)
    toolbar.parentNode.insertBefore(dropdown, toolbar.nextSibling);

    // Animate in
    setTimeout(() => {
      dropdown.style.opacity = "1";
      dropdown.style.transform = "translateY(0)";
    }, 10);
  }

  /**
   * Show picker to add table items to chat - Matched to Add Papers style
   */
  private static async showChatTablePicker(
    doc: Document,
    stateManager: ReturnType<typeof getChatStateManager>,
  ): Promise<void> {
    // Toggle existing dropdown
    const existing = doc.getElementById(
      "chat-table-picker-dropdown",
    ) as HTMLElement;
    if (existing) {
      existing.style.opacity = "0";
      existing.style.transform = "translateY(-10px)";
      setTimeout(() => existing.remove(), 200);
      return;
    }

    const selectionArea = doc.getElementById("selection-area");
    if (!selectionArea || !selectionArea.parentNode) return;

    // Create dropdown panel
    const dropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "chat-table-picker-dropdown" },
      styles: {
        backgroundColor: "var(--background-primary)",
        borderRadius: "8px",
        padding: "0",
        boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
        overflow: "hidden",
        border: "1px solid var(--border-primary)",
        transition: "all 0.2s ease-out",
        opacity: "0",
        transform: "translateY(-10px)",
        marginTop: "8px",
        marginLeft: "8px",
        marginRight: "8px",
      },
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        background:
          "linear-gradient(135deg, var(--highlight-primary) 0%, color-mix(in srgb, var(--highlight-primary) 80%, purple) 100%)",
        padding: "10px 14px",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        gap: "12px",
      },
    });

    const headerTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "📊 Add From Table" },
      styles: {
        fontSize: "13px",
        fontWeight: "600",
        color: "var(--highlight-text)",
        textShadow: "0 1px 2px rgba(0,0,0,0.1)",
      },
    });
    header.appendChild(headerTitle);

    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "rgba(0,0,0,0.1)",
        border: "none",
        borderRadius: "50%",
        width: "22px",
        height: "22px",
        cursor: "pointer",
        color: "var(--highlight-text)",
        fontSize: "11px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            dropdown.style.opacity = "0";
            dropdown.style.transform = "translateY(-10px)";
            setTimeout(() => dropdown.remove(), 200);
          },
        },
      ],
    });
    header.appendChild(closeBtn);
    dropdown.appendChild(header);

    // Content
    const content = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "10px 14px",
        display: "flex",
        flexDirection: "column",
        gap: "8px",
      },
    });

    const listContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        maxHeight: "240px",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
      },
    });

    // Load tables
    const tableStore = getTableStore();
    const history = await tableStore.loadHistory();

    if (history.entries.length === 0) {
      listContainer.appendChild(
        ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: "No saved tables found." },
          styles: {
            padding: "20px",
            textAlign: "center",
            color: "var(--text-secondary)",
            fontSize: "12px",
          },
        }),
      );
    } else {
      for (const entry of history.entries) {
        const row = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "8px 10px",
            borderBottom: "1px solid var(--border-primary)",
            cursor: "pointer",
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            transition: "background-color 0.1s",
          },
        });
        row.addEventListener("mouseenter", () => {
          row.style.backgroundColor = "var(--background-primary)";
        });
        row.addEventListener("mouseleave", () => {
          row.style.backgroundColor = "";
        });

        const info = ztoolkit.UI.createElement(doc, "div", {
          styles: { flex: "1", overflow: "hidden", marginRight: "10px" },
        });

        const nameEl = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: entry.config.name || "Untitled Table" },
          styles: {
            fontSize: "12px",
            fontWeight: "600",
            color: "var(--text-primary)",
          },
        });

        const count = entry.config.addedPaperIds?.length || 0;
        const metaEl = ztoolkit.UI.createElement(doc, "div", {
          properties: {
            innerText: `${count} papers • ${new Date(entry.usedAt).toLocaleDateString()}`,
          },
          styles: {
            fontSize: "11px",
            color: "var(--text-secondary)",
            marginTop: "1px",
          },
        });

        info.appendChild(nameEl);
        info.appendChild(metaEl);
        row.appendChild(info);

        const addBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "Add Table Context" },
          styles: {
            padding: "4px 10px",
            borderRadius: "12px",
            border: "1px solid var(--highlight-primary)",
            backgroundColor: "transparent",
            color: "var(--highlight-primary)",
            fontSize: "11px",
            cursor: "pointer",
            fontWeight: "600",
            transition: "all 0.15s",
          },
          listeners: [
            {
              type: "click",
              listener: async (e: Event) => {
                e.stopPropagation();
                const paperIds = entry.config.addedPaperIds || [];
                if (paperIds.length === 0) {
                  doc.defaultView?.alert("This table is empty.");
                  return;
                }

                // Build table context from the saved table data
                const columns =
                  entry.config.columns?.filter((c) => c.visible) ||
                  defaultColumns.filter((c) => c.visible);
                const columnNames = columns.map((c) => c.name);
                const generatedData = entry.config.generatedData || {};

                // Format table data as text context
                let tableContent = "";
                let rowCount = 0;

                for (const paperId of paperIds) {
                  const item = Zotero.Items.get(paperId);
                  if (!item || !item.isRegularItem()) continue;

                  const paperTitle =
                    (item.getField("title") as string) || "Untitled";
                  const creators = item.getCreators();
                  const authorNames =
                    creators
                      .map((c) =>
                        `${c.firstName || ""} ${c.lastName || ""}`.trim(),
                      )
                      .join(", ") || "Unknown";
                  const year = (item.getField("year") as string) || "";
                  const noteIDs = item.getNotes();
                  const persistedData = generatedData[paperId] || {};

                  // Build row data
                  const rowData: Record<string, string> = {
                    title: paperTitle,
                    author: authorNames,
                    year: year,
                    sources: String(noteIDs.length),
                    ...persistedData,
                  };

                  // Format as readable entry
                  tableContent += `\n### ${paperTitle}\n`;
                  for (const col of columns) {
                    if (col.id === "title") continue; // Title already in header
                    const value = rowData[col.id] || "";
                    if (value) {
                      tableContent += `- **${col.name}**: ${value}\n`;
                    }
                  }
                  rowCount++;
                }

                // Create table selection object
                const tableSelection: SelectedTable = {
                  id: entry.config.id || `table_${Date.now()}`,
                  type: "table",
                  title: entry.config.name || "Untitled Table",
                  content: tableContent,
                  rowCount: rowCount,
                  columnNames: columnNames,
                };

                // Add to state manager
                stateManager.addSelection("tables", tableSelection);
                this.reRenderSelectionArea();

                // Feedback
                addBtn.innerText = "✓ Added";
                addBtn.style.backgroundColor = "var(--highlight-primary)";
                addBtn.style.color = "var(--highlight-text)";
                setTimeout(() => {
                  dropdown.style.opacity = "0";
                  setTimeout(() => dropdown.remove(), 200);
                }, 500);
              },
            },
          ],
        });
        addBtn.addEventListener("mouseenter", () => {
          addBtn.style.backgroundColor = "var(--highlight-primary)";
          addBtn.style.color = "var(--highlight-text)";
        });
        addBtn.addEventListener("mouseleave", () => {
          if (addBtn.innerText !== "✓ Added") {
            addBtn.style.backgroundColor = "transparent";
            addBtn.style.color = "var(--highlight-primary)";
          }
        });

        row.appendChild(addBtn);
        listContainer.appendChild(row);
      }
    }

    content.appendChild(listContainer);
    dropdown.appendChild(content);

    // Done button logic (optional, users can just close)
    const buttonRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "8px 14px",
        borderTop: "1px solid var(--border-primary)",
        display: "flex",
        justifyContent: "flex-end",
      },
    });
    const doneBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Done" },
      styles: {
        padding: "6px 14px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            dropdown.style.opacity = "0";
            dropdown.style.transform = "translateY(-10px)";
            setTimeout(() => dropdown.remove(), 200);
          },
        },
      ],
    });
    buttonRow.appendChild(doneBtn);
    dropdown.appendChild(buttonRow);

    selectionArea.parentNode.insertBefore(dropdown, selectionArea.nextSibling);

    // Animate in
    setTimeout(() => {
      dropdown.style.opacity = "1";
      dropdown.style.transform = "translateY(0)";
    }, 10);
  }

  /**
   * Start a fresh workspace
   */
  private static async startFreshWorkspace(
    doc: Document,
    item: Zotero.Item,
  ): Promise<void> {
    try {
      const tableStore = getTableStore();
      currentTableConfig = await tableStore.createFreshConfig();
      if (currentContainer && currentItem) {
        this.renderInterface(currentContainer, currentItem);
      }
      Zotero.debug("[seerai] Fresh workspace started");
    } catch (e) {
      Zotero.debug(`[seerai] Error starting fresh workspace: ${e}`);
    }
  }

  /**
   * Create response length slider control
   */
  private static createResponseLengthControl(doc: Document): HTMLElement {
    const container = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "response-length-container" },
      styles: {
        display: "flex",
        alignItems: "center",
        gap: "8px",
        padding: "4px 8px",
        backgroundColor: "var(--background-tertiary)",
        borderRadius: "4px",
      },
    });

    const label = ztoolkit.UI.createElement(doc, "span", {
      properties: {
        className: "response-length-label",
        innerText: "Response:",
      },
      styles: {
        fontSize: "11px",
        color: "var(--text-secondary)",
        whiteSpace: "nowrap",
      },
    });

    const slider = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "range", min: "0", max: "4200", step: "100" },
      properties: { className: "response-length-slider" },
      styles: { width: "80px", cursor: "pointer" },
    }) as HTMLInputElement;

    slider.value = String(currentTableConfig?.responseLength || 100);

    const getDisplayValue = (val: string) => {
      const num = parseInt(val, 10);
      return num >= 4192 ? "∞" : val;
    };

    const valueDisplay = ztoolkit.UI.createElement(doc, "span", {
      properties: {
        className: "response-length-value",
        innerText: getDisplayValue(slider.value),
      },
      styles: {
        fontSize: "11px",
        color: "var(--text-primary)",
        minWidth: "30px",
      },
    });

    slider.addEventListener("input", async () => {
      const val = parseInt(slider.value, 10);
      valueDisplay.textContent = getDisplayValue(slider.value);
      if (currentTableConfig) {
        // 4192+ means unlimited (store as 0)
        currentTableConfig.responseLength = val >= 4192 ? 0 : val;
        const tableStore = getTableStore();
        await tableStore.saveConfig(currentTableConfig);
      }
    });

    container.appendChild(label);
    container.appendChild(slider);
    container.appendChild(valueDisplay);

    return container;
  }

  /**
   * Create empty state for table
   */
  private static createTableEmptyState(
    doc: Document,
    item: Zotero.Item,
  ): HTMLElement {
    const emptyState = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "table-empty-state" },
      styles: {
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        padding: "40px 20px",
        color: "var(--text-tertiary)",
        textAlign: "center",
        gap: "8px",
      },
    });

    const icon = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "table-empty-state-icon", innerText: "📋" },
      styles: { fontSize: "32px", opacity: "0.5" },
    });

    const text = ztoolkit.UI.createElement(doc, "div", {
      properties: {
        className: "table-empty-state-text",
        innerText: "Start by adding papers to create a comparison table.",
      },
      styles: { fontSize: "13px" },
    });
    emptyState.appendChild(icon);
    emptyState.appendChild(text);

    // Add Button
    const addBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "➕ Add Papers" },
      styles: {
        marginTop: "12px",
        padding: "8px 16px",
        fontSize: "13px",
        border: "none",
        borderRadius: "6px",
        backgroundColor: "var(--highlight-primary)",
        color: "var(--highlight-text)",
        cursor: "pointer",
        fontWeight: "500",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            this.showTablePaperPicker(doc, item);
          },
        },
      ],
    });
    emptyState.appendChild(addBtn);

    return emptyState;
  }

  /**
   * Create the papers table element
   */
  private static createPapersTable(
    doc: Document,
    tableData: TableData,
  ): HTMLElement {
    const table = ztoolkit.UI.createElement(doc, "table", {
      properties: { className: "papers-table" },
      styles: {
        width: "100%",
        borderCollapse: "collapse",
        fontSize: "12px",
        tableLayout: "fixed",
      },
    });

    // Create header
    const thead = ztoolkit.UI.createElement(doc, "thead", {});
    const headerRow = ztoolkit.UI.createElement(doc, "tr", {});

    const columns = currentTableConfig?.columns || defaultColumns;

    // Core columns to combine into "Paper"
    const coreColumnIds = [
      "title",
      "author",
      "year",
      "sources",
      "zoteroKey",
      "DOI",
    ];
    const otherColumns = columns.filter(
      (col) => col.visible && !coreColumnIds.includes(col.id),
    );

    // Paper column width (stored in config or default to 280)
    let paperColumnWidth = (currentTableConfig as any)?.paperColumnWidth ?? 280;

    // Calculate total width of all columns to convert to percentages.
    // This ensures columns always fill exactly 100% of the available space
    // and shrink proportionally when the panel narrows, regardless of
    // how wide the user has manually resized individual columns.
    const actionsColumnWidth = 70;
    const totalColumnWidth =
      paperColumnWidth +
      otherColumns.reduce((sum, col) => sum + col.width, 0) +
      actionsColumnWidth;
    const paperPct = ((paperColumnWidth / totalColumnWidth) * 100).toFixed(2);

    // Add combined "Paper" header (for title, author, year, sources)
    const paperHeader = ztoolkit.UI.createElement(doc, "th", {
      properties: { innerText: "Paper", className: "sortable" },
      styles: {
        position: "relative",
        backgroundColor: "var(--background-secondary)",
        borderBottom: "1px solid rgba(128, 128, 128, 0.4)",
        borderRight: "1px solid rgba(128, 128, 128, 0.4)",
        padding: "8px 10px",
        textAlign: "left",
        fontWeight: "600",
        width: `${paperPct}%`,
        minWidth: "40px",
        cursor: "pointer",
        userSelect: "none",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            if (currentTableConfig) {
              if (currentTableConfig.sortBy === "title") {
                currentTableConfig.sortOrder =
                  currentTableConfig.sortOrder === "asc" ? "desc" : "asc";
              } else {
                currentTableConfig.sortBy = "title";
                currentTableConfig.sortOrder = "asc";
              }
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
              if (currentContainer && currentItem) {
                this.renderInterface(currentContainer, currentItem);
              }
            }
          },
        },
      ],
    });

    // Add resize handle for Paper column
    const paperResizeHandle = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "column-resize-handle" },
      styles: {
        position: "absolute",
        right: "0",
        top: "0",
        bottom: "0",
        width: "6px",
        cursor: "col-resize",
        backgroundColor: "transparent",
      },
    });
    paperResizeHandle.addEventListener("mouseenter", () => {
      (paperResizeHandle as HTMLElement).style.backgroundColor =
        "var(--highlight-primary)";
    });
    paperResizeHandle.addEventListener("mouseleave", () => {
      (paperResizeHandle as HTMLElement).style.backgroundColor = "transparent";
    });
    paperResizeHandle.addEventListener("mousedown", (e: MouseEvent) => {
      e.preventDefault();
      e.stopPropagation();

      const startX = e.clientX;
      const startWidth = paperColumnWidth;

      const onMouseMove = (moveE: MouseEvent) => {
        const delta = moveE.clientX - startX;
        const newWidth = Math.max(40, startWidth + delta);
        paperColumnWidth = newWidth;

        // Recalculate all column percentages so the table always fits 100%
        const newTotal =
          newWidth +
          otherColumns.reduce((sum, col) => sum + col.width, 0) +
          actionsColumnWidth;
        const newPaperPct = ((newWidth / newTotal) * 100).toFixed(2);

        // Update header width
        (paperHeader as HTMLElement).style.width = `${newPaperPct}%`;

        // Update all Paper cells (first column)
        const cells = table.querySelectorAll(
          `td:nth-child(1), th:nth-child(1)`,
        );
        cells.forEach((cell: Element) => {
          (cell as HTMLElement).style.width = `${newPaperPct}%`;
        });

        // Update other column percentages
        otherColumns.forEach((col, idx) => {
          const colPct = ((col.width / newTotal) * 100).toFixed(2);
          const colCells = table.querySelectorAll(
            `td:nth-child(${idx + 2}), th:nth-child(${idx + 2})`,
          );
          colCells.forEach((cell: Element) => {
            (cell as HTMLElement).style.width = `${colPct}%`;
            (cell as HTMLElement).style.maxWidth = `${colPct}%`;
          });
        });

        // Update actions column percentage
        const actionsPct = ((actionsColumnWidth / newTotal) * 100).toFixed(2);
        const lastColIdx = otherColumns.length + 2;
        const actionsCells = table.querySelectorAll(
          `td:nth-child(${lastColIdx}), th:nth-child(${lastColIdx})`,
        );
        actionsCells.forEach((cell: Element) => {
          (cell as HTMLElement).style.width = `${actionsPct}%`;
        });
      };

      const onMouseUp = async () => {
        doc.removeEventListener("mousemove", onMouseMove);
        doc.removeEventListener("mouseup", onMouseUp);

        // Save Paper column width
        if (currentTableConfig) {
          (currentTableConfig as any).paperColumnWidth = paperColumnWidth;
          const tableStore = getTableStore();
          await tableStore.saveConfig(currentTableConfig);
        }
      };

      doc.addEventListener("mousemove", onMouseMove);
      doc.addEventListener("mouseup", onMouseUp);
    });
    paperHeader.appendChild(paperResizeHandle);
    headerRow.appendChild(paperHeader);

    // Add other column headers (computed/custom columns)
    otherColumns.forEach((col) => {
      const isComputedColumn = col.type === "computed";
      const th = ztoolkit.UI.createElement(doc, "th", {
        properties: {
          className: `${col.sortable ? "sortable" : ""} ${currentTableConfig?.sortBy === col.id ? `sort-${currentTableConfig.sortOrder}` : ""}`,
        },
        attributes: isComputedColumn ? { "data-column-id": col.id } : {},
        styles: {
          position: "relative",
          top: "0",
          backgroundColor: "var(--background-secondary)",
          borderBottom: "1px solid rgba(128, 128, 128, 0.4)",
          borderRight: "1px solid rgba(128, 128, 128, 0.4)",
          padding: "8px 10px",
          textAlign: "left",
          fontWeight: "600",
          width: `${((col.width / totalColumnWidth) * 100).toFixed(2)}%`,
          minWidth: "40px",
          cursor: isComputedColumn
            ? "pointer"
            : col.sortable
              ? "pointer"
              : "default",
          userSelect: "none",
          whiteSpace: "nowrap",
          overflow: "hidden",
          textOverflow: "ellipsis",
        },
      });

      // For computed columns, wrap content with edit capability
      if (isComputedColumn) {
        const headerContent = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            gap: "4px",
            width: "100%",
          },
        });

        const nameSpan = ztoolkit.UI.createElement(doc, "span", {
          properties: { innerText: col.name, className: "column-header-text" },
          attributes: { title: col.aiPrompt || "" },
          styles: {
            flex: "1",
            overflow: "hidden",
            textOverflow: "ellipsis",
          },
        });
        headerContent.appendChild(nameSpan);

        // Edit icon (pencil)
        const editIcon = ztoolkit.UI.createElement(doc, "span", {
          properties: { innerHTML: "✎" },
          styles: { fontSize: "11px", opacity: "0.4" },
        });
        headerContent.appendChild(editIcon);

        // Hover effect
        th.addEventListener("mouseenter", () => (editIcon.style.opacity = "1"));
        th.addEventListener(
          "mouseleave",
          () => (editIcon.style.opacity = "0.4"),
        );

        // Click to edit
        th.addEventListener("click", (e: Event) => {
          e.stopPropagation();
          if (currentItem) {
            this.showTableColumnEditPopover(
              doc,
              th,
              col,
              currentItem,
              currentContainer!,
            );
          }
        });

        th.appendChild(headerContent);
      } else {
        // Non-computed columns just show text
        th.innerText = col.name;

        // Sorting for sortable columns
        if (col.sortable) {
          th.addEventListener("click", async () => {
            if (currentTableConfig) {
              if (currentTableConfig.sortBy === col.id) {
                currentTableConfig.sortOrder =
                  currentTableConfig.sortOrder === "asc" ? "desc" : "asc";
              } else {
                currentTableConfig.sortBy = col.id;
                currentTableConfig.sortOrder = "asc";
              }
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
              if (currentContainer && currentItem) {
                this.renderInterface(currentContainer, currentItem);
              }
            }
          });
        }
      }

      // Add resize handle if resizable
      if (col.resizable) {
        const resizeHandle = ztoolkit.UI.createElement(doc, "div", {
          properties: { className: "column-resize-handle" },
          styles: {
            position: "absolute",
            right: "0",
            top: "0",
            bottom: "0",
            width: "6px",
            cursor: "col-resize",
            backgroundColor: "transparent",
          },
        });

        // Hover effect
        resizeHandle.addEventListener("mouseenter", () => {
          resizeHandle.style.backgroundColor = "var(--highlight-primary)";
        });
        resizeHandle.addEventListener("mouseleave", () => {
          resizeHandle.style.backgroundColor = "transparent";
        });

        // Drag to resize
        resizeHandle.addEventListener("mousedown", (e: MouseEvent) => {
          e.preventDefault();
          e.stopPropagation();

          const startX = e.clientX;
          const startWidth = col.width;
          const colIndex = otherColumns.findIndex((c) => c.id === col.id) + 1; // +1 for Paper column

          const onMouseMove = (moveE: MouseEvent) => {
            const delta = moveE.clientX - startX;
            const newWidth = Math.max(col.minWidth, startWidth + delta);
            col.width = newWidth;

            // Recalculate all column percentages so the table always fits 100%
            const newTotal =
              paperColumnWidth +
              otherColumns.reduce((sum, c) => sum + c.width, 0) +
              actionsColumnWidth;
            const colPct = ((newWidth / newTotal) * 100).toFixed(2);

            // Update header width
            th.style.width = `${colPct}%`;

            // Update all cells in this column (+2 because Paper is +1 and nth-child is 1-indexed)
            const cells = table.querySelectorAll(
              `td:nth-child(${colIndex + 1}), th:nth-child(${colIndex + 1})`,
            );
            cells.forEach((cell: Element) => {
              (cell as HTMLElement).style.width = `${colPct}%`;
              (cell as HTMLElement).style.maxWidth = `${colPct}%`;
            });

            // Update paper column percentage
            const paperPctNew = ((paperColumnWidth / newTotal) * 100).toFixed(
              2,
            );
            const paperCells = table.querySelectorAll(
              `td:nth-child(1), th:nth-child(1)`,
            );
            paperCells.forEach((cell: Element) => {
              (cell as HTMLElement).style.width = `${paperPctNew}%`;
            });

            // Update actions column percentage
            const actionsPctNew = (
              (actionsColumnWidth / newTotal) *
              100
            ).toFixed(2);
            const lastColIdx = otherColumns.length + 2;
            const actionsCells = table.querySelectorAll(
              `td:nth-child(${lastColIdx}), th:nth-child(${lastColIdx})`,
            );
            actionsCells.forEach((cell: Element) => {
              (cell as HTMLElement).style.width = `${actionsPctNew}%`;
            });
          };

          const onMouseUp = async () => {
            doc.removeEventListener("mousemove", onMouseMove);
            doc.removeEventListener("mouseup", onMouseUp);

            // Save column widths
            if (currentTableConfig) {
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
            }
          };

          doc.addEventListener("mousemove", onMouseMove);
          doc.addEventListener("mouseup", onMouseUp);
        });

        th.style.position = "relative";
        th.appendChild(resizeHandle);
      }

      headerRow.appendChild(th);
    });

    // Add Actions header cell
    const actionsPct = ((actionsColumnWidth / totalColumnWidth) * 100).toFixed(
      2,
    );
    const actionsHeader = ztoolkit.UI.createElement(doc, "th", {
      properties: { innerText: "Actions" },
      attributes: { title: "Save or remove row" },
      styles: {
        padding: "8px 6px",
        backgroundColor: "var(--background-secondary)",
        borderBottom: "2px solid rgba(128, 128, 128, 0.5)",
        borderRight: "1px solid rgba(128, 128, 128, 0.4)",
        fontSize: "11px",
        fontWeight: "600",
        width: `${actionsPct}%`,
        minWidth: "40px",
        textAlign: "center",
      },
    });
    headerRow.appendChild(actionsHeader);

    thead.appendChild(headerRow);
    table.appendChild(thead);

    // Create body
    const tbody = ztoolkit.UI.createElement(doc, "tbody", {});

    tableData.rows.forEach((row) => {
      const tr = ztoolkit.UI.createElement(doc, "tr", {
        properties: {
          className: tableData.selectedRowIds.has(row.paperId)
            ? "selected"
            : "",
        },
        attributes: { "data-paper-id": String(row.paperId) },
        listeners: [
          {
            type: "click",
            listener: (e: MouseEvent) => {
              const id = row.paperId;
              const newSelection = new Set(tableData.selectedRowIds);

              if (e.shiftKey && lastInteractedRowId) {
                // Range selection
                const startIdx = tableData.rows.findIndex(
                  (r) => r.paperId === lastInteractedRowId,
                );
                const endIdx = tableData.rows.findIndex(
                  (r) => r.paperId === id,
                );
                if (startIdx !== -1 && endIdx !== -1) {
                  const low = Math.min(startIdx, endIdx);
                  const high = Math.max(startIdx, endIdx);
                  for (let i = low; i <= high; i++) {
                    newSelection.add(tableData.rows[i].paperId);
                  }
                }
              } else if (e.ctrlKey || e.metaKey) {
                // Toggle
                if (newSelection.has(id)) {
                  newSelection.delete(id);
                } else {
                  newSelection.add(id);
                }
                lastInteractedRowId = id;
              } else {
                // Single Select
                newSelection.clear();
                newSelection.add(id);
                lastInteractedRowId = id;
              }

              tableData.selectedRowIds = newSelection;

              // Update UI (all rows)
              const allRows = tbody.querySelectorAll("tr[data-paper-id]");
              allRows.forEach((r: Element) => {
                const rId = parseInt(r.getAttribute("data-paper-id") || "0");
                if (tableData.selectedRowIds.has(rId)) {
                  r.classList.add("selected");
                } else {
                  r.classList.remove("selected");
                }
              });

              // Update Bulk Actions Visibility
              const bulkActions = doc.getElementById(
                "table-bulk-actions",
              ) as HTMLElement;
              if (bulkActions) {
                bulkActions.style.display =
                  tableData.selectedRowIds.size > 0 ? "flex" : "none";
              }
            },
          },
        ],
      });

      // Create combined "Paper" cell (title, author, year, sources)
      const paperCell = ztoolkit.UI.createElement(doc, "td", {
        styles: {
          padding: "8px 10px",
          borderBottom: "1px solid rgba(128, 128, 128, 0.4)",
          borderRight: "1px solid rgba(128, 128, 128, 0.4)",
          verticalAlign: "top",
          cursor: "pointer",
          width: `${paperPct}%`,
          minWidth: "40px",
          overflow: "hidden",
        },
      });

      // Title (clickable, opens PDF)
      const titleDiv = ztoolkit.UI.createElement(doc, "div", {
        properties: { innerText: row.data["title"] || "Untitled" },
        styles: {
          fontWeight: "600",
          fontSize: "12px",
          color: "var(--highlight-primary)",
          marginBottom: "3px",
          lineHeight: "1.4",
          whiteSpace: "normal",
          wordBreak: "break-word",
        },
      }) as HTMLDivElement;
      titleDiv.addEventListener("mouseenter", () => {
        titleDiv.style.textDecoration = "underline";
      });
      titleDiv.addEventListener("mouseleave", () => {
        titleDiv.style.textDecoration = "none";
      });
      titleDiv.addEventListener("click", async (event: Event) => {
        const e = event as MouseEvent;
        e.preventDefault();
        e.stopPropagation();

        const id = row.paperId;
        const newSelection = new Set(tableData.selectedRowIds);

        if (e.shiftKey && lastInteractedRowId) {
          // Range selection
          const startIdx = tableData.rows.findIndex(
            (r) => r.paperId === lastInteractedRowId,
          );
          const endIdx = tableData.rows.findIndex((r) => r.paperId === id);
          if (startIdx !== -1 && endIdx !== -1) {
            const low = Math.min(startIdx, endIdx);
            const high = Math.max(startIdx, endIdx);
            for (let i = low; i <= high; i++) {
              newSelection.add(tableData.rows[i].paperId);
            }
          }
        } else if (e.ctrlKey || e.metaKey) {
          // Toggle
          if (newSelection.has(id)) {
            newSelection.delete(id);
          } else {
            newSelection.add(id);
          }
          lastInteractedRowId = id;
        } else {
          // Single Select
          newSelection.clear();
          newSelection.add(id);
          lastInteractedRowId = id;
        }

        tableData.selectedRowIds = newSelection;

        // Update UI (all rows)
        const allRows = table.querySelectorAll("tr[data-paper-id]");
        allRows.forEach((r: Element) => {
          const rId = parseInt(r.getAttribute("data-paper-id") || "0");
          if (tableData.selectedRowIds.has(rId)) {
            r.classList.add("selected");
          } else {
            r.classList.remove("selected");
          }
        });

        // Update Bulk Actions Visibility
        const bulkActions = doc.getElementById(
          "table-bulk-actions",
        ) as HTMLElement;
        if (bulkActions) {
          bulkActions.style.display =
            tableData.selectedRowIds.size > 0 ? "flex" : "none";
        }

        const item = Zotero.Items.get(row.paperId);
        if (!item) return;

        // Interactions (Only if no modifiers)
        if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
          // Single Click: Select item in Zotero
          if (e.detail === 1) {
            const zp = Zotero.getActiveZoteroPane();
            if (zp) zp.selectItem(item.id);
          }
          // Double Click: Open PDF
          else if (e.detail === 2) {
            const attachmentIds = item.getAttachments();
            for (const attachId of attachmentIds) {
              const attachment = Zotero.Items.get(attachId);
              if (
                attachment &&
                attachment.isPDFAttachment &&
                attachment.isPDFAttachment()
              ) {
                await Zotero.Reader.open(attachment.id);
                return;
              }
            }
          }
        }
      });

      paperCell.appendChild(titleDiv);

      // Author, Year, Sources, Zotero Key, DOI on one line
      const author = row.data["author"] || "";
      const year = row.data["year"] || "";
      const sources = row.data["sources"] || "0";
      const zoteroKey = row.data["zoteroKey"] || "";
      const doi = row.data["DOI"] || "";
      const metaText = [
        author
          ? `${author.length > 30 ? author.substring(0, 30) + "..." : author}`
          : "",
        year ? `(${year})` : "",
        zoteroKey ? `[${zoteroKey}]` : "",
        doi ? `DOI: ${doi}` : "",
        `📝 ${sources}`,
      ]
        .filter(Boolean)
        .join(" · ");

      const metaDiv = ztoolkit.UI.createElement(doc, "div", {
        properties: { innerText: metaText },
        styles: {
          fontSize: "10px",
          color: "var(--text-secondary)",
          overflow: "hidden",
          textOverflow: "ellipsis",
          whiteSpace: "nowrap",
        },
      });
      paperCell.appendChild(metaDiv);
      tr.appendChild(paperCell);

      // Render other columns (computed/custom only)
      otherColumns.forEach((col) => {
        const cellValue = row.data[col.id] || "";
        const isComputed = col.type === "computed";
        const isEmpty = !cellValue || cellValue.trim() === "";

        const td = ztoolkit.UI.createElement(doc, "td", {
          styles: {
            padding: "8px 10px",
            borderBottom: "1px solid rgba(128, 128, 128, 0.4)",
            borderRight: "1px solid rgba(128, 128, 128, 0.4)",
            overflow: "hidden",
            textOverflow: "ellipsis",
            whiteSpace: "normal",
            wordBreak: "break-word",
            width: `${((col.width / totalColumnWidth) * 100).toFixed(2)}%`,
            maxHeight: "60px",
            lineHeight: "1.4",
            verticalAlign: "top",
            cursor: "pointer",
          },
        });

        // Show content or empty indicator
        if (isEmpty && isComputed) {
          const hasNotes = row.noteIds && row.noteIds.length > 0;
          const itemForIndicator = Zotero.Items.get(row.paperId);
          const attachmentsForIndicator =
            itemForIndicator?.getAttachments() || [];
          const hasPDFForIndicator = attachmentsForIndicator.some(
            (attId: number) => {
              const att = Zotero.Items.get(attId);
              return (
                att &&
                (att.attachmentContentType === "application/pdf" ||
                  att.attachmentPath?.toLowerCase().endsWith(".pdf"))
              );
            },
          );

          if (hasNotes) {
            if (hasPDFForIndicator) {
              td.innerHTML = `<span class="generate-indexed-btn" style="color: var(--highlight-primary); font-size: 11px; cursor: pointer;">⚡ Generate</span>`;
              td.title = "Generate: uses existing notes";
            } else {
              td.innerHTML = `<span style="font-size: 11px;">
                              <span class="generate-indexed-btn" style="color: var(--highlight-primary); cursor: pointer;">⚡ Generate</span>
                              <span style="margin: 0 4px; color: var(--text-tertiary);">|</span>
                              <span class="search-pdf-btn" style="color: var(--text-secondary); cursor: pointer;">🔍 Search PDF</span>
                          </span>`;
              td.title =
                "Generate: uses notes | Search PDF: find and attach PDF";
            }
          } else if (hasPDFForIndicator) {
            // Show both options: Generate (uses indexed PDF text) and OCR (creates refined notes)
            td.innerHTML = `<span style="font-size: 11px;">
                            <span class="generate-indexed-btn" style="color: var(--highlight-primary); cursor: pointer;">⚡ Generate</span>
                            <span style="margin: 0 4px; color: var(--text-tertiary);">|</span>
                            <span class="process-pdf-btn" style="color: var(--text-secondary); cursor: pointer;">📄 OCR</span>
                        </span>`;
            td.title = "Generate: uses PDF text | OCR: extracts refined notes";
          } else {
            // Check if paper has identifiers for PDF search
            const hasDoi = !!itemForIndicator?.getField("DOI");
            const hasArxiv = !!extractArxivFromItem(itemForIndicator);
            const hasPmid = !!extractPmidFromItem(itemForIndicator);
            const hasTitle = !!itemForIndicator?.getField("title");

            if (hasDoi || hasArxiv || hasPmid || hasTitle) {
              // FIRST: Check for active/in-progress PDF search state (survives tab switches)
              const activeSearch = activePdfSearches.get(row.paperId);
              if (activeSearch) {
                switch (activeSearch.status) {
                  case "queued":
                  case "searching":
                    td.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">🔍 Searching...</span>`;
                    break;
                  case "retrying":
                    td.innerHTML = `<span style="color: #f57c00; font-size: 11px;">↻ Retrying...</span>`;
                    break;
                  case "found":
                    td.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">📄 Process PDF</span>`;
                    break;
                  case "notfound": {
                    // Show source link if we can resolve one
                    const doi = itemForIndicator?.getField("DOI") as string;
                    const arxiv = extractArxivFromItem(itemForIndicator);
                    const pmid = extractPmidFromItem(itemForIndicator);
                    const itemUrl = itemForIndicator?.getField("url") as string;
                    const sourceLink = getSourceLinkForPaper(
                      doi,
                      arxiv,
                      pmid,
                      undefined,
                      itemUrl,
                    );
                    if (sourceLink) {
                      td.innerHTML = `<span class="source-link-btn" data-url="${sourceLink}" style="color: var(--highlight-primary); font-size: 11px; cursor: pointer;">🔗 Source Link</span>`;
                    } else {
                      td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px; font-style: italic;">❌ No PDF found</span>`;
                    }
                    break;
                  }
                  case "skipped":
                    td.innerHTML = `<span style="color: #f57c00; font-size: 11px;">⏭ ${activeSearch.message || "Skipped"}</span>`;
                    break;
                  case "error":
                    td.innerHTML = `<span style="color: #c62828; font-size: 11px;">✗ ${activeSearch.message || "Error"}</span>`;
                    break;
                }
              } else {
                // Check for persisted PDF discovery result
                const persistedDiscovery =
                  currentTableConfig?.pdfDiscoveryData?.[row.paperId];
                if (persistedDiscovery) {
                  if (persistedDiscovery.status === "found") {
                    td.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">📄 Process PDF</span>`;
                  } else if (
                    persistedDiscovery.status === "source_link" &&
                    persistedDiscovery.sourceUrl
                  ) {
                    td.innerHTML = `<span class="source-link-btn" data-url="${persistedDiscovery.sourceUrl}" style="color: var(--highlight-primary); font-size: 11px; cursor: pointer;">🔗 Source-Link</span>`;
                  } else if (persistedDiscovery.status === "not_found") {
                    td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px; font-style: italic;">❌ Not found</span>`;
                  } else {
                    td.innerHTML = `<span class="search-pdf-btn" style="color: var(--highlight-primary); font-size: 11px; cursor: pointer;">🔍 Search PDF</span>`;
                  }
                } else {
                  td.innerHTML = `<span class="search-pdf-btn" style="color: var(--highlight-primary); font-size: 11px; cursor: pointer;">🔍 Search PDF</span>`;
                }
              }
            } else {
              td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px; font-style: italic;">No source</span>`;
            }
          }
        } else {
          td.innerHTML = parseMarkdown(cellValue);
        }

        // Click behavior
        td.addEventListener("click", async (e) => {
          e.stopPropagation();

          const currentValue = row.data[col.id] || "";
          const currentlyEmpty = !currentValue || currentValue.trim() === "";
          const hasNotes = row.noteIds && row.noteIds.length > 0;

          const item = Zotero.Items.get(row.paperId);
          const attachments = item?.getAttachments() || [];
          const hasPDF = attachments.some((attId: number) => {
            const att = Zotero.Items.get(attId);
            return (
              att &&
              (att.attachmentContentType === "application/pdf" ||
                att.attachmentPath?.toLowerCase().endsWith(".pdf"))
            );
          });

          if (currentlyEmpty && isComputed) {
            // Handle Attach PDF click FIRST (before Search PDF)
            const attachPdfBtn = (e.target as Element).closest(
              ".attach-pdf-btn",
            );
            if (attachPdfBtn && item) {
              // Open file picker to attach PDF
              const fp = new (Zotero.getMainWindow() as any).FilePicker();
              fp.init(
                Zotero.getMainWindow(),
                "Select PDF to attach",
                fp.modeOpen,
              );
              fp.appendFilter("PDF Files", "*.pdf");
              const result = await fp.show();
              if (result === fp.returnOK && fp.file) {
                try {
                  await Zotero.Attachments.importFromFile({
                    file: fp.file,
                    parentItemID: item.id,
                    contentType: "application/pdf",
                  });
                  td.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">📄 Process PDF</span>`;
                } catch (e) {
                  td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Attach failed</span>`;
                }
              }
              return;
            }

            // Handle Generate from indexed PDF click
            const generateIndexedBtn = (e.target as Element).closest(
              ".generate-indexed-btn",
            );
            if (generateIndexedBtn && item) {
              td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">⏳ Generating...</span>`;
              td.style.cursor = "wait";
              try {
                // Use generateFromPDF which now uses indexed text first
                const content = await this.generateFromPDF(item, col);
                row.data[col.id] = content;
                td.innerHTML = content
                  ? parseMarkdown(content)
                  : '<span style="color: var(--text-tertiary); font-size: 11px;">(Empty)</span>';
                td.style.cursor = "pointer";

                if (currentTableConfig) {
                  if (!currentTableConfig.generatedData)
                    currentTableConfig.generatedData = {};
                  if (!currentTableConfig.generatedData[row.paperId])
                    currentTableConfig.generatedData[row.paperId] = {};
                  currentTableConfig.generatedData[row.paperId][col.id] =
                    content;
                  const tableStore = getTableStore();
                  await tableStore.saveConfig(currentTableConfig);
                }
              } catch (err) {
                td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Error: ${err}</span>`;
                td.style.cursor = "pointer";
              }
              return;
            }

            // Handle OCR (Process PDF with DataLabs) click
            const processPdfBtn = (e.target as Element).closest(
              ".process-pdf-btn",
            );
            if (processPdfBtn && item) {
              const pdf = ocrService.getFirstPdfAttachment(item);
              if (!pdf) {
                td.innerHTML = `<span style="color: #c62828; font-size: 11px;">No PDF found</span>`;
                return;
              }
              td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">📄 OCR Processing...</span>`;
              td.style.cursor = "wait";
              try {
                await ocrService.convertToMarkdown(pdf);
                await new Promise((r) => setTimeout(r, 500));

                // Now generate content using the new notes
                const newNoteIds = item.getNotes();
                if (newNoteIds.length > 0) {
                  // CRITICAL: Update row.noteIds so subsequent clicks know we have notes
                  row.noteIds = newNoteIds;
                  row.data["sources"] = String(newNoteIds.length);

                  // Update metadata UI in the first cell
                  const tr = td.parentElement as HTMLTableRowElement;
                  if (tr) {
                    const paperCell = tr.cells[0];
                    if (paperCell && paperCell.children.length > 1) {
                      const metaDiv = paperCell.children[1] as HTMLElement;
                      const author = row.data["author"] || "";
                      const year = row.data["year"] || "";
                      const sources = row.data["sources"];
                      const metaText = [
                        author
                          ? `${author.length > 30 ? author.substring(0, 30) + "..." : author}`
                          : "",
                        year ? `(${year})` : "",
                        `📝 ${sources}`,
                      ]
                        .filter(Boolean)
                        .join(" · ");
                      metaDiv.innerText = metaText;
                    }
                  }

                  const content = await this.generateColumnContent(
                    item,
                    col,
                    newNoteIds,
                  );
                  row.data[col.id] = content;
                  td.innerHTML = content
                    ? parseMarkdown(content)
                    : '<span style="color: var(--text-tertiary); font-size: 11px;">(Empty)</span>';
                  td.style.cursor = "pointer";

                  if (currentTableConfig) {
                    if (!currentTableConfig.generatedData)
                      currentTableConfig.generatedData = {};
                    if (!currentTableConfig.generatedData[row.paperId])
                      currentTableConfig.generatedData[row.paperId] = {};
                    currentTableConfig.generatedData[row.paperId][col.id] =
                      content;
                    const tableStore = getTableStore();
                    await tableStore.saveConfig(currentTableConfig);
                  }
                } else {
                  td.innerHTML = `<span style="color: #ff9800; font-size: 11px;">OCR done, no note created</span>`;
                }
              } catch (err) {
                td.innerHTML = `<span style="color: #c62828; font-size: 11px;">OCR Error</span>`;
                td.title = String(err);
                td.style.cursor = "pointer";
              }
              return;
            }

            // Check if "Search PDF" button was clicked
            const searchPdfBtn = (e.target as Element).closest(
              ".search-pdf-btn",
            );
            if (searchPdfBtn && !hasNotes && !hasPDF && item) {
              // Run PDF discovery pipeline
              td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">⏳ Searching...</span>`;
              td.style.cursor = "wait";

              try {
                const success = await findAndAttachPdfForItem(item, (step) => {
                  td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">⏳ ${step}</span>`;
                });

                if (success) {
                  td.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">📄 Process PDF</span>`;
                  td.style.cursor = "pointer";
                  // Persist the discovery result
                  if (currentTableConfig) {
                    if (!currentTableConfig.pdfDiscoveryData)
                      currentTableConfig.pdfDiscoveryData = {};
                    currentTableConfig.pdfDiscoveryData[row.paperId] = {
                      status: "found",
                      discoveredAt: new Date().toISOString(),
                    };
                    const tableStore = getTableStore();
                    await tableStore.saveConfig(currentTableConfig);
                  }
                } else {
                  // Step 7: Show Source-Link if identifiers available
                  const doi = (item.getField("DOI") as string) || undefined;
                  const arxivId = extractArxivFromItem(item);
                  const pmid = extractPmidFromItem(item);
                  const itemUrl = (item.getField("url") as string) || undefined;
                  const sourceLink = getSourceLinkForPaper(
                    doi,
                    arxivId,
                    pmid,
                    undefined,
                    itemUrl,
                  );

                  if (sourceLink) {
                    td.innerHTML = `<span class="source-link-btn" data-url="${sourceLink}" style="color: var(--highlight-primary); font-size: 11px; cursor: pointer;">🔗 Source-Link</span>`;
                    td.style.cursor = "pointer";
                    // Persist the source link discovery result
                    if (currentTableConfig) {
                      if (!currentTableConfig.pdfDiscoveryData)
                        currentTableConfig.pdfDiscoveryData = {};
                      currentTableConfig.pdfDiscoveryData[row.paperId] = {
                        status: "source_link",
                        sourceUrl: sourceLink,
                        discoveredAt: new Date().toISOString(),
                      };
                      const tableStore = getTableStore();
                      await tableStore.saveConfig(currentTableConfig);
                    }
                  } else {
                    td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px; font-style: italic;">❌ Not found</span>`;
                    td.style.cursor = "pointer";
                    // Persist the not-found result
                    if (currentTableConfig) {
                      if (!currentTableConfig.pdfDiscoveryData)
                        currentTableConfig.pdfDiscoveryData = {};
                      currentTableConfig.pdfDiscoveryData[row.paperId] = {
                        status: "not_found",
                        discoveredAt: new Date().toISOString(),
                      };
                      const tableStore = getTableStore();
                      await tableStore.saveConfig(currentTableConfig);
                    }
                  }
                }
              } catch (err) {
                td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Error</span>`;
                td.style.cursor = "pointer";
                // Persist the error result
                if (currentTableConfig) {
                  if (!currentTableConfig.pdfDiscoveryData)
                    currentTableConfig.pdfDiscoveryData = {};
                  currentTableConfig.pdfDiscoveryData[row.paperId] = {
                    status: "error",
                    discoveredAt: new Date().toISOString(),
                  };
                  const tableStore = getTableStore();
                  await tableStore.saveConfig(currentTableConfig);
                }
              }
              return;
            }

            // Handle Source-Link click
            const sourceLinkBtn = td.querySelector(".source-link-btn");
            if (sourceLinkBtn && item) {
              const linkUrl = sourceLinkBtn.getAttribute("data-url");
              if (linkUrl) {
                Zotero.launchURL(linkUrl);
                // Show Attach, Retry, and Add Note buttons
                td.innerHTML = `<span style="font-size: 11px;">
                                    <span class="attach-pdf-btn" data-item-id="${row.paperId}" style="color: var(--highlight-primary); cursor: pointer; margin-right: 8px;">⬇️ Attach</span>
                                    <span class="search-pdf-btn" style="color: var(--highlight-primary); cursor: pointer; margin-right: 8px;">🔁 Retry</span>
                                    <span class="add-note-btn" data-item-id="${row.paperId}" style="color: var(--highlight-primary); cursor: pointer;">📝 Add Note</span>
                                </span>`;
                td.style.cursor = "pointer";
              }
              return;
            }

            // Handle Add Note click
            const addNoteBtn = (e.target as Element).closest(".add-note-btn");
            if (addNoteBtn && item) {
              try {
                td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">⏳ Creating note...</span>`;
                td.style.cursor = "wait";

                // Create a new note with the paper title as the note title
                const paperTitle =
                  (item.getField("title") as string) || "Untitled";
                const note = new Zotero.Item("note");
                note.libraryID = item.libraryID;
                note.parentID = item.id;
                note.setNote(`<h1>${paperTitle}</h1><p>Context note</p>`);
                await note.saveTx();

                // Open the note immediately
                const zp = Zotero.getActiveZoteroPane();
                if (zp) {
                  await zp.selectItem(note.id);
                  // Open in note editor pane
                  const doc = zp.document || zp.ownerDocument;
                  if (doc) {
                    const notePane = doc.getElementById("zotero-note-editor");
                    if (notePane) {
                      (notePane as any).mode = "edit";
                      (notePane as any).item = note;
                    }
                  }
                }

                // Update the row's noteIds and sources count
                row.noteIds = item.getNotes();
                row.data["sources"] = String(row.noteIds.length);

                // Show "Generate" button now that we have a note
                td.innerHTML = `<span style="color: var(--highlight-primary); font-size: 11px;">⚡ Generate</span>`;
                td.style.cursor = "pointer";

                Zotero.debug(`[seerai] Created note for paper: ${paperTitle}`);
              } catch (err) {
                td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Error creating note</span>`;
                td.style.cursor = "pointer";
                Zotero.debug(`[seerai] Error creating note: ${err}`);
              }
              return;
            }

            if (hasNotes || hasPDF) {
              td.innerHTML = `<span style="color: var(--text-tertiary); font-size: 11px;">⏳ ${hasNotes ? "Generating..." : "Processing..."}</span>`;
              td.style.cursor = "wait";

              try {
                if (item) {
                  const content = hasNotes
                    ? await this.generateColumnContent(item, col, row.noteIds)
                    : await this.generateFromPDF(item, col);

                  row.data[col.id] = content;
                  td.innerHTML = content
                    ? parseMarkdown(content)
                    : '<span style="color: var(--text-tertiary); font-size: 11px;">(Empty)</span>';
                  td.style.cursor = "pointer";

                  if (currentTableConfig) {
                    if (!currentTableConfig.generatedData)
                      currentTableConfig.generatedData = {};
                    if (!currentTableConfig.generatedData[row.paperId])
                      currentTableConfig.generatedData[row.paperId] = {};
                    currentTableConfig.generatedData[row.paperId][col.id] =
                      content;
                    const tableStore = getTableStore();
                    await tableStore.saveConfig(currentTableConfig);
                  }
                }
              } catch (err) {
                td.innerHTML = `<span style="color: #c62828; font-size: 11px;">Error</span>`;
                td.style.cursor = "pointer";
              }
            }
          } else {
            this.showCellDetailModal(doc, row, col, row.data[col.id] || "");
          }
        });

        tr.appendChild(td);
      });

      // Add actions cell with save and remove buttons
      const actionsCell = ztoolkit.UI.createElement(doc, "td", {
        styles: {
          padding: "4px",
          borderBottom: "1px solid rgba(128, 128, 128, 0.4)",
          borderRight: "1px solid rgba(128, 128, 128, 0.4)",
          width: `${actionsPct}%`,
          minWidth: "40px",
          textAlign: "center",
          verticalAlign: "middle",
          overflow: "hidden",
        },
      });

      // Wrapper div for flexbox layout (flex on td doesn't work well)
      const actionsWrapper = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          display: "flex",
          flexDirection: "row",
          gap: "2px",
          justifyContent: "center",
          alignItems: "center",
          flexWrap: "nowrap",
        },
      });

      const saveRowBtn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: "💾" },
        attributes: { title: "Save this row as a note attached to the paper" },
        styles: {
          background: "none",
          border: "none",
          fontSize: "14px",
          cursor: "pointer",
          padding: "4px",
          borderRadius: "4px",
          transition: "background-color 0.15s",
        },
        listeners: [
          {
            type: "click",
            listener: async (e: Event) => {
              e.stopPropagation();
              const btn = e.target as HTMLElement;
              btn.innerText = "⏳";
              btn.style.cursor = "wait";

              const cols = currentTableConfig?.columns || defaultColumns;
              const success = await this.saveRowAsNote(row, cols);

              if (success) {
                btn.innerText = "✓";
                btn.style.color = "#4CAF50";
                setTimeout(() => {
                  btn.innerText = "💾";
                  btn.style.color = "";
                }, 2000);
              } else {
                btn.innerText = "✕";
                btn.style.color = "#c62828";
                setTimeout(() => {
                  btn.innerText = "💾";
                  btn.style.color = "";
                }, 2000);
              }
              btn.style.cursor = "pointer";
            },
          },
        ],
      });
      saveRowBtn.addEventListener("mouseenter", () => {
        saveRowBtn.style.backgroundColor = "rgba(128,128,128,0.2)";
      });
      saveRowBtn.addEventListener("mouseleave", () => {
        saveRowBtn.style.backgroundColor = "";
      });
      actionsWrapper.appendChild(saveRowBtn);

      // Tag generation button
      const tagBtn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: "🏷️" },
        attributes: { title: "Generate AI tags for this paper" },
        styles: {
          background: "none",
          border: "none",
          fontSize: "14px",
          cursor: "pointer",
          padding: "4px",
          borderRadius: "4px",
          transition: "background-color 0.15s",
        },
        listeners: [
          {
            type: "click",
            listener: async (e: Event) => {
              e.stopPropagation();
              const btn = e.target as HTMLElement;
              await this.generateTagsForItem(row, btn);
            },
          },
        ],
      });
      tagBtn.addEventListener("mouseenter", () => {
        tagBtn.style.backgroundColor = "rgba(128,128,128,0.2)";
      });
      tagBtn.addEventListener("mouseleave", () => {
        tagBtn.style.backgroundColor = "";
      });
      actionsWrapper.appendChild(tagBtn);

      // Bomb button (Delete from Zotero)
      const bombBtn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: "💣" },
        attributes: { title: "Delete item from Zotero completely" },
        styles: {
          background: "none",
          border: "none",
          fontSize: "14px",
          cursor: "pointer",
          padding: "4px",
          borderRadius: "4px",
          transition: "background-color 0.15s",
        },
        listeners: [
          {
            type: "click",
            listener: async (e: Event) => {
              e.stopPropagation();
              const btn = (e.target as HTMLElement).closest(
                "button",
              ) as HTMLElement;
              if (!btn) return;

              if (btn.dataset.confirmBomb !== "true") {
                btn.dataset.confirmBomb = "true";
                btn.innerText = "💥";
                btn.style.color = "#c62828";
                btn.style.backgroundColor = "rgba(198,40,40,0.15)";
                btn.title = "Click again to PERMANENTLY DELETE from Zotero";
                setTimeout(() => {
                  btn.dataset.confirmBomb = "";
                  btn.innerText = "💣";
                  btn.style.color = "";
                  btn.style.backgroundColor = "";
                  btn.title = "Delete item from Zotero completely";
                }, 3000);
                return;
              }

              // Execute delete
              try {
                btn.innerText = "⌛";
                await Zotero.Items.erase([row.paperId]);

                // Also remove from table config if present
                if (currentTableConfig) {
                  currentTableConfig.addedPaperIds =
                    currentTableConfig.addedPaperIds.filter(
                      (id) => id !== row.paperId,
                    );
                  if (
                    currentTableConfig.generatedData &&
                    currentTableConfig.generatedData[row.paperId]
                  ) {
                    delete currentTableConfig.generatedData[row.paperId];
                  }
                  const tableStore = getTableStore();
                  await tableStore.saveConfig(currentTableConfig);
                }

                await this.forceRefreshTable();
              } catch (err) {
                Zotero.debug(`[seerai] Error deleting item: ${err}`);
                btn.innerText = "❌";
              }
            },
          },
        ],
      });
      bombBtn.addEventListener("mouseenter", () => {
        bombBtn.style.backgroundColor = "rgba(198,40,40,0.2)";
      });
      bombBtn.addEventListener("mouseleave", () => {
        if (bombBtn.dataset.confirmBomb !== "true") {
          bombBtn.style.backgroundColor = "";
        }
      });
      actionsWrapper.appendChild(bombBtn);

      // Remove button
      const removeRowBtn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: "🗑️" },
        attributes: { title: "Remove this paper from the table" },
        styles: {
          background: "none",
          border: "none",
          fontSize: "14px",
          cursor: "pointer",
          padding: "4px",
          borderRadius: "4px",
          transition: "background-color 0.15s",
        },
        listeners: [
          {
            type: "click",
            listener: async (e: Event) => {
              e.stopPropagation();
              const btn = (e.target as HTMLElement).closest(
                "button",
              ) as HTMLElement;
              if (!btn) return;

              // Confirm removal with visual feedback
              if (btn.dataset.confirmRemove !== "true") {
                btn.dataset.confirmRemove = "true";
                btn.innerText = "❌";
                btn.style.color = "#c62828";
                btn.style.backgroundColor = "rgba(198,40,40,0.15)";
                btn.title = "Click again to confirm removal";
                setTimeout(() => {
                  btn.dataset.confirmRemove = "";
                  btn.innerText = "🗑️";
                  btn.style.color = "";
                  btn.style.backgroundColor = "";
                  btn.title = "Remove this paper from the table";
                }, 3000);
                return;
              }

              // Remove paper from table
              if (currentTableConfig) {
                // Remove from addedPaperIds
                currentTableConfig.addedPaperIds =
                  currentTableConfig.addedPaperIds.filter(
                    (id) => id !== row.paperId,
                  );

                // Remove generated data for this paper
                if (
                  currentTableConfig.generatedData &&
                  currentTableConfig.generatedData[row.paperId]
                ) {
                  delete currentTableConfig.generatedData[row.paperId];
                }

                // Save config
                const tableStore = getTableStore();
                await tableStore.saveConfig(currentTableConfig);

                // Force immediate refresh
                await this.forceRefreshTable();
              }
            },
          },
        ],
      });
      removeRowBtn.addEventListener("mouseenter", () => {
        removeRowBtn.style.backgroundColor = "rgba(198,40,40,0.2)";
      });
      removeRowBtn.addEventListener("mouseleave", () => {
        removeRowBtn.style.backgroundColor = "";
      });
      actionsWrapper.appendChild(removeRowBtn);

      // Add wrapper to cell
      actionsCell.appendChild(actionsWrapper);
      tr.appendChild(actionsCell);

      tbody.appendChild(tr);
    });

    table.appendChild(tbody);

    // Return table directly - side strip handles add column button
    return table;
  }

  // Debounce timer for table refresh
  private static tableRefreshTimer: ReturnType<typeof setTimeout> | null = null;

  /**
   * Debounce table refresh to avoid too-frequent re-renders
   */
  private static debounceTableRefresh(doc: Document, item: Zotero.Item): void {
    if (this.tableRefreshTimer) {
      clearTimeout(this.tableRefreshTimer);
    }
    this.tableRefreshTimer = setTimeout(async () => {
      if (currentContainer && currentItem) {
        // Just refresh the table, not the whole interface
        const tableWrapper = doc.querySelector(".table-wrapper");
        if (tableWrapper) {
          const tableData = await this.loadTableData();
          tableWrapper.innerHTML = "";
          if (tableData.rows.length === 0) {
            tableWrapper.appendChild(this.createTableEmptyState(doc, item));
          } else {
            tableWrapper.appendChild(this.createPapersTable(doc, tableData));
          }
        }
      }
    }, 300);
  }
  /**
   * Handle table pagination with proper state management
   */
  private static async handleTablePagination(delta: number): Promise<void> {
    if (!currentTableConfig) return;

    // Apply delta
    currentTableConfig.currentPage += delta;

    // Simple lower bound check
    if (currentTableConfig.currentPage < 1) currentTableConfig.currentPage = 1;

    // We don't strictly check upper bound here as it depends on total pages which might change.
    // loadTableData() called by forceRefreshTable() will handle clamping to max pages and updating config.

    const tableStore = getTableStore();
    await tableStore.saveConfig(currentTableConfig);

    await this.forceRefreshTable();
  }

  /**
   * Force an immediate table refresh with proper container validation.
   * Use this for user-initiated actions that need immediate feedback.
   * Only refreshes the table content, preserves scroll position.
   */
  private static async forceRefreshTable(): Promise<void> {
    Zotero.debug("[seerai] forceRefreshTable: Starting targeted refresh...");

    if (!currentItem) {
      Zotero.debug("[seerai] forceRefreshTable: No item available");
      return;
    }

    // Find the table wrapper - try multiple strategies
    let tableWrapper: HTMLElement | null = null;
    let doc: Document | null = null;

    // Strategy 1: Look in currentContainer
    if (currentContainer && currentContainer.isConnected) {
      tableWrapper = currentContainer.querySelector(
        ".table-wrapper",
      ) as HTMLElement;
      if (tableWrapper) {
        doc = currentContainer.ownerDocument;
        Zotero.debug(
          "[seerai] forceRefreshTable: Found table wrapper in currentContainer",
        );
      }
    }

    // Strategy 2: Look in main windows
    if (!tableWrapper) {
      const windows = Zotero.getMainWindows();
      for (const win of windows) {
        tableWrapper = win.document.querySelector(
          ".table-wrapper",
        ) as HTMLElement;
        if (tableWrapper && tableWrapper.isConnected) {
          doc = win.document;
          Zotero.debug(
            "[seerai] forceRefreshTable: Found table wrapper in main window",
          );
          break;
        }
      }
    }

    // Strategy 3: Use document.querySelectorAll to find any table wrapper
    if (!tableWrapper && currentContainer?.ownerDocument) {
      const allWrappers =
        currentContainer.ownerDocument.querySelectorAll(".table-wrapper");
      for (const wrapper of allWrappers) {
        if ((wrapper as HTMLElement).isConnected) {
          tableWrapper = wrapper as HTMLElement;
          doc = currentContainer.ownerDocument;
          Zotero.debug(
            "[seerai] forceRefreshTable: Found table wrapper via querySelectorAll",
          );
          break;
        }
      }
    }

    if (!tableWrapper || !tableWrapper.isConnected || !doc) {
      Zotero.debug(
        "[seerai] forceRefreshTable: No table wrapper found, falling back to full refresh",
      );
      // Fallback to full refresh
      if (currentContainer && currentContainer.isConnected) {
        this.lastRenderedTab = "";
        this.lastRenderedItemId = 0;
        this.lastRenderedContainer = null;
        await this.renderInterface(currentContainer, currentItem);
      }
      return;
    }

    // Save scroll position
    const scrollTop = tableWrapper.scrollTop;
    const scrollLeft = tableWrapper.scrollLeft;
    Zotero.debug(
      `[seerai] forceRefreshTable: Preserving scroll (${scrollTop}, ${scrollLeft})`,
    );

    // Reload table data and rebuild just the table
    const tableData = await this.loadTableData();
    Zotero.debug(
      `[seerai] forceRefreshTable: Loaded ${tableData.rows.length} rows, page ${tableData.currentPage}/${tableData.totalPages}`,
    );

    tableWrapper.innerHTML = "";

    if (tableData.rows.length === 0) {
      tableWrapper.appendChild(this.createTableEmptyState(doc, currentItem));
    } else {
      tableWrapper.appendChild(this.createPapersTable(doc, tableData));
    }

    // Restore scroll position
    tableWrapper.scrollTop = scrollTop;
    tableWrapper.scrollLeft = scrollLeft;

    // Also update the pagination UI in the toolbar
    this.updatePaginationUI(doc, tableData);

    Zotero.debug("[seerai] forceRefreshTable: Targeted refresh complete");
  }

  /**
   * Update just the pagination UI without full refresh
   */
  private static updatePaginationUI(doc: Document, tableData: TableData): void {
    // Try to find elements within currentContainer first for better reliability
    const root: ParentNode =
      currentContainer && currentContainer.isConnected ? currentContainer : doc;

    // Update page indicator using ID
    const pageIndicator = root.querySelector(
      "#table-page-indicator",
    ) as HTMLElement;
    if (pageIndicator) {
      pageIndicator.innerText = `${tableData.currentPage} / ${tableData.totalPages}`;
      Zotero.debug(
        `[seerai] updatePaginationUI: Updated to ${tableData.currentPage}/${tableData.totalPages}`,
      );
    } else {
      Zotero.debug("[seerai] updatePaginationUI: Page indicator not found");
    }

    // Update button states
    const paginationContainer = root.querySelector(".pagination-container");
    if (paginationContainer) {
      const buttons = paginationContainer.querySelectorAll("button");
      // First button is prev, last button is next
      const prevBtn = buttons[0] as HTMLButtonElement;
      const nextBtn = buttons[buttons.length - 1] as HTMLButtonElement;

      if (prevBtn) {
        prevBtn.disabled = tableData.currentPage <= 1;
        prevBtn.style.opacity = tableData.currentPage <= 1 ? "0.5" : "1";
      }
      if (nextBtn) {
        nextBtn.disabled = tableData.currentPage >= tableData.totalPages;
        nextBtn.style.opacity =
          tableData.currentPage >= tableData.totalPages ? "0.5" : "1";
      }
    }
  }

  /**
   * Load table data - find papers with notes that share titles
   */
  private static async loadTableData(): Promise<TableData> {
    // Preserve previous selection if available
    const previousSelection =
      currentTableData?.selectedRowIds || new Set<number>();

    const tableData: TableData = {
      rows: [],
      selectedRowIds: previousSelection,
      isLoading: false,
      totalRows: 0,
      totalPages: 1,
      currentPage: currentTableConfig?.currentPage || 1,
      pageSize: currentTableConfig?.pageSize || 25,
    };

    try {
      // Only show papers that have been manually added
      const addedIds = currentTableConfig?.addedPaperIds || [];

      if (addedIds.length === 0) {
        // Table starts empty - user needs to add papers
        return tableData;
      }

      // Get filter settings - strip any quote characters that might have been added
      const rawFilterQuery = currentTableConfig?.filterQuery || "";
      const filterQuery = rawFilterQuery
        .toLowerCase()
        .trim()
        .replace(/^["']+|["']+$/g, "") // Remove leading/trailing quotes
        .replace(/[""'']/g, ""); // Remove any curly/smart quotes

      // STEP 1: Build ALL rows first (without filtering)
      const allRows: TableRow[] = [];

      // Prepare generated data map with string key fallback
      const generatedDataMap = currentTableConfig?.generatedData || {};

      for (const paperId of addedIds) {
        const item = Zotero.Items.get(paperId);
        if (!item || !item.isRegularItem()) continue;

        // Get paper metadata
        const paperTitle = (item.getField("title") as string) || "Untitled";
        const creators = item.getCreators();
        const authorNames =
          creators
            .map((c) => `${c.firstName || ""} ${c.lastName || ""}`.trim())
            .join(", ") || "Unknown";
        const year = (item.getField("year") as string) || "";

        // Load persisted generated data - try both number and string keys
        // (JSON serialization converts number keys to strings)
        const persistedData =
          generatedDataMap[item.id] ||
          (generatedDataMap as Record<string, { [columnId: string]: string }>)[
            String(item.id)
          ] ||
          {};

        // Get note count for sources column
        const noteIDs = item.getNotes();
        const doi = (item.getField("DOI") as string) || "";

        // Build the complete row with all data
        const rowData: Record<string, string> = {
          title: paperTitle,
          author: authorNames,
          year: year,
          sources: String(noteIDs.length),
          zoteroKey: item.key,
          DOI: doi,
          // Merge ALL persisted computed columns
          ...persistedData,
        };

        allRows.push({
          paperId: item.id,
          paperTitle: paperTitle,
          noteIds: noteIDs,
          noteTitle: "",
          data: rowData,
        });
      }

      let filteredRows: TableRow[];

      if (filterQuery) {
        Zotero.debug(
          `[seerai:search] Starting search for: "${filterQuery}" across ${allRows.length} rows`,
        );

        filteredRows = allRows.filter((row) => {
          // Identify all search targets (column values)
          const rawSearchTargets = Object.values(row.data).map((v) =>
            String(v || ""),
          );

          // Strip markdown from targets to allow clean phrase matching
          // e.g. "**Verdict:** no" -> "Verdict: no"
          const stripMarkdown = (t: string) =>
            t.replace(/[*_~`]/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");

          const cleanSearchTargets = rawSearchTargets.map(stripMarkdown);

          // Use advancedSearch which now handles Boolean logic if detected
          // We search against the CLEAN (stripped) text to match user intent
          // We also search against RAW text to support searching for markdown symbols if needed
          // But prioritizing clean text ensures "Verdict: no" matches "**Verdict:** no" correctly
          const { matches } = advancedSearch(filterQuery, [
            ...cleanSearchTargets,
            ...rawSearchTargets,
          ]);
          if (matches) return true;

          // Fallback: Try markdown-stripped match for phrase search if advancedSearch didn't match
          // (This handles cases where the user types a literal phrase that might be broken by Boolean tokenization if it contains AND/OR/NOT but isn't meant as a Boolean query)
          const rawBlob = rawSearchTargets
            .join(" ")
            .replace(/<think>[\s\S]*?<\/think>/gi, "")
            .toLowerCase();

          const target = filterQuery.toLowerCase().trim();
          if (rawBlob.includes(target)) return true;

          return stripMarkdown(rawBlob).includes(stripMarkdown(target));
        });

        Zotero.debug(
          `[seerai:search] Result: ${filteredRows.length}/${allRows.length} rows match "${filterQuery}"`,
        );
      } else {
        filteredRows = allRows;
      }

      const allFilteredRows = filteredRows;

      // Sort
      const sortBy = currentTableConfig?.sortBy || "title";
      const sortOrder = currentTableConfig?.sortOrder || "asc";
      allFilteredRows.sort((a, b) => {
        const aVal = a.data[sortBy] || "";
        const bVal = b.data[sortBy] || "";
        const cmp = aVal.localeCompare(bVal);
        return sortOrder === "asc" ? cmp : -cmp;
      });

      // Set total rows
      tableData.totalRows = allFilteredRows.length;

      // Calculate pagination
      const pageSize = tableData.pageSize;
      tableData.totalPages = Math.max(
        1,
        Math.ceil(tableData.totalRows / pageSize),
      );

      // Ensure current page is within bounds
      if (tableData.currentPage > tableData.totalPages) {
        tableData.currentPage = tableData.totalPages;
        if (currentTableConfig)
          currentTableConfig.currentPage = tableData.currentPage;
      }
      if (tableData.currentPage < 1) {
        tableData.currentPage = 1;
        if (currentTableConfig) currentTableConfig.currentPage = 1;
      }

      // Slice rows for current page
      const startIndex = (tableData.currentPage - 1) * pageSize;
      const endIndex = startIndex + pageSize;
      tableData.rows = allFilteredRows.slice(startIndex, endIndex);
    } catch (e) {
      Zotero.debug(`[seerai] Error loading table data: ${e}`);
      tableData.error = String(e);
    }

    currentTableData = tableData;
    return tableData;
  }

  /**
   * Show column manager as a dropdown panel (like paper picker)
   */
  private static showColumnManagerModal(
    doc: Document,
    item: Zotero.Item,
  ): void {
    // Toggle existing dropdown
    const existing = doc.getElementById(
      "column-manager-dropdown",
    ) as HTMLElement;
    if (existing) {
      existing.style.opacity = "0";
      existing.style.transform = "translateY(-10px)";
      setTimeout(() => existing.remove(), 200);
      return;
    }

    // Find the toolbar to position dropdown below it
    const toolbar = doc.querySelector(".table-toolbar") as HTMLElement;
    const tabContent = doc.getElementById("tab-content");
    if (!toolbar || !tabContent) return;

    // Helper to close dropdown with animation
    const closeDropdown = () => {
      dropdown.style.opacity = "0";
      dropdown.style.transform = "translateY(-10px)";
      setTimeout(() => dropdown.remove(), 200);
    };

    // Create dropdown panel
    const dropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "column-manager-dropdown" },
      styles: {
        backgroundColor: "var(--background-primary)",
        borderRadius: "8px",
        padding: "0",
        boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
        overflow: "hidden",
        border: "1px solid var(--border-primary)",
        transition: "all 0.2s ease-out",
        opacity: "0",
        transform: "translateY(-10px)",
        marginTop: "4px",
        marginLeft: "8px",
        marginRight: "8px",
        maxHeight: "70vh",
        display: "flex",
        flexDirection: "column",
      },
    });

    // Header with gradient
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        background:
          "linear-gradient(135deg, var(--highlight-primary) 0%, color-mix(in srgb, var(--highlight-primary) 80%, purple) 100%)",
        padding: "10px 14px",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        gap: "12px",
      },
    });

    const headerTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "⚙️ Manage Columns" },
      styles: {
        fontSize: "13px",
        fontWeight: "600",
        color: "var(--highlight-text)",
        textShadow: "0 1px 2px rgba(0,0,0,0.1)",
      },
    });
    header.appendChild(headerTitle);

    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "rgba(0,0,0,0.1)",
        border: "none",
        borderRadius: "50%",
        width: "22px",
        height: "22px",
        cursor: "pointer",
        color: "var(--highlight-text)",
        fontSize: "11px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
      listeners: [
        {
          type: "click",
          listener: () => closeDropdown(),
        },
      ],
    });
    header.appendChild(closeBtn);
    dropdown.appendChild(header);

    // Content container (scrollable)
    const content = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "10px 14px",
        display: "flex",
        flexDirection: "column",
        gap: "10px",
        overflowY: "auto",
        flex: "1",
      },
    });

    // --- Presets Section ---
    const presetSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "6px",
        padding: "10px",
        backgroundColor: "var(--background-secondary)",
        borderRadius: "6px",
        border: "1px solid var(--border-primary)",
        flexWrap: "wrap",
        alignItems: "center",
      },
    });

    // Preset selector
    const presetSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        flex: "1",
        padding: "6px 10px",
        borderRadius: "6px",
        border: "1px solid var(--border-primary)",
        minWidth: "120px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
        outline: "none",
      },
    }) as HTMLSelectElement;

    const defaultOption = ztoolkit.UI.createElement(doc, "option", {
      properties: { value: "", innerText: "Select a preset..." },
    });
    presetSelect.appendChild(defaultOption);

    // Load presets function
    const loadPresetsList = async () => {
      const tableStore = getTableStore();
      const presets = await tableStore.loadPresets();

      // Clear except default
      while (presetSelect.options.length > 1) {
        presetSelect.remove(1);
      }

      presets.forEach((p) => {
        const opt = ztoolkit.UI.createElement(doc, "option", {
          properties: { value: p.id, innerText: p.name },
        });
        presetSelect.appendChild(opt);
      });
      return presets;
    };
    // Initial load
    loadPresetsList();

    // Preset buttons container
    const presetBtnsRow = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "4px" },
    });

    // Load Button
    const loadPresetBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📥" },
      attributes: { title: "Load preset" },
      styles: {
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        cursor: "pointer",
        backgroundColor: "var(--background-primary)",
        fontSize: "12px",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const selectedId = presetSelect.value;
            if (!selectedId) return;

            const tableStore = getTableStore();
            const presets = await tableStore.loadPresets();
            const preset = presets.find((p) => p.id === selectedId);

            if (preset && currentTableConfig) {
              // confirm overwrite
              const confirmLoad = doc.defaultView?.confirm(
                `Load preset "${preset.name}"? This will replace current columns.`,
              );
              if (confirmLoad) {
                currentTableConfig.columns = [...preset.columns];
                await tableStore.saveConfig(currentTableConfig);
                closeDropdown();
                if (currentContainer && currentItem) {
                  this.renderInterface(currentContainer, currentItem);
                }
              }
            }
          },
        },
      ],
    });

    // Save Button
    const savePresetBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "💾" },
      attributes: { title: "Save current as preset" },
      styles: {
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        cursor: "pointer",
        backgroundColor: "var(--background-primary)",
        fontSize: "12px",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            if (!currentTableConfig) return;

            const name = doc.defaultView?.prompt(
              "Enter name for this column preset:",
              "My Custom Columns",
            );
            if (name) {
              const newPreset: ColumnPreset = {
                id: `preset_${Date.now()}`,
                name: name,
                columns: [...currentTableConfig.columns],
                createdAt: new Date().toISOString(),
              };

              const tableStore = getTableStore();
              await tableStore.savePreset(newPreset);
              await loadPresetsList(); // Refresh list
              presetSelect.value = newPreset.id; // Select it
              doc.defaultView?.alert(`Preset "${name}" saved!`);
            }
          },
        },
      ],
    });

    // Delete Button
    const deletePresetBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "🗑" },
      attributes: { title: "Delete selected preset" },
      styles: {
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        cursor: "pointer",
        color: "#c62828",
        backgroundColor: "var(--background-primary)",
        fontSize: "12px",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const selectedId = presetSelect.value;
            if (!selectedId) return;

            const tableStore = getTableStore();
            const presets = await tableStore.loadPresets();
            const preset = presets.find((p) => p.id === selectedId);

            if (preset) {
              const confirmDelete = doc.defaultView?.confirm(
                `Delete preset "${preset.name}"?`,
              );
              if (confirmDelete) {
                await tableStore.deletePreset(selectedId);
                await loadPresetsList();
                presetSelect.value = "";
              }
            }
          },
        },
      ],
    });

    presetBtnsRow.appendChild(loadPresetBtn);
    presetBtnsRow.appendChild(savePresetBtn);
    presetBtnsRow.appendChild(deletePresetBtn);

    presetSection.appendChild(presetSelect);
    presetSection.appendChild(presetBtnsRow);
    content.appendChild(presetSection);

    // Column list
    const columnList = ztoolkit.UI.createElement(doc, "div", {
      properties: { className: "column-manager-list" },
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "4px",
        maxHeight: "200px",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
      },
    });

    const columns = currentTableConfig?.columns || defaultColumns;
    columns.forEach((col) => {
      const row = ztoolkit.UI.createElement(doc, "label", {
        properties: { className: "column-manager-item" },
        styles: {
          display: "flex",
          alignItems: "center",
          gap: "8px",
          padding: "8px 10px",
          cursor: "pointer",
          borderBottom: "1px solid var(--border-primary)",
        },
      });

      const checkbox = ztoolkit.UI.createElement(doc, "input", {
        attributes: { type: "checkbox" },
      }) as HTMLInputElement;
      checkbox.checked = col.visible;
      checkbox.addEventListener("change", async () => {
        col.visible = checkbox.checked;
        if (currentTableConfig) {
          const tableStore = getTableStore();
          await tableStore.saveConfig(currentTableConfig);
        }
      });

      const label = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: col.name },
        styles: { flex: "1", fontSize: "12px" },
      });

      row.appendChild(checkbox);
      row.appendChild(label);

      // Delete button (only for non-core columns)
      const coreColumns = ["title", "author", "year", "sources"];
      if (!coreColumns.includes(col.id)) {
        const deleteBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "🗑" },
          styles: {
            background: "none",
            border: "none",
            fontSize: "12px",
            cursor: "pointer",
            color: "#c62828",
            padding: "2px 4px",
          },
          listeners: [
            {
              type: "click",
              listener: async (e: Event) => {
                e.preventDefault();
                e.stopPropagation();
                if (currentTableConfig) {
                  currentTableConfig.columns =
                    currentTableConfig.columns.filter((c) => c.id !== col.id);
                  const tableStore = getTableStore();
                  await tableStore.saveConfig(currentTableConfig);
                  row.remove();
                }
              },
            },
          ],
        });
        row.appendChild(deleteBtn);
      }

      columnList.appendChild(row);
    });

    content.appendChild(columnList);

    // Add new column section
    const addSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        borderTop: "1px solid var(--border-primary)",
        paddingTop: "10px",
        display: "flex",
        flexDirection: "column",
        gap: "6px",
      },
    });

    const addLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "➕ Add New Column" },
      styles: {
        fontSize: "12px",
        fontWeight: "600",
        color: "var(--text-secondary)",
      },
    });
    addSection.appendChild(addLabel);

    const newColumnInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "text", placeholder: "Column name..." },
      styles: {
        width: "100%",
        padding: "8px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        outline: "none",
        boxSizing: "border-box",
      },
    }) as HTMLInputElement;
    addSection.appendChild(newColumnInput);

    const newColumnDesc = ztoolkit.UI.createElement(doc, "textarea", {
      attributes: {
        placeholder: 'AI Prompt (e.g. "Extract the main findings...")',
      },
      styles: {
        width: "100%",
        padding: "8px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        minHeight: "50px",
        resize: "vertical",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        outline: "none",
        boxSizing: "border-box",
      },
    }) as HTMLTextAreaElement;
    addSection.appendChild(newColumnDesc);

    const getEffectiveTableTemp = () => currentTableConfig?.temperature ?? 0.7;
    const getMaxLenPlaceholder = () => {
      const tableLen = currentTableConfig?.responseLength;
      if (tableLen === undefined) return "Default";
      if (tableLen === 0) return "Limitless";
      return String(tableLen);
    };

    const managerAiParams = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        padding: "8px",
        backgroundColor: "var(--background-secondary)",
        border: "1px solid var(--border-secondary)",
        borderRadius: "6px",
        marginTop: "4px",
      },
    });

    const managerAiParamsTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "AI Parameters" },
      styles: {
        fontSize: "10px",
        fontWeight: "700",
        color: "var(--highlight-primary)",
        marginBottom: "-2px",
        textTransform: "uppercase",
      },
    });
    managerAiParams.appendChild(managerAiParamsTitle);

    // Temp
    const managerTempHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        fontSize: "10px",
        fontWeight: "600",
        color: "var(--text-secondary)",
      },
    });
    const managerTempLabelText = (val: number) =>
      `Temp: Default (${val.toFixed(1)})`;
    const managerTempLabel = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: managerTempLabelText(getEffectiveTableTemp()) },
    });
    managerTempHeader.appendChild(managerTempLabel);
    managerAiParams.appendChild(managerTempHeader);

    const managerTempSlider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "0",
        max: "2",
        step: "0.1",
        value: String(getEffectiveTableTemp()),
      },
      styles: { width: "100%", cursor: "pointer", height: "14px" },
    }) as HTMLInputElement;
    managerTempSlider.addEventListener("input", () => {
      managerTempLabel.innerText = managerTempLabelText(
        parseFloat(managerTempSlider.value),
      );
    });
    managerAiParams.appendChild(managerTempSlider);

    // Max Len
    const managerMaxLenRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        fontSize: "10px",
        fontWeight: "600",
        color: "var(--text-secondary)",
        marginTop: "2px",
      },
    });
    managerMaxLenRow.appendChild(
      ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: "Max Resp. Length:" },
      }),
    );
    const managerMaxLenInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "number",
        min: "1",
        placeholder: getMaxLenPlaceholder(),
      },
      styles: {
        width: "60px",
        padding: "2px 4px",
        fontSize: "10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
      },
    }) as HTMLInputElement;
    managerMaxLenRow.appendChild(managerMaxLenInput);
    managerAiParams.appendChild(managerMaxLenRow);

    addSection.appendChild(managerAiParams);

    const addColumnBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Add Column" },
      styles: {
        padding: "8px 16px",
        border: "none",
        borderRadius: "6px",
        backgroundColor: "var(--highlight-primary)",
        color: "var(--highlight-text)",
        cursor: "pointer",
        fontWeight: "600",
        fontSize: "12px",
        width: "100%",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const name = newColumnInput.value.trim();
            const aiPrompt = newColumnDesc.value.trim();
            if (name && currentTableConfig) {
              const customTemp = parseFloat(managerTempSlider.value);
              const customMaxTokens = parseInt(managerMaxLenInput.value);

              const newColumn: TableColumn = {
                id: `custom_${Date.now()}`,
                name,
                width: 150,
                minWidth: 80,
                visible: true,
                sortable: false,
                resizable: true,
                type: "computed", // AI-generated column
                aiPrompt:
                  aiPrompt ||
                  `Extract information related to "${name}" from this paper.`,
                temperature:
                  customTemp !== getEffectiveTableTemp()
                    ? customTemp
                    : undefined,
                maxTokens: !isNaN(customMaxTokens)
                  ? customMaxTokens
                  : undefined,
              };
              currentTableConfig.columns.push(newColumn);
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
              closeDropdown();
              if (currentContainer && currentItem) {
                this.renderInterface(currentContainer, currentItem);
              }
            }
          },
        },
      ],
    });
    addSection.appendChild(addColumnBtn);
    content.appendChild(addSection);

    dropdown.appendChild(content);

    // Insert dropdown after toolbar
    toolbar.insertAdjacentElement("afterend", dropdown);

    // Animate in
    setTimeout(() => {
      dropdown.style.opacity = "1";
      dropdown.style.transform = "translateY(0)";
    }, 10);

    // Click outside to close
    const handleClickOutside = (e: Event) => {
      if (
        !dropdown.contains(e.target as Node) &&
        !toolbar.contains(e.target as Node)
      ) {
        closeDropdown();
        doc.removeEventListener("click", handleClickOutside);
      }
    };
    // Delay to avoid immediate trigger
    setTimeout(() => {
      doc.addEventListener("click", handleClickOutside);
    }, 100);
  }

  /**
   * Show quick dropdown for adding a new column (triggered from + in table header)
   */
  private static showQuickAddColumnDropdown(
    doc: Document,
    anchorEl: HTMLElement,
  ): void {
    // Toggle existing dropdown
    const existing = doc.getElementById(
      "quick-add-column-dropdown",
    ) as HTMLElement;
    if (existing) {
      existing.style.opacity = "0";
      existing.style.transform = "translateY(-5px)";
      setTimeout(() => existing.remove(), 150);
      return;
    }

    // Helper to close dropdown with animation
    const closeDropdown = () => {
      dropdown.style.opacity = "0";
      dropdown.style.transform = "translateY(-5px)";
      setTimeout(() => dropdown.remove(), 150);
    };

    // Create dropdown panel
    const dropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "quick-add-column-dropdown" },
      styles: {
        position: "absolute",
        top: "100%",
        right: "0",
        zIndex: "1000",
        backgroundColor: "var(--background-primary)",
        borderRadius: "8px",
        padding: "0",
        boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
        overflow: "hidden",
        border: "1px solid var(--border-primary)",
        transition: "all 0.15s ease-out",
        opacity: "0",
        transform: "translateY(-5px)",
        marginTop: "4px",
        minWidth: "220px",
      },
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        background:
          "linear-gradient(135deg, var(--highlight-primary) 0%, color-mix(in srgb, var(--highlight-primary) 80%, purple) 100%)",
        padding: "8px 12px",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      },
    });

    const headerTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "➕ New Column" },
      styles: {
        fontSize: "12px",
        fontWeight: "600",
        color: "var(--highlight-text)",
      },
    });
    header.appendChild(headerTitle);

    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "rgba(0,0,0,0.1)",
        border: "none",
        borderRadius: "50%",
        width: "18px",
        height: "18px",
        cursor: "pointer",
        color: "var(--highlight-text)",
        fontSize: "10px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
      listeners: [
        {
          type: "click",
          listener: () => closeDropdown(),
        },
      ],
    });
    header.appendChild(closeBtn);
    dropdown.appendChild(header);

    // Content
    const content = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "10px 12px",
        display: "flex",
        flexDirection: "column",
        gap: "8px",
      },
    });

    // Name input
    const nameInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "text", placeholder: "Column name..." },
      styles: {
        width: "100%",
        padding: "8px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        outline: "none",
        boxSizing: "border-box",
      },
    }) as HTMLInputElement;
    content.appendChild(nameInput);

    // AI Prompt input
    const promptInput = ztoolkit.UI.createElement(doc, "textarea", {
      attributes: { placeholder: 'AI Prompt (e.g. "Extract findings...")' },
      styles: {
        width: "100%",
        padding: "8px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "11px",
        minHeight: "50px",
        resize: "vertical",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        outline: "none",
        boxSizing: "border-box",
      },
    }) as HTMLTextAreaElement;
    promptInput.addEventListener("click", (e) => e.stopPropagation());
    promptInput.addEventListener("mousedown", (e) => e.stopPropagation());
    content.appendChild(promptInput);

    const getEffectiveTableTemp = () => currentTableConfig?.temperature ?? 0.7;
    const getMaxLenPlaceholder = () => {
      const tableLen = currentTableConfig?.responseLength;
      if (tableLen === undefined) return "Default";
      if (tableLen === 0) return "Limitless";
      return String(tableLen);
    };

    const aiParamsContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        padding: "10px",
        backgroundColor: "rgba(0,0,0,0.03)",
        border: "1px solid var(--border-secondary)",
        borderRadius: "6px",
        marginTop: "6px",
      },
    });

    const aiParamsTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "AI Parameters" },
      styles: {
        fontSize: "10px",
        fontWeight: "800",
        color: "var(--highlight-primary)",
        marginBottom: "2px",
        textTransform: "uppercase",
        borderBottom: "1px solid rgba(128,128,128,0.1)",
        paddingBottom: "2px",
      },
    });
    aiParamsContainer.appendChild(aiParamsTitle);

    // Temperature Slider
    const tempHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        fontSize: "10px",
        fontWeight: "600",
        color: "var(--text-secondary)",
      },
    });
    const tempLabelText = (val: number) => `Temp: Default (${val.toFixed(1)})`;
    const tempLabel = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: tempLabelText(getEffectiveTableTemp()) },
    });
    tempHeader.appendChild(tempLabel);
    aiParamsContainer.appendChild(tempHeader);

    const tempSlider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "0",
        max: "2",
        step: "0.1",
        value: String(getEffectiveTableTemp()),
      },
      styles: { width: "100%", cursor: "pointer", height: "14px" },
    }) as HTMLInputElement;
    tempSlider.addEventListener("input", () => {
      tempLabel.innerText = tempLabelText(parseFloat(tempSlider.value));
    });
    aiParamsContainer.appendChild(tempSlider);

    // Max Length Input
    const maxLenRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        fontSize: "10px",
        fontWeight: "600",
        color: "var(--text-secondary)",
        marginTop: "4px",
      },
    });
    maxLenRow.appendChild(
      ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: "Max Response Length:" },
      }),
    );
    const maxLenInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "number",
        min: "1",
        placeholder: getMaxLenPlaceholder(),
      },
      styles: {
        width: "60px",
        padding: "2px 4px",
        fontSize: "10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-primary)",
      },
    }) as HTMLInputElement;
    maxLenRow.appendChild(maxLenInput);
    aiParamsContainer.appendChild(maxLenRow);

    content.appendChild(aiParamsContainer);

    // Add button
    const addBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Add Column" },
      styles: {
        padding: "8px 12px",
        border: "none",
        borderRadius: "6px",
        backgroundColor: "var(--highlight-primary)",
        color: "var(--highlight-text)",
        cursor: "pointer",
        fontWeight: "600",
        fontSize: "12px",
        width: "100%",
        marginTop: "4px",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const name = nameInput.value.trim();
            if (!name) {
              nameInput.style.borderColor = "#c62828";
              return;
            }

            if (currentTableConfig) {
              const aiPrompt = promptInput.value.trim();
              const customTemp = parseFloat(tempSlider.value);
              const customMaxTokens = parseInt(maxLenInput.value);

              const newColumn: TableColumn = {
                id: `custom_${Date.now()}`,
                name,
                width: 150,
                minWidth: 80,
                visible: true,
                sortable: false,
                resizable: true,
                type: "computed",
                aiPrompt:
                  aiPrompt ||
                  `Extract information related to "${name}" from this paper.`,
                temperature:
                  customTemp !== getEffectiveTableTemp()
                    ? customTemp
                    : undefined,
                maxTokens: !isNaN(customMaxTokens)
                  ? customMaxTokens
                  : undefined,
              };
              currentTableConfig.columns.push(newColumn);
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
              closeDropdown();
              if (currentContainer && currentItem) {
                this.renderInterface(currentContainer, currentItem);
              }
            }
          },
        },
      ],
    });
    content.appendChild(addBtn);

    dropdown.appendChild(content);

    // Position dropdown to the left of the + button
    anchorEl.style.position = "relative";
    dropdown.style.position = "absolute";
    dropdown.style.top = "0";
    dropdown.style.right = "100%"; // Position to the left of the button
    dropdown.style.marginRight = "4px";
    anchorEl.appendChild(dropdown);

    // Stop propagation on inputs to prevent click-outside from triggering
    nameInput.addEventListener("click", (e) => e.stopPropagation());
    nameInput.addEventListener("mousedown", (e) => e.stopPropagation());
    promptInput.addEventListener("click", (e) => e.stopPropagation());
    promptInput.addEventListener("mousedown", (e) => e.stopPropagation());

    // Animate in
    setTimeout(() => {
      dropdown.style.opacity = "1";
      dropdown.style.transform = "translateY(0)";
    }, 10);

    // Focus name input
    setTimeout(() => nameInput.focus(), 50);

    // Click outside to close
    const handleClickOutside = (e: Event) => {
      const target = e.target as Node;
      // Check if click is inside dropdown or on the anchor button
      if (!dropdown.contains(target) && !anchorEl.contains(target)) {
        closeDropdown();
        doc.removeEventListener("click", handleClickOutside);
      }
    };
    setTimeout(() => {
      doc.addEventListener("click", handleClickOutside);
    }, 150);
  }

  /**
   * Immediately add a new table column and open inline editor
   */
  private static async addImmediateTableColumn(
    doc: Document,
    item: Zotero.Item,
    container: HTMLElement,
  ): Promise<void> {
    if (!currentTableConfig) return;

    const newColumn: TableColumn = {
      id: `custom_${Date.now()}`,
      name: "New Column",
      width: 150,
      minWidth: 80,
      visible: true,
      sortable: false,
      resizable: true,
      type: "computed",
      aiPrompt: "Describe what information to extract from this paper...",
    };

    currentTableConfig.columns.push(newColumn);
    const tableStore = getTableStore();
    await tableStore.saveConfig(currentTableConfig);

    // Re-render the interface
    if (currentContainer && currentItem) {
      this.renderInterface(currentContainer, currentItem);

      // After render, find the new column header and open editor
      setTimeout(() => {
        const headerCells = doc.querySelectorAll("th[data-column-id]");
        const newColHeader = (Array.from(headerCells) as HTMLElement[]).find(
          (th) => th.getAttribute("data-column-id") === newColumn.id,
        );

        if (newColHeader) {
          this.showTableColumnEditPopover(
            doc,
            newColHeader,
            newColumn,
            item,
            container,
          );
        }
      }, 100);
    }
  }

  /**
   * Show inline editor popover for a table column header
   * Auto-saves changes on input blur/change
   */
  private static showTableColumnEditPopover(
    doc: Document,
    anchorEl: HTMLElement,
    column: TableColumn,
    item: Zotero.Item,
    container: HTMLElement,
  ): void {
    // Remove any existing popover
    const existing = doc.getElementById("table-column-editor-popover");
    if (existing) existing.remove();
    const existingBackdrop = doc.getElementById("table-column-editor-backdrop");
    if (existingBackdrop) existingBackdrop.remove();

    // Debounce timer for auto-save
    let saveDebounceTimer: ReturnType<typeof setTimeout> | null = null;

    const autoSave = async () => {
      if (saveDebounceTimer) clearTimeout(saveDebounceTimer);
      saveDebounceTimer = setTimeout(async () => {
        if (currentTableConfig) {
          const tableStore = getTableStore();
          await tableStore.saveConfig(currentTableConfig);
          // Update header text in place
          const headerText = anchorEl.querySelector(
            ".column-header-text",
          ) as HTMLElement;
          if (headerText) {
            headerText.innerText = column.name;
          }
        }
      }, 300);
    };

    // Backdrop for click-outside-to-close
    const backdrop = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "table-column-editor-backdrop" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        right: "0",
        bottom: "0",
        zIndex: "998",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            if (saveDebounceTimer) clearTimeout(saveDebounceTimer);
            backdrop.remove();
            const p = doc.getElementById("table-column-editor-popover");
            if (p) p.remove();
          },
        },
      ],
    });
    if (doc.body) {
      doc.body.appendChild(backdrop);
    } else {
      doc.documentElement?.appendChild(backdrop);
    }

    // Create popover
    const popover = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "table-column-editor-popover" },
      styles: {
        position: "fixed",
        zIndex: "999",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        padding: "12px",
        boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
        width: "280px",
        maxHeight: "450px",
        overflowY: "auto",
        display: "flex",
        flexDirection: "column",
        gap: "10px",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => e.stopPropagation(),
        },
      ],
    });

    // Title label
    const nameLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "Column Title" },
      styles: {
        fontSize: "11px",
        fontWeight: "600",
        color: "var(--text-secondary)",
      },
    });
    popover.appendChild(nameLabel);

    // Title input
    const nameInput = ztoolkit.UI.createElement(doc, "input", {
      properties: { value: column.name, placeholder: "Column Name" },
      styles: {
        padding: "8px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "13px",
        width: "100%",
        boxSizing: "border-box",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
      },
    }) as HTMLInputElement;

    nameInput.addEventListener("input", () => {
      column.name = nameInput.value;
      autoSave();
    });
    popover.appendChild(nameInput);

    // AI Prompt label
    const promptLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "AI Instructions" },
      styles: {
        fontSize: "11px",
        fontWeight: "600",
        color: "var(--text-secondary)",
        marginTop: "4px",
      },
    });
    popover.appendChild(promptLabel);

    // AI Prompt textarea
    const promptInput = ztoolkit.UI.createElement(doc, "textarea", {
      properties: {
        value: column.aiPrompt || "",
        placeholder: "E.g., Summarize the methodology...",
      },
      styles: {
        padding: "8px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        width: "100%",
        height: "80px",
        resize: "vertical",
        fontFamily: "inherit",
        boxSizing: "border-box",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        lineHeight: "1.4",
      },
    }) as HTMLTextAreaElement;

    promptInput.addEventListener("input", () => {
      column.aiPrompt = promptInput.value;
      autoSave();
    });
    popover.appendChild(promptInput);

    const paramContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "12px",
        padding: "12px",
        backgroundColor: "rgba(0,0,0,0.03)",
        border: "1px solid var(--border-secondary)",
        borderRadius: "8px",
        marginTop: "10px",
      },
    });

    const paramTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "AI Parameters" },
      styles: {
        fontSize: "11px",
        fontWeight: "800",
        color: "var(--highlight-primary)",
        marginBottom: "2px",
        textTransform: "uppercase",
        letterSpacing: "0.8px",
        borderBottom: "1px solid rgba(128,128,128,0.2)",
        paddingBottom: "4px",
      },
    });
    paramContainer.appendChild(paramTitle);

    // Temperature
    const tempRow = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexDirection: "column", gap: "4px" },
    });

    const tempHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        fontSize: "11px",
        fontWeight: "600",
        color: "var(--text-secondary)",
      },
    });
    const getEffectiveTableTemp = () => currentTableConfig?.temperature ?? 0.7;
    const getTempDisplayText = (val?: number) => {
      if (val !== undefined) return val.toFixed(1);
      const tableTemp = currentTableConfig?.temperature;
      if (tableTemp !== undefined) return `Default (${tableTemp.toFixed(1)})`;
      return "Default (0.7)";
    };

    const tempLabel = ztoolkit.UI.createElement(doc, "span", {
      properties: {
        innerText: `Temperature: ${getTempDisplayText(column.temperature)}`,
      },
    });
    const tempResetBtn = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "↺", title: "Reset to Default" },
      styles: {
        cursor: "pointer",
        display: column.temperature === undefined ? "none" : "inline-block",
        marginLeft: "6px",
      },
    });
    tempHeader.appendChild(tempLabel);
    tempHeader.appendChild(tempResetBtn);
    tempRow.appendChild(tempHeader);

    const tempSlider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "0",
        max: "2",
        step: "0.1",
        value: String(column.temperature ?? getEffectiveTableTemp()),
      },
      styles: { width: "100%", cursor: "pointer" },
    }) as HTMLInputElement;
    tempRow.appendChild(tempSlider);
    paramContainer.appendChild(tempRow);

    tempSlider.addEventListener("input", () => {
      const val = parseFloat(tempSlider.value);
      column.temperature = val;
      tempLabel.innerText = `Temperature: ${val.toFixed(1)}`;
      tempResetBtn.style.display = "inline-block";
      autoSave();
    });

    tempResetBtn.addEventListener("click", () => {
      column.temperature = undefined;
      tempLabel.innerText = `Temperature: ${getTempDisplayText(undefined)}`;
      tempSlider.value = String(getEffectiveTableTemp());
      tempResetBtn.style.display = "none";
      autoSave();
    });

    // Max Response Length Slider
    const maxLengthRow = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexDirection: "column", gap: "4px" },
    });

    const getMaxLenDisplayText = (val?: number) => {
      if (val !== undefined) {
        if (val === 0) return "Limitless";
        return `${val} words`;
      }
      const tableLen = currentTableConfig?.responseLength;
      if (tableLen === undefined) return "Default";
      if (tableLen === 0) return "Default (Limitless)";
      return `Default (${tableLen} words)`;
    };

    const maxLenHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        fontSize: "11px",
        fontWeight: "600",
        color: "var(--text-secondary)",
      },
    });
    const maxLenLabel = ztoolkit.UI.createElement(doc, "span", {
      properties: {
        innerText: `Max Length: ${getMaxLenDisplayText(column.maxTokens)}`,
      },
    });
    const maxLenResetBtn = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "↺", title: "Reset to Default" },
      styles: {
        cursor: "pointer",
        display: column.maxTokens === undefined ? "none" : "inline-block",
        marginLeft: "6px",
      },
    });
    maxLenHeader.appendChild(maxLenLabel);
    maxLenHeader.appendChild(maxLenResetBtn);
    maxLengthRow.appendChild(maxLenHeader);

    const getEffectiveMaxLen = () => currentTableConfig?.responseLength ?? 100;

    const maxLenSlider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "0",
        max: "2000",
        step: "10",
        value: String(column.maxTokens ?? getEffectiveMaxLen()),
      },
      styles: { width: "100%", cursor: "pointer" },
    }) as HTMLInputElement;
    maxLengthRow.appendChild(maxLenSlider);
    paramContainer.appendChild(maxLengthRow);

    maxLenSlider.addEventListener("input", () => {
      const val = parseInt(maxLenSlider.value);
      column.maxTokens = val;
      maxLenLabel.innerText = `Max Length: ${getMaxLenDisplayText(val)}`;
      maxLenResetBtn.style.display = "inline-block";
      autoSave();
    });

    maxLenResetBtn.addEventListener("click", () => {
      column.maxTokens = undefined;
      maxLenLabel.innerText = `Max Length: ${getMaxLenDisplayText(undefined)}`;
      maxLenSlider.value = String(getEffectiveMaxLen());
      maxLenResetBtn.style.display = "none";
      autoSave();
    });

    popover.appendChild(paramContainer);

    // Remove Column button
    const removeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "🗑️ Remove Column" },
      styles: {
        padding: "8px 12px",
        fontSize: "12px",
        color: "#c62828",
        border: "1px solid #c62828",
        borderRadius: "6px",
        backgroundColor: "transparent",
        cursor: "pointer",
        marginTop: "6px",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            if (currentTableConfig) {
              // Remove column from config
              currentTableConfig.columns = currentTableConfig.columns.filter(
                (c) => c.id !== column.id,
              );
              // Remove generated data for this column
              if (currentTableConfig.generatedData) {
                for (const paperId in currentTableConfig.generatedData) {
                  delete currentTableConfig.generatedData[paperId][column.id];
                }
              }
              const tableStore = getTableStore();
              await tableStore.saveConfig(currentTableConfig);
              backdrop.remove();
              popover.remove();
              // Re-render
              if (currentContainer && currentItem) {
                this.renderInterface(currentContainer, currentItem);
              }
            }
          },
        },
      ],
    });

    // Hover effect for remove button
    removeBtn.addEventListener("mouseenter", () => {
      removeBtn.style.backgroundColor = "#c62828";
      removeBtn.style.color = "white";
    });
    removeBtn.addEventListener("mouseleave", () => {
      removeBtn.style.backgroundColor = "transparent";
      removeBtn.style.color = "#c62828";
    });

    popover.appendChild(removeBtn);

    // Position popover below the anchor element
    const rect = anchorEl.getBoundingClientRect();
    const view = doc.defaultView || { innerHeight: 800, innerWidth: 1200 };
    const spaceBelow = view.innerHeight - rect.bottom;

    if (spaceBelow < 280) {
      // Position above if not enough space below
      popover.style.bottom = `${view.innerHeight - rect.top + 5}px`;
    } else {
      popover.style.top = `${rect.bottom + 5}px`;
    }

    // Horizontal positioning - try to align left edge with anchor
    let leftPos = rect.left;
    if (leftPos + 280 > view.innerWidth) {
      leftPos = view.innerWidth - 290;
    }
    popover.style.left = `${leftPos}px`;

    if (doc.body) {
      doc.body.appendChild(popover);
    } else {
      doc.documentElement?.appendChild(popover);
    }

    // Focus the name input
    nameInput.focus();
    nameInput.select();
  }

  // ==================== Search Results Table ====================

  /**
   * Helper to create a unified result row (Card + AI Columns)
   */
  private static createUnifiedResultRow(
    doc: Document,
    paper: SemanticScholarPaper,
    item: Zotero.Item,
    columns: SearchColumnConfig["columns"],
  ): HTMLTableRowElement {
    const tr = ztoolkit.UI.createElement(doc, "tr", {
      styles: {
        borderBottom: "1px solid var(--border-primary)",
      },
    }) as HTMLTableRowElement;

    // Hover effect
    tr.addEventListener("mouseenter", () => {
      tr.style.backgroundColor = "var(--background-secondary)";
    });
    tr.addEventListener("mouseleave", () => {
      tr.style.backgroundColor = "";
    });

    // === CARD CELL ===
    const cardCell = ztoolkit.UI.createElement(doc, "td", {
      styles: {
        padding: "0",
        width: "100%", // Take mostly all space if no columns
        verticalAlign: "top",
        minWidth: "0",
        overflow: "hidden",
      },
    });

    // Create the card with Table Cell mode (optional logic could reside in createSearchResultCard,
    // but here we just manually unset border since we returned HTMLElement)
    const card = this.createSearchResultCard(doc, paper, item, true);
    card.style.borderBottom = "none"; // Ensure no double border
    card.style.width = "100%";
    card.style.boxSizing = "border-box"; // Ensure padding doesn't overflow
    cardCell.appendChild(card);
    tr.appendChild(cardCell);

    // === AI COLUMN CELLS ===
    columns.forEach((col) => {
      const td = ztoolkit.UI.createElement(doc, "td", {
        styles: {
          padding: "12px",
          verticalAlign: "top",
          borderLeft: "1px solid var(--border-primary)",
          minWidth: "80px",
          maxWidth: "250px",
          backgroundColor: "rgba(0,0,0,0.01)", // Slight tint for AI columns
        },
      });

      const cachedValue =
        searchColumnConfig.generatedData[paper.paperId]?.[col.id];

      if (cachedValue) {
        const contentDiv = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            whiteSpace: "pre-wrap",
            lineHeight: "1.4",
            fontSize: "11px",
            color: "var(--text-secondary)",
            cursor: "pointer",
          },
          listeners: [
            {
              type: "click",
              listener: (e: Event) => {
                e.stopPropagation();
                // Find results container (ancestor)
                const resultsContainer = tr.closest(
                  "#semantic-scholar-results",
                );
                if (resultsContainer) {
                  this.showSearchCellDetailModal(
                    doc,
                    paper,
                    col,
                    cachedValue,
                    resultsContainer as HTMLElement,
                    item,
                  );
                }
              },
            },
          ],
        });
        contentDiv.innerHTML = parseMarkdown(cachedValue);
        td.appendChild(contentDiv);
      } else {
        // Generate button
        const genBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "✨ Generate" },
          styles: {
            padding: "6px 12px",
            fontSize: "11px",
            border: "1px solid var(--highlight-primary)",
            borderRadius: "4px",
            backgroundColor: "var(--background-primary)",
            color: "var(--highlight-primary)",
            cursor: "pointer",
            width: "100%",
          },
          listeners: [
            {
              type: "click",
              listener: async (e: Event) => {
                e.stopPropagation();
                const btn = e.target as HTMLButtonElement;
                const originalText = btn.innerText;
                btn.innerText = "⏳ Thinking...";
                btn.disabled = true;

                try {
                  const result = await this.analyzeSearchPaperColumn(
                    paper,
                    col,
                  );
                  td.innerHTML = "";
                  const contentDiv = ztoolkit.UI.createElement(doc, "div", {
                    styles: {
                      whiteSpace: "pre-wrap",
                      lineHeight: "1.4",
                      fontSize: "11px",
                      color: "var(--text-primary)",
                      cursor: "pointer",
                    },
                    listeners: [
                      {
                        type: "click",
                        listener: (e: Event) => {
                          e.stopPropagation();
                          const resultsContainer = tr.closest(
                            "#semantic-scholar-results",
                          );
                          if (resultsContainer) {
                            this.showSearchCellDetailModal(
                              doc,
                              paper,
                              col,
                              result,
                              resultsContainer as HTMLElement,
                              item,
                            );
                          }
                        },
                      },
                    ],
                  });
                  contentDiv.innerHTML = parseMarkdown(result);
                  td.appendChild(contentDiv);
                } catch (err) {
                  btn.innerText = "❌ Error";
                  btn.title = String(err);
                  setTimeout(() => {
                    btn.innerText = originalText;
                    btn.disabled = false;
                  }, 3000);
                }
              },
            },
          ],
        });
        td.appendChild(genBtn);
      }

      tr.appendChild(td);
    });

    return tr;
  }

  /**
   * Render the unified search results table (Card + Optional Columns)
   */
  private static renderUnifiedSearchResults(
    doc: Document,
    resultsContainer: HTMLElement,
    item: Zotero.Item,
  ): HTMLElement {
    // Main flex container: table on left, + button on right
    const wrapper = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "row",
        width: "100%",
        flex: "1",
        minHeight: "0",
        overflow: "hidden",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    // Scrollable table container
    const tableContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        flex: "1",
        overflow: "auto",
        minWidth: "0",
      },
    });

    // Create HTML table
    const table = ztoolkit.UI.createElement(doc, "table", {
      styles: {
        width: "100%",
        borderCollapse: "collapse",
        fontSize: "11px",
        tableLayout: "fixed", // CRITICAL FOR RESIZING
      },
    });

    // === TABLE HEADER ===
    // Only show header if we have AI columns
    if (searchColumnConfig.columns.length > 0) {
      const thead = ztoolkit.UI.createElement(doc, "thead", {});
      const headerRow = ztoolkit.UI.createElement(doc, "tr", {
        styles: {
          backgroundColor: "var(--background-tertiary)",
          position: "sticky",
          top: "0",
          zIndex: "10",
        },
      });

      // Paper column header (Main Card Column)

      // Paper column header (Main Card Column)
      const paperHeader = ztoolkit.UI.createElement(doc, "th", {
        properties: { innerText: "Paper Details" },
        styles: {
          padding: "8px 12px",
          textAlign: "left",
          fontWeight: "600",
          borderBottom: "2px solid var(--border-primary)",
          width: "300px", // Default start width
          color: "var(--text-primary)",
          position: "relative",
          userSelect: "none",
          boxSizing: "border-box",
          overflow: "hidden",
          whiteSpace: "nowrap",
          textOverflow: "ellipsis",
        },
      });

      // === RESIZER HANDLE FOR PAPER COLUMN ===
      const paperResizer = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          position: "absolute",
          right: "0",
          top: "0",
          bottom: "0",
          width: "5px",
          cursor: "col-resize",
          backgroundColor: "transparent",
          zIndex: "11",
        },
      });

      paperResizer.addEventListener("mouseenter", () => {
        paperResizer.style.backgroundColor = "var(--highlight-primary)";
      });
      paperResizer.addEventListener("mouseleave", () => {
        paperResizer.style.backgroundColor = "transparent";
      });

      paperResizer.addEventListener("mousedown", (e: MouseEvent) => {
        e.stopPropagation();
        e.preventDefault(); // Prevent text selection
        // Use rect for stable calculation
        const startLeft = paperHeader.getBoundingClientRect().left;

        // Set global cursor to prevent fluttering
        if (doc.body) doc.body.style.cursor = "col-resize";

        const onMouseMove = (moveEvent: MouseEvent) => {
          // Calculate exact width based on current mouse position relative to left edge
          const newWidth = moveEvent.clientX - startLeft;
          if (newWidth > 60) {
            // Ultra-low min width
            paperHeader.style.width = `${newWidth}px`;
            paperHeader.style.minWidth = `${newWidth}px`;
          }
        };

        const onMouseUp = () => {
          doc.removeEventListener("mousemove", onMouseMove);
          doc.removeEventListener("mouseup", onMouseUp);
          if (doc.body) doc.body.style.cursor = ""; // Reset cursor
        };

        doc.addEventListener("mousemove", onMouseMove);
        doc.addEventListener("mouseup", onMouseUp);
      });
      paperHeader.appendChild(paperResizer);
      headerRow.appendChild(paperHeader);

      // AI column headers
      searchColumnConfig.columns.forEach((col) => {
        const th = ztoolkit.UI.createElement(doc, "th", {
          styles: {
            padding: "8px 10px",
            textAlign: "left",
            fontWeight: "600",
            borderBottom: "2px solid var(--border-primary)",
            borderLeft: "1px solid var(--border-primary)",
            minWidth: `${Math.min(col.width || 200, 120)}px`, // Use saved width, cap minWidth
            width: `${col.width || 200}px`,
            maxWidth: "600px",
            color: "var(--text-primary)",
            position: "relative", // For absolute positioning of resizer
            userSelect: "none",
          },
        });

        // Column name with Edit capability (Title & Description adjustable)
        const headerContent = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            gap: "4px",
            cursor: "pointer",
            width: "100%",
          },
          listeners: [
            {
              type: "click",
              listener: (e: Event) => {
                e.stopPropagation();
                this.showColumnEditor(doc, th, col, resultsContainer, item);
              },
            },
          ],
        });

        // Name + Edit Hint Container
        const nameContainer = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            display: "flex",
            alignItems: "center",
            gap: "6px",
            overflow: "hidden",
          },
        });

        const colName = ztoolkit.UI.createElement(doc, "span", {
          properties: { innerText: col.name },
          attributes: { title: col.aiPrompt },
          styles: {
            whiteSpace: "nowrap",
            overflow: "hidden",
            textOverflow: "ellipsis",
            fontSize: "12px",
            fontWeight: "600",
          },
        });
        nameContainer.appendChild(colName);

        // Edit Icon (Pencil)
        const editIcon = ztoolkit.UI.createElement(doc, "span", {
          properties: { innerHTML: "✎" },
          styles: { fontSize: "12px", opacity: "0.4" },
        });

        // Hover effect for edit icon
        headerContent.addEventListener(
          "mouseenter",
          () => (editIcon.style.opacity = "1"),
        );
        headerContent.addEventListener(
          "mouseleave",
          () => (editIcon.style.opacity = "0.4"),
        );

        nameContainer.appendChild(editIcon);
        headerContent.appendChild(nameContainer);

        th.appendChild(headerContent);

        // === RESIZER HANDLE ===
        const resizer = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            position: "absolute",
            right: "0",
            top: "0",
            bottom: "0",
            width: "5px",
            cursor: "col-resize",
            backgroundColor: "transparent",
            zIndex: "11",
          },
        });

        resizer.addEventListener("mouseenter", () => {
          resizer.style.backgroundColor = "var(--highlight-primary)";
        });
        resizer.addEventListener("mouseleave", () => {
          resizer.style.backgroundColor = "transparent";
        });

        resizer.addEventListener("mousedown", (e: MouseEvent) => {
          e.stopPropagation();
          e.preventDefault();
          // Use rect for stable calculation
          const startLeft = th.getBoundingClientRect().left;

          if (doc.body) doc.body.style.cursor = "col-resize";

          const onMouseMove = (moveEvent: MouseEvent) => {
            const newWidth = moveEvent.clientX - startLeft;
            if (newWidth > 50) {
              // Min width
              th.style.width = `${newWidth}px`;
              th.style.minWidth = `${newWidth}px`;
            }
          };

          const onMouseUp = async () => {
            doc.removeEventListener("mousemove", onMouseMove);
            doc.removeEventListener("mouseup", onMouseUp);
            if (doc.body) doc.body.style.cursor = ""; // Reset cursor
            // Save new width
            col.width = parseInt(th.style.width);
            await saveSearchColumnConfig();
          };

          doc.addEventListener("mousemove", onMouseMove);
          doc.addEventListener("mouseup", onMouseUp);
        });

        th.appendChild(resizer);
        headerRow.appendChild(th);
      });

      thead.appendChild(headerRow);
      table.appendChild(thead);
    }

    // === TABLE BODY ===
    const tbody = ztoolkit.UI.createElement(doc, "tbody", {});
    currentSearchResults.forEach((paper) => {
      const tr = this.createUnifiedResultRow(
        doc,
        paper,
        item,
        searchColumnConfig.columns,
      );
      tbody.appendChild(tr);
    });
    table.appendChild(tbody);
    tableContainer.appendChild(table);

    // "Show More" button (Initial placement at bottom of table container)
    const showMoreBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📥 Show More", id: "show-more-btn" },
      styles: {
        display: "block",
        width: "calc(100% - 24px)",
        margin: "12px",
        padding: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "13px",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            const loadingDiv = ztoolkit.UI.createElement(doc, "div", {
              properties: { id: "show-more-loading" },
              styles: {
                textAlign: "center",
                padding: "16px",
                color: "var(--text-secondary)",
                fontSize: "12px",
              },
            });
            loadingDiv.innerHTML = "⏳ Loading more papers...";
            showMoreBtn.replaceWith(loadingDiv);
            await this.performSearch(doc);
          },
        },
      ],
    });
    tableContainer.appendChild(showMoreBtn);

    wrapper.appendChild(tableContainer);

    // === SIDE + BUTTON STRIP (Right Side) ===
    const sideStrip = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        width: "30px",
        minWidth: "30px",
        borderLeft: "1px solid var(--border-primary)",
        backgroundColor: "var(--background-secondary)",
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        gap: "8px",
        paddingTop: "8px",
      },
    });

    // Helper for side buttons
    const createSideBtn = (
      icon: string,
      title: string,
      onClick: (e: Event) => void,
    ) => {
      const btn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: icon },
        attributes: { title: title },
        styles: {
          width: "24px",
          height: "24px",
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          border: "1px solid var(--border-primary)",
          borderRadius: "4px",
          backgroundColor: "var(--background-primary)",
          cursor: "pointer",
          fontSize: "14px",
          color: "var(--text-primary)",
          transition: "all 0.2s ease",
        },
        listeners: [
          {
            type: "click",
            listener: onClick,
          },
        ],
      });

      // Hover effects
      btn.addEventListener("mouseenter", () => {
        btn.style.backgroundColor = "var(--highlight-primary)";
        btn.style.color = "var(--highlight-text)";
        btn.style.borderColor = "var(--highlight-primary)";
      });
      btn.addEventListener("mouseleave", () => {
        btn.style.backgroundColor = "var(--background-primary)";
        btn.style.color = "var(--text-primary)";
        btn.style.borderColor = "var(--border-primary)";
      });

      return btn;
    };

    // 1. (+) Add Column (Immediate)
    const addColumnBtn = createSideBtn("➕", "Add Analysis Column", (e) => {
      e.stopPropagation();
      this.addImmediateSearchColumn(doc, resultsContainer, item);
    });
    sideStrip.appendChild(addColumnBtn);

    // 2. (⚡) Generate All
    const generateAllBtn = createSideBtn("⚡", "Generate All Analysis", (e) => {
      e.stopPropagation();
      this.generateAllSearchColumns(doc, wrapper);
    });
    sideStrip.appendChild(generateAllBtn);

    // 3. (⚙️) Settings
    const settingsBtn = createSideBtn(
      "⚙️",
      "Manage Columns & Settings",
      (e) => {
        e.stopPropagation();
        this.showSearchSettingsPopover(
          doc,
          e.currentTarget as HTMLElement,
          resultsContainer,
          item,
        );
      },
    );
    sideStrip.appendChild(settingsBtn);

    wrapper.appendChild(sideStrip);

    return wrapper;
  }

  // ==================== Search Column Feature ====================

  /**
   * Show editor popover for a search column
   */
  private static showColumnEditor(
    doc: Document,
    anchorEl: HTMLElement,
    column: SearchAnalysisColumn,
    resultsContainer: HTMLElement,
    item: Zotero.Item,
  ): void {
    const existing = doc.getElementById("column-editor-popover");
    if (existing) existing.remove();
    const existingBackdrop = doc.getElementById("column-editor-backdrop");
    if (existingBackdrop) existingBackdrop.remove();

    // Backdrop
    const backdrop = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "column-editor-backdrop" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        right: "0",
        bottom: "0",
        zIndex: "998",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            backdrop.remove();
            const p = doc.getElementById("column-editor-popover");
            if (p) p.remove();
          },
        },
      ],
    });
    if (doc.body) {
      doc.body.appendChild(backdrop);
    } else {
      doc.documentElement?.appendChild(backdrop);
    }

    const popover = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "column-editor-popover" },
      styles: {
        position: "fixed",
        zIndex: "999",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        padding: "12px",
        boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
        width: "320px",
        display: "flex",
        flexDirection: "column",
        gap: "8px",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => e.stopPropagation(),
        },
      ],
    });

    // Input: Name
    const nameLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "Column Title" },
      styles: {
        fontSize: "11px",
        fontWeight: "600",
        color: "var(--text-secondary)",
      },
    });
    popover.appendChild(nameLabel);

    const nameInput = ztoolkit.UI.createElement(doc, "input", {
      properties: { value: column.name, placeholder: "Column Name" },
      styles: {
        padding: "6px 8px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "13px",
        width: "100%",
        boxSizing: "border-box",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
      },
    });
    popover.appendChild(nameInput);

    // Textarea: Prompt
    const promptLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "AI Instructions" },
      styles: {
        fontSize: "11px",
        fontWeight: "600",
        color: "var(--text-secondary)",
      },
    });
    popover.appendChild(promptLabel);

    const promptInput = ztoolkit.UI.createElement(doc, "textarea", {
      properties: {
        value: column.aiPrompt,
        placeholder: "E.g., Summarize the methodology...",
      },
      styles: {
        padding: "8px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        fontSize: "12px",
        width: "100%",
        height: "100px",
        resize: "vertical",
        fontFamily: "inherit",
        boxSizing: "border-box",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        lineHeight: "1.4",
      },
    });
    popover.appendChild(promptInput);

    // Actions: Save, Remove
    const actions = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        marginTop: "8px",
      },
    });

    const removeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "🗑️ Remove Column" },
      styles: {
        padding: "6px 10px",
        fontSize: "11px",
        color: "var(--button-clear-text)",
        border: "1px solid var(--button-clear-border)",
        borderRadius: "4px",
        backgroundColor: "var(--button-clear-background)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            // Simple confirm
            // Note: ztoolkit doesn't have confirm modal, using native for speed or could build one.
            // Given constraints, I'll execute immediately but maybe safer to ask?
            // User said "adjustable at any time".
            searchColumnConfig.columns = searchColumnConfig.columns.filter(
              (c) => c.id !== column.id,
            );
            for (const pId in searchColumnConfig.generatedData) {
              delete searchColumnConfig.generatedData[pId][column.id];
            }
            await saveSearchColumnConfig();
            backdrop.remove();
            popover.remove();
            this.renderSearchResults(doc, resultsContainer, item);
          },
        },
      ],
    });
    actions.appendChild(removeBtn);

    const saveBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Save Changes" },
      styles: {
        padding: "6px 12px",
        fontSize: "11px",
        fontWeight: "600",
        color: "var(--highlight-text)",
        backgroundColor: "var(--highlight-primary)",
        border: "1px solid var(--highlight-primary)",
        borderRadius: "4px",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            column.name = nameInput.value;
            column.aiPrompt = promptInput.value;
            await saveSearchColumnConfig();
            backdrop.remove();
            popover.remove();
            this.renderSearchResults(doc, resultsContainer, item);
          },
        },
      ],
    });
    actions.appendChild(saveBtn);
    popover.appendChild(actions);

    // Positioning
    const rect = anchorEl.getBoundingClientRect();
    // Check if fits below
    const view = doc.defaultView || { innerHeight: 800, innerWidth: 1200 };
    const spaceBelow = view.innerHeight - rect.bottom;
    if (spaceBelow < 250) {
      popover.style.bottom = `${view.innerHeight - rect.top + 5}px`;
    } else {
      popover.style.top = `${rect.bottom + 5}px`;
    }

    // Horizontal clamping
    let leftObj = rect.left;
    if (leftObj + 320 > view.innerWidth) {
      leftObj = view.innerWidth - 330;
    }
    popover.style.left = `${leftObj}px`;

    if (doc.body) {
      doc.body.appendChild(popover);
    } else {
      doc.documentElement?.appendChild(popover);
    }
    nameInput.focus();
  }

  /**
   * Immediately add a new analysis column
   */
  private static async addImmediateSearchColumn(
    doc: Document,
    container: HTMLElement,
    item: Zotero.Item,
  ): Promise<void> {
    const newCol: SearchAnalysisColumn = {
      id: Math.random().toString(36).substring(2, 9),
      name: "New Analysis",
      aiPrompt: "Summarize this paper in one sentence.",
      width: 250,
    };
    searchColumnConfig.columns.push(newCol);
    await saveSearchColumnConfig();
    this.renderSearchResults(doc, container, item);
  }

  /**
   * Trigger batch generation for all empty cells
   */
  /**
   * Trigger batch generation for all empty cells
   */
  private static async generateAllSearchColumns(
    doc: Document,
    container: HTMLElement,
  ): Promise<void> {
    // Find all "Generate" buttons within the container and click them
    const buttons = Array.from(container.querySelectorAll("button")).filter(
      (b) => (b as HTMLElement).innerText?.includes("Generate"),
    ) as HTMLButtonElement[];

    if (buttons.length === 0) {
      return;
    }

    // Click them with a slight stagger to avoid UI freeze (though they are async)
    buttons.forEach((btn, index) => {
      setTimeout(() => {
        if (!btn.disabled) btn.click();
      }, index * 50);
    });
  }

  /**
   * Show search settings popover (Presets, Response Length)
   */
  private static showSearchSettingsPopover(
    doc: Document,
    anchorEl: HTMLElement,
    container: HTMLElement,
    item: Zotero.Item,
  ): void {
    const existing = doc.getElementById("settings-popover");
    if (existing) existing.remove();
    const existingBackdrop = doc.getElementById("settings-backdrop");
    if (existingBackdrop) existingBackdrop.remove();

    const backdrop = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "settings-backdrop" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        right: "0",
        bottom: "0",
        zIndex: "998",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            backdrop.remove();
            const p = doc.getElementById("settings-popover");
            if (p) p.remove();
          },
        },
      ],
    });

    const popover = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "settings-popover" },
      styles: {
        position: "fixed",
        backgroundColor: "var(--background-primary)",
        width: "320px",
        maxHeight: "400px",
        overflowY: "auto",
        borderRadius: "8px",
        boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
        border: "1px solid var(--border-primary)",
        padding: "16px",
        display: "flex",
        flexDirection: "column",
        gap: "12px",
        zIndex: "999",
      },
      listeners: [
        { type: "click", listener: (e: Event) => e.stopPropagation() },
      ],
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "Search Settings" },
      styles: {
        fontSize: "14px",
        fontWeight: "600",
        borderBottom: "1px solid var(--border-primary)",
        paddingBottom: "8px",
      },
    });
    popover.appendChild(header);

    // === AI Model Selection ===
    const modelSection = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexDirection: "column", gap: "4px" },
    });
    const modelLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "AI Model" },
      styles: { fontSize: "12px", fontWeight: "600" },
    });
    modelSection.appendChild(modelLabel);

    const modelSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        width: "100%",
        padding: "6px 8px",
        borderRadius: "4px",
        fontSize: "12px",
        border: "1px solid var(--border-primary)",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
    }) as HTMLSelectElement;

    const configs = getModelConfigs();
    const activeConfig = getActiveModelConfig();

    if (configs.length === 0) {
      const opt = doc.createElement("option");
      opt.value = "default";
      opt.innerText = "Default (configure in Settings)";
      modelSelect.appendChild(opt);
    } else {
      configs.forEach((cfg) => {
        const opt = doc.createElement("option");
        opt.value = cfg.id;
        opt.innerText = cfg.name;
        if (activeConfig && cfg.id === activeConfig.id) opt.selected = true;
        modelSelect.appendChild(opt);
      });
    }

    modelSelect.addEventListener("change", () => {
      setActiveModelId(modelSelect.value);
      Zotero.debug(`[seerai] Search: Model changed to ${modelSelect.value}`);
    });
    modelSection.appendChild(modelSelect);
    popover.appendChild(modelSection);

    // === Response Length ===
    const lengthSection = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexDirection: "column", gap: "8px" },
    });
    const currentLen = searchColumnConfig.responseLength || 100;
    const lenText = currentLen > 4200 ? "Limitless" : `${currentLen} words`;
    const lenLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: `Max Response Length: ${lenText}` },
      styles: { fontSize: "12px", fontWeight: "600" },
    });
    lengthSection.appendChild(lenLabel);

    const sliderContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", alignItems: "center", gap: "10px" },
    });
    const slider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "20",
        max: "4300",
        step: "10",
        value: String(currentLen),
      },
      styles: { flex: "1" },
    });
    sliderContainer.appendChild(slider);
    lengthSection.appendChild(sliderContainer);

    slider.addEventListener("input", async () => {
      const val = parseInt((slider as HTMLInputElement).value);
      if (val > 4200) {
        lenLabel.innerText = `Max Response Length: Limitless`;
      } else {
        lenLabel.innerText = `Max Response Length: ${val} words`;
      }
      searchColumnConfig.responseLength = val;
      // Debounce save? Or simple save.
      await saveSearchColumnConfig();
    });
    popover.appendChild(lengthSection);

    // === Presets ===
    const presetsSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        marginTop: "10px",
      },
    });
    const presetsHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      },
    });
    presetsHeader.appendChild(
      ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: "Column Presets" },
        styles: { fontSize: "12px", fontWeight: "600" },
      }),
    );

    // Save Current Preset UI (Inline)
    const saveRow = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "6px", alignItems: "center" },
    });

    const saveInput = ztoolkit.UI.createElement(doc, "input", {
      properties: { placeholder: "New Preset Name" },
      styles: {
        flex: "1",
        padding: "4px 6px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
      },
    });
    saveRow.appendChild(saveInput);

    const savePresetBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "💾 Save" },
      styles: {
        padding: "4px 8px",
        fontSize: "11px",
        borderRadius: "4px",
        border: "1px solid var(--highlight-primary)",
        color: "var(--highlight-text)",
        backgroundColor: "var(--highlight-primary)",
        cursor: "pointer",
      },
    });
    saveRow.appendChild(savePresetBtn);
    presetsHeader.appendChild(saveRow);
    presetsSection.appendChild(presetsHeader);

    const presetsList = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        maxHeight: "150px",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        padding: "4px",
      },
    });

    // Load presets logic
    const loadPresets = () => {
      presetsList.innerHTML = "";
      let savedPresets: any = {};
      try {
        const raw = Zotero.Prefs.get("extensions.seer-ai.search.presets");
        if (raw) savedPresets = JSON.parse(raw as string);
      } catch (e) {
        Zotero.debug(`Error loading presets: ${e}`);
      }

      if (Object.keys(savedPresets).length === 0) {
        const empty = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: "No saved presets" },
          styles: {
            padding: "10px",
            fontSize: "11px",
            color: "var(--text-secondary)",
            textAlign: "center",
          },
        });
        presetsList.appendChild(empty);
      } else {
        Object.keys(savedPresets).forEach((key) => {
          const itemRow = ztoolkit.UI.createElement(doc, "div", {
            styles: {
              display: "flex",
              justifyContent: "space-between",
              padding: "6px 8px",
              alignItems: "center",
              borderBottom: "1px solid var(--border-secondary)",
            },
          });

          const nameSpan = ztoolkit.UI.createElement(doc, "span", {
            properties: { innerText: key },
            styles: { fontSize: "12px", fontWeight: "500" },
          });
          itemRow.appendChild(nameSpan);

          const btns = ztoolkit.UI.createElement(doc, "div", {
            styles: { display: "flex", gap: "4px" },
          });

          // Load
          const loadBtn = ztoolkit.UI.createElement(doc, "button", {
            namespace: "html",
            properties: { innerText: "Load" },
            styles: {
              fontSize: "10px",
              padding: "2px 6px",
              cursor: "pointer",
              backgroundColor: "var(--highlight-primary)",
              color: "white",
              border: "none",
              borderRadius: "3px",
            },
            listeners: [
              {
                type: "click",
                listener: async () => {
                  if (
                    doc.defaultView &&
                    doc.defaultView.confirm(
                      `Load preset "${key}"? Current columns will be replaced.`,
                    )
                  ) {
                    searchColumnConfig.columns = savedPresets[key];
                    await saveSearchColumnConfig();
                    backdrop.remove();
                    popover.remove();
                    this.renderSearchResults(doc, container, item);
                  }
                },
              },
            ],
          });
          btns.appendChild(loadBtn);

          // Delete
          const delBtn = ztoolkit.UI.createElement(doc, "button", {
            namespace: "html",
            properties: { innerText: "✕" },
            styles: {
              fontSize: "10px",
              padding: "2px 6px",
              cursor: "pointer",
              backgroundColor: "transparent",
              color: "var(--text-secondary)",
              border: "1px solid var(--border-secondary)",
              borderRadius: "3px",
            },
            listeners: [
              {
                type: "click",
                listener: () => {
                  if (
                    doc.defaultView &&
                    doc.defaultView.confirm(`Delete preset "${key}"?`)
                  ) {
                    delete savedPresets[key];
                    Zotero.Prefs.set(
                      "extensions.seer-ai.search.presets",
                      JSON.stringify(savedPresets),
                    );
                    loadPresets();
                  }
                },
              },
            ],
          });
          btns.appendChild(delBtn);

          itemRow.appendChild(btns);
          presetsList.appendChild(itemRow);
        });
      }
    };

    savePresetBtn.addEventListener("click", () => {
      const name = (saveInput as HTMLInputElement).value;
      if (name && name.trim()) {
        let savedPresets: any = {};
        try {
          const raw = Zotero.Prefs.get("extensions.seer-ai.search.presets");
          if (raw) savedPresets = JSON.parse(raw as string);
        } catch (e) {
          /* intentionally ignore parse errors */
        }

        savedPresets[name.trim()] = searchColumnConfig.columns;
        Zotero.Prefs.set(
          "extensions.seer-ai.search.presets",
          JSON.stringify(savedPresets),
        );
        (saveInput as HTMLInputElement).value = ""; // Clear input
        loadPresets();
      } else {
        (saveInput as HTMLInputElement).style.borderColor = "red";
        setTimeout(() => {
          (saveInput as HTMLInputElement).style.borderColor =
            "var(--border-primary)";
        }, 2000);
      }
    });

    loadPresets();
    presetsSection.appendChild(presetsList);
    popover.appendChild(presetsSection);

    // Footer / Close (Optional for popover, but nice to have)
    /*
            const footer = ztoolkit.UI.createElement(doc, 'div', { styles: { display: "flex", justifyContent: "flex-end", marginTop: "10px" } });
            const closeBtn = ztoolkit.UI.createElement(doc, 'button', {
      namespace: "html",
            namespace: "html",
                properties: { innerText: "Close" },
                styles: { padding: "4px 10px", cursor: "pointer", backgroundColor: "var(--background-secondary)", border: "1px solid var(--border-primary)", borderRadius: "4px", fontSize: "11px" },
                listeners: [{ type: "click", listener: () => { backdrop.remove(); popover.remove(); } }]
            });
            footer.appendChild(closeBtn);
            popover.appendChild(footer);
            */

    // Positioning
    const rect = anchorEl.getBoundingClientRect();
    const view = doc.defaultView || { innerHeight: 800, innerWidth: 1200 };

    // Default: to the left of button
    let top = rect.top;
    let left = rect.left - 330; // Width + margin

    if (left < 10) {
      // Not enough space left? Right side? (Unlikely for side strip on right edge)
      // Or below?
      left = rect.right - 320;
      top = rect.bottom + 5;
    }

    // Vertical Clamping
    const h = 400; // max height
    if (top + h > view.innerHeight) {
      top = view.innerHeight - h - 10;
    }
    if (top < 10) top = 10;

    popover.style.top = `${top}px`;
    popover.style.left = `${left}px`;

    if (doc.body) {
      doc.body.appendChild(backdrop);
      doc.body.appendChild(popover);
    } else {
      doc.documentElement?.appendChild(backdrop);
      doc.documentElement?.appendChild(popover);
    }
  }

  /**
   * Show table settings popover (Response Length, Presets, Columns)
   * Matches the search settings popover pattern
   */
  private static showTableSettingsPopover(
    doc: Document,
    anchorEl: HTMLElement,
    container: HTMLElement,
    item: Zotero.Item,
  ): void {
    const existing = doc.getElementById("table-settings-popover");
    if (existing) existing.remove();
    const existingBackdrop = doc.getElementById("table-settings-backdrop");
    if (existingBackdrop) existingBackdrop.remove();

    const backdrop = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "table-settings-backdrop" },
      styles: {
        position: "fixed",
        top: "0",
        left: "0",
        right: "0",
        bottom: "0",
        zIndex: "998",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            backdrop.remove();
            const p = doc.getElementById("table-settings-popover");
            if (p) p.remove();
          },
        },
      ],
    });

    const popover = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "table-settings-popover" },
      styles: {
        position: "fixed",
        backgroundColor: "var(--background-primary)",
        width: "320px",
        maxHeight: "450px",
        overflowY: "auto",
        borderRadius: "8px",
        boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
        border: "1px solid var(--border-primary)",
        padding: "16px",
        display: "flex",
        flexDirection: "column",
        gap: "12px",
        zIndex: "999",
      },
      listeners: [
        { type: "click", listener: (e: Event) => e.stopPropagation() },
      ],
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "Table Settings" },
      styles: {
        fontSize: "14px",
        fontWeight: "600",
        borderBottom: "1px solid var(--border-primary)",
        paddingBottom: "8px",
      },
    });
    popover.appendChild(header);

    // === AI Model Selection ===
    const modelSection = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexDirection: "column", gap: "4px" },
    });
    const modelLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "AI Model" },
      styles: { fontSize: "12px", fontWeight: "600" },
    });
    modelSection.appendChild(modelLabel);

    const modelSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        width: "100%",
        padding: "6px 8px",
        borderRadius: "4px",
        fontSize: "12px",
        border: "1px solid var(--border-primary)",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
      },
    }) as HTMLSelectElement;

    const configs = getModelConfigs();
    const activeConfig = getActiveModelConfig();

    if (configs.length === 0) {
      const opt = doc.createElement("option");
      opt.value = "default";
      opt.innerText = "Default (configure in Settings)";
      modelSelect.appendChild(opt);
    } else {
      configs.forEach((cfg) => {
        const opt = doc.createElement("option");
        opt.value = cfg.id;
        opt.innerText = cfg.name;
        if (activeConfig && cfg.id === activeConfig.id) opt.selected = true;
        modelSelect.appendChild(opt);
      });
    }

    modelSelect.addEventListener("change", () => {
      setActiveModelId(modelSelect.value);
      Zotero.debug(`[seerai] Table: Model changed to ${modelSelect.value}`);
    });
    modelSection.appendChild(modelSelect);
    popover.appendChild(modelSection);

    // === Response Length ===
    const lengthSection = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", flexDirection: "column", gap: "8px" },
    });

    const currentLen = currentTableConfig?.responseLength;
    const getLenText = (len?: number) => {
      if (len === undefined) return "Default";
      if (len === 0 || len >= 4192) return "Limitless";
      return `${len} words`;
    };

    const lenLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: {
        innerText: `Max Response Length: ${getLenText(currentLen)}`,
      },
      styles: { fontSize: "12px", fontWeight: "600" },
    });

    const lenHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      },
    });
    lenHeader.appendChild(lenLabel);

    const lenResetBtn = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "↺", title: "Reset to Default" },
      styles: {
        fontSize: "14px",
        cursor: "pointer",
        color: "var(--text-tertiary, #999)",
        display: currentLen === undefined ? "none" : "inline-block",
      },
    });
    lenHeader.appendChild(lenResetBtn);
    lengthSection.appendChild(lenHeader);

    const sliderContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", alignItems: "center", gap: "10px" },
    });

    const sliderValue =
      currentLen === undefined || currentLen === 0 ? 4200 : currentLen;
    const lengthSlider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "20",
        max: "4300",
        step: "10",
        value: String(sliderValue),
      },
      styles: { flex: "1" },
    }) as HTMLInputElement;
    sliderContainer.appendChild(lengthSlider);
    lengthSection.appendChild(sliderContainer);

    lengthSlider.addEventListener("input", async () => {
      const val = parseInt(lengthSlider.value);
      lenLabel.innerText = `Max Response Length: ${val > 4200 ? "Limitless" : val + " words"}`;
      lenResetBtn.style.display = "inline-block";
      if (currentTableConfig) {
        currentTableConfig.responseLength = val > 4200 ? 0 : val;
        await getTableStore().saveConfig(currentTableConfig);
      }
    });

    lenResetBtn.addEventListener("click", async (e) => {
      e.stopPropagation();
      if (currentTableConfig) {
        currentTableConfig.responseLength = undefined;
        await getTableStore().saveConfig(currentTableConfig);
      }
      lenLabel.innerText = `Max Response Length: Default`;
      lengthSlider.value = "4200";
      lenResetBtn.style.display = "none";
    });

    popover.appendChild(lengthSection);

    // === Temperature ===
    const tempSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        marginTop: "4px",
      },
    });

    const currentTemp = currentTableConfig?.temperature;
    const getTempText = (t?: number) =>
      t === undefined ? "Default" : t.toFixed(1);

    const tempLabel = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: `Temperature: ${getTempText(currentTemp)}` },
      styles: { fontSize: "12px", fontWeight: "600" },
    });

    const tempHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      },
    });
    tempHeader.appendChild(tempLabel);

    const tempResetBtn = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "↺", title: "Reset to Default" },
      styles: {
        fontSize: "14px",
        cursor: "pointer",
        color: "var(--text-tertiary, #999)",
        display: currentTemp === undefined ? "none" : "inline-block",
      },
    });
    tempHeader.appendChild(tempResetBtn);
    tempSection.appendChild(tempHeader);

    const tempSlider = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "range",
        min: "0",
        max: "2",
        step: "0.1",
        value: String(currentTemp ?? 0.7),
      },
      styles: { flex: "1", cursor: "pointer" },
    }) as HTMLInputElement;

    tempSlider.addEventListener("input", async () => {
      const val = parseFloat(tempSlider.value);
      tempLabel.innerText = `Temperature: ${val.toFixed(1)}`;
      tempResetBtn.style.display = "inline-block";
      if (currentTableConfig) {
        currentTableConfig.temperature = val;
        await getTableStore().saveConfig(currentTableConfig);
      }
    });

    tempResetBtn.addEventListener("click", async (e) => {
      e.stopPropagation();
      if (currentTableConfig) {
        currentTableConfig.temperature = undefined;
        await getTableStore().saveConfig(currentTableConfig);
      }
      tempLabel.innerText = `Temperature: Default`;
      tempSlider.value = "0.7";
      tempResetBtn.style.display = "none";
    });

    tempSection.appendChild(tempSlider);
    popover.appendChild(tempSection);

    // === Column Presets ===

    // === Column Presets ===
    const presetsSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        marginTop: "10px",
      },
    });
    const presetsHeader = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      },
    });
    presetsHeader.appendChild(
      ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: "Column Presets" },
        styles: { fontSize: "12px", fontWeight: "600" },
      }),
    );

    // Save row
    const saveRow = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "6px", alignItems: "center" },
    });

    const saveInput = ztoolkit.UI.createElement(doc, "input", {
      properties: { placeholder: "New Preset Name" },
      styles: {
        flex: "1",
        padding: "4px 6px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
      },
    }) as HTMLInputElement;
    saveRow.appendChild(saveInput);

    const savePresetBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "💾 Save" },
      styles: {
        padding: "4px 8px",
        fontSize: "11px",
        borderRadius: "4px",
        border: "1px solid var(--highlight-primary)",
        color: "var(--highlight-text)",
        backgroundColor: "var(--highlight-primary)",
        cursor: "pointer",
      },
    });
    saveRow.appendChild(savePresetBtn);
    presetsHeader.appendChild(saveRow);
    presetsSection.appendChild(presetsHeader);

    const presetsList = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        maxHeight: "100px",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        padding: "4px",
      },
    });

    // Load presets logic using tableStore
    const loadPresets = async () => {
      presetsList.innerHTML = "";
      const tableStore = getTableStore();
      const presets = await tableStore.loadPresets();

      if (presets.length === 0) {
        const empty = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: "No saved presets" },
          styles: {
            padding: "10px",
            fontSize: "11px",
            color: "var(--text-secondary)",
            textAlign: "center",
          },
        });
        presetsList.appendChild(empty);
      } else {
        presets.forEach((preset) => {
          const itemRow = ztoolkit.UI.createElement(doc, "div", {
            styles: {
              display: "flex",
              justifyContent: "space-between",
              padding: "6px 8px",
              alignItems: "center",
              borderBottom: "1px solid var(--border-secondary)",
            },
          });

          const nameSpan = ztoolkit.UI.createElement(doc, "span", {
            properties: { innerText: preset.name },
            styles: { fontSize: "12px", fontWeight: "500" },
          });
          itemRow.appendChild(nameSpan);

          const btns = ztoolkit.UI.createElement(doc, "div", {
            styles: { display: "flex", gap: "4px" },
          });

          // Load
          const loadBtn = ztoolkit.UI.createElement(doc, "button", {
            namespace: "html",
            properties: { innerText: "Load" },
            styles: {
              fontSize: "10px",
              padding: "2px 6px",
              cursor: "pointer",
              backgroundColor: "var(--highlight-primary)",
              color: "white",
              border: "none",
              borderRadius: "3px",
            },
            listeners: [
              {
                type: "click",
                listener: async () => {
                  if (
                    doc.defaultView &&
                    doc.defaultView.confirm(
                      `Load preset "${preset.name}"? Current columns will be replaced.`,
                    )
                  ) {
                    if (currentTableConfig) {
                      currentTableConfig.columns = [...preset.columns];
                      await tableStore.saveConfig(currentTableConfig);
                      backdrop.remove();
                      popover.remove();
                      if (currentContainer && currentItem) {
                        this.renderInterface(currentContainer, currentItem);
                      }
                    }
                  }
                },
              },
            ],
          });
          btns.appendChild(loadBtn);

          // Delete
          const delBtn = ztoolkit.UI.createElement(doc, "button", {
            namespace: "html",
            properties: { innerText: "✕" },
            styles: {
              fontSize: "10px",
              padding: "2px 6px",
              cursor: "pointer",
              backgroundColor: "transparent",
              color: "var(--text-secondary)",
              border: "1px solid var(--border-secondary)",
              borderRadius: "3px",
            },
            listeners: [
              {
                type: "click",
                listener: async () => {
                  if (
                    doc.defaultView &&
                    doc.defaultView.confirm(`Delete preset "${preset.name}"?`)
                  ) {
                    await tableStore.deletePreset(preset.id);
                    loadPresets();
                  }
                },
              },
            ],
          });
          btns.appendChild(delBtn);

          itemRow.appendChild(btns);
          presetsList.appendChild(itemRow);
        });
      }
    };

    savePresetBtn.addEventListener("click", async () => {
      const name = saveInput.value.trim();
      if (name && currentTableConfig) {
        const newPreset: ColumnPreset = {
          id: `preset_${Date.now()}`,
          name: name,
          columns: [...currentTableConfig.columns],
          createdAt: new Date().toISOString(),
        };
        const tableStore = getTableStore();
        await tableStore.savePreset(newPreset);
        saveInput.value = "";
        loadPresets();
      } else {
        saveInput.style.borderColor = "red";
        setTimeout(() => {
          saveInput.style.borderColor = "var(--border-primary)";
        }, 2000);
      }
    });

    loadPresets();
    presetsSection.appendChild(presetsList);
    popover.appendChild(presetsSection);

    // === Columns List ===
    const columnsSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "8px",
        marginTop: "10px",
      },
    });
    columnsSection.appendChild(
      ztoolkit.UI.createElement(doc, "div", {
        properties: { innerText: "Column Visibility" },
        styles: { fontSize: "12px", fontWeight: "600" },
      }),
    );

    const columnsList = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        maxHeight: "120px",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        padding: "4px",
        backgroundColor: "var(--background-secondary)",
      },
    });

    const columns = currentTableConfig?.columns || defaultColumns;
    const coreColumnIds = [
      "title",
      "author",
      "year",
      "sources",
      "zoteroKey",
      "DOI",
    ];
    columns.forEach((col) => {
      const row = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          display: "flex",
          alignItems: "center",
          gap: "8px",
          padding: "6px 8px",
          borderBottom: "1px solid var(--border-primary)",
        },
      });

      const checkbox = ztoolkit.UI.createElement(doc, "input", {
        attributes: { type: "checkbox" },
      }) as HTMLInputElement;
      checkbox.checked = col.visible;
      checkbox.style.cursor = "pointer";
      checkbox.addEventListener("change", async () => {
        col.visible = checkbox.checked;
        if (currentTableConfig) {
          const tableStore = getTableStore();
          await tableStore.saveConfig(currentTableConfig);
        }
      });

      const label = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: col.name },
        styles: { flex: "1", fontSize: "12px" },
      });

      row.appendChild(checkbox);
      row.appendChild(label);

      // Delete button for non-core columns
      if (!coreColumnIds.includes(col.id)) {
        const deleteBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "🗑" },
          attributes: { title: "Delete column" },
          styles: {
            background: "none",
            border: "none",
            fontSize: "12px",
            cursor: "pointer",
            color: "#c62828",
            padding: "2px 4px",
          },
          listeners: [
            {
              type: "click",
              listener: async (e: Event) => {
                e.preventDefault();
                e.stopPropagation();
                if (
                  currentTableConfig &&
                  doc.defaultView?.confirm(`Delete column "${col.name}"?`)
                ) {
                  currentTableConfig.columns =
                    currentTableConfig.columns.filter((c) => c.id !== col.id);
                  const tableStore = getTableStore();
                  await tableStore.saveConfig(currentTableConfig);
                  row.remove();
                  // Refresh the interface
                  if (currentContainer && currentItem) {
                    backdrop.remove();
                    popover.remove();
                    this.renderInterface(currentContainer, currentItem);
                  }
                }
              },
            },
          ],
        });
        row.appendChild(deleteBtn);
      }

      columnsList.appendChild(row);
    });

    columnsSection.appendChild(columnsList);
    popover.appendChild(columnsSection);

    // Positioning (left of anchor button)
    const rect = anchorEl.getBoundingClientRect();
    const view = doc.defaultView || { innerHeight: 800, innerWidth: 1200 };

    let top = rect.top;
    let left = rect.left - 330;

    if (left < 10) {
      left = rect.right - 320;
      top = rect.bottom + 5;
    }

    const h = 450;
    if (top + h > view.innerHeight) {
      top = view.innerHeight - h - 10;
    }
    if (top < 10) top = 10;

    popover.style.top = `${top}px`;
    popover.style.left = `${left}px`;

    if (doc.body) {
      doc.body.appendChild(backdrop);
      doc.body.appendChild(popover);
    } else {
      doc.documentElement?.appendChild(backdrop);
      doc.documentElement?.appendChild(popover);
    }
  }

  /**
  
       * Show dropdown to add a new analysis column for search results
       * (Deprecated, replaced by immediate add + settings)
       */
  private static showSearchColumnDropdown(
    doc: Document,
    anchorEl: HTMLElement,
    resultsContainer: HTMLElement,
    item: Zotero.Item,
  ): void {
    // Deprecated
  }

  /**
   * Create horizontal bar showing active search columns as tags
   */
  private static createSearchColumnTagsBar(
    doc: Document,
    resultsContainer: HTMLElement,
    item: Zotero.Item,
  ): HTMLElement {
    const tagsBar = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexWrap: "wrap",
        gap: "6px",
        padding: "8px 12px",
        backgroundColor: "var(--background-tertiary)",
        borderBottom: "1px solid var(--border-primary)",
        alignItems: "center",
      },
    });

    const label = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "📊 Columns:" },
      styles: {
        fontSize: "11px",
        color: "var(--text-secondary)",
        marginRight: "4px",
      },
    });
    tagsBar.appendChild(label);

    searchColumnConfig.columns.forEach((col) => {
      const tag = ztoolkit.UI.createElement(doc, "span", {
        styles: {
          display: "inline-flex",
          alignItems: "center",
          gap: "4px",
          padding: "3px 8px",
          backgroundColor: "var(--highlight-primary)",
          color: "var(--highlight-text)",
          borderRadius: "12px",
          fontSize: "11px",
          fontWeight: "500",
        },
      });

      const nameSpan = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: col.name },
      });
      tag.appendChild(nameSpan);

      // Remove button
      const removeBtn = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: "✕" },
        styles: {
          cursor: "pointer",
          marginLeft: "2px",
          opacity: "0.8",
          fontSize: "10px",
        },
        listeners: [
          {
            type: "click",
            listener: async (e: Event) => {
              e.stopPropagation();
              // Remove column
              searchColumnConfig.columns = searchColumnConfig.columns.filter(
                (c) => c.id !== col.id,
              );
              // Remove generated data for this column
              for (const paperId in searchColumnConfig.generatedData) {
                delete searchColumnConfig.generatedData[paperId][col.id];
              }
              await saveSearchColumnConfig();
              // Re-render
              this.renderSearchResults(doc, resultsContainer, item);
            },
          },
        ],
      });
      tag.appendChild(removeBtn);

      tagsBar.appendChild(tag);
    });

    return tagsBar;
  }

  /**
   * Create column values section for a search result card
   */
  private static createSearchCardColumns(
    doc: Document,
    paper: SemanticScholarPaper,
    resultsContainer: HTMLElement,
    item: Zotero.Item,
  ): HTMLElement {
    const columnsSection = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        marginTop: "8px",
        padding: "8px",
        backgroundColor: "var(--background-secondary)",
        borderRadius: "6px",
        border: "1px solid var(--border-primary)",
      },
    });

    searchColumnConfig.columns.forEach((col) => {
      const cachedValue =
        searchColumnConfig.generatedData[paper.paperId]?.[col.id];

      const row = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          display: "flex",
          marginBottom: "6px",
          alignItems: "flex-start",
        },
      });

      // Column name
      const nameEl = ztoolkit.UI.createElement(doc, "span", {
        properties: { innerText: `${col.name}:` },
        styles: {
          fontSize: "11px",
          fontWeight: "600",
          color: "var(--text-primary)",
          minWidth: "100px",
          marginRight: "8px",
        },
      });
      row.appendChild(nameEl);

      // Value or generate button
      const valueEl = ztoolkit.UI.createElement(doc, "div", {
        styles: {
          flex: "1",
          fontSize: "11px",
          color: "var(--text-secondary)",
          lineHeight: "1.4",
        },
      });

      if (cachedValue) {
        valueEl.textContent = cachedValue;
      } else {
        // Generate button
        const generateBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "🔄 Generate" },
          styles: {
            padding: "3px 8px",
            fontSize: "10px",
            border: "1px solid var(--border-primary)",
            borderRadius: "4px",
            backgroundColor: "var(--background-primary)",
            color: "var(--text-primary)",
            cursor: "pointer",
          },
          listeners: [
            {
              type: "click",
              listener: async (e: Event) => {
                e.stopPropagation();
                const btn = e.target as HTMLButtonElement;
                btn.textContent = "⏳ Analyzing...";
                btn.disabled = true;

                try {
                  const result = await this.analyzeSearchPaperColumn(
                    paper,
                    col,
                  );
                  // Update display
                  valueEl.textContent = result;
                } catch (err) {
                  valueEl.textContent = `❌ Error: ${err}`;
                }
              },
            },
          ],
        });
        valueEl.appendChild(generateBtn);
      }

      row.appendChild(valueEl);
      columnsSection.appendChild(row);
    });

    return columnsSection;
  }

  /**
   * Analyze a search paper for a specific column using AI
   * Uses PDF if available, otherwise falls back to abstract/TLDR/metadata
   */
  private static async analyzeSearchPaperColumn(
    paper: SemanticScholarPaper,
    column: SearchAnalysisColumn,
  ): Promise<string> {
    let sourceText = "";

    // Priority 1: Use abstract + TLDR (most common for search results)
    if (paper.abstract) {
      sourceText = `Abstract: ${paper.abstract}`;
    }
    if (paper.tldr?.text) {
      sourceText += `\n\nTLDR: ${paper.tldr.text}`;
    }

    // Priority 2: Fall back to metadata if no abstract
    if (!sourceText.trim()) {
      const authors = paper.authors?.map((a) => a.name).join(", ") || "Unknown";
      sourceText = `Title: ${paper.title}\nAuthors: ${authors}\nYear: ${paper.year || "Unknown"}\nVenue: ${paper.venue || "Unknown"}\nCitations: ${paper.citationCount}`;
    }

    // Generate using AI
    const result = await this.generateSearchColumnContent(
      paper,
      column,
      sourceText,
    );

    // Cache result
    if (!searchColumnConfig.generatedData[paper.paperId]) {
      searchColumnConfig.generatedData[paper.paperId] = {};
    }
    searchColumnConfig.generatedData[paper.paperId][column.id] = result;
    await saveSearchColumnConfig();

    return result;
  }

  /**
   * Generate column content for a search result using AI
   */
  private static async generateSearchColumnContent(
    paper: SemanticScholarPaper,
    column: SearchAnalysisColumn,
    sourceText: string,
  ): Promise<string> {
    const systemPrompt = `You are extracting structured information from academic papers. Be concise and factual. Return ONLY the requested information, no explanations.`;

    const limit = searchColumnConfig.responseLength || 100;
    const lengthConstraint =
      limit > 4200 ? "" : `Keep response under ${limit} words.`;

    const userPrompt = `Paper: "${paper.title}"

Source content:
${sourceText}

Task: For the column "${column.name}": ${column.aiPrompt}
${lengthConstraint}`;

    const activeModel = getActiveModelConfig();
    if (!activeModel) {
      throw new Error(
        "No AI model configured. Please set up a model in settings.",
      );
    }

    let fullResponse = "";
    await openAIService.chatCompletionStream(
      [
        { role: "system", content: systemPrompt },
        { role: "user", content: userPrompt },
      ],
      {
        onToken: (token) => {
          fullResponse += token;
        },
        onComplete: () => {},
        onError: (err) => {
          throw err;
        },
      },
      {
        apiURL: activeModel.apiURL,
        apiKey: activeModel.apiKey,
        model: activeModel.model,
      },
    );

    return fullResponse.trim();
  }

  /**
   * Export table data to CSV
   */
  private static async exportTableToCSV(): Promise<void> {
    try {
      const tableData = await this.loadTableData();
      if (tableData.rows.length === 0) {
        Zotero.debug("[seerai] No data to export");
        return;
      }

      const columns =
        currentTableConfig?.columns.filter((c) => c.visible) ||
        defaultColumns.filter((c) => c.visible);

      // Build CSV header
      const header = columns.map((c) => `"${c.name}"`).join(",");

      // Build CSV rows
      const rows = tableData.rows.map((row) => {
        return columns
          .map((col) => {
            const value = row.data[col.id] || "";
            // Escape quotes and wrap in quotes
            return `"${value.replace(/"/g, '""')}"`;
          })
          .join(",");
      });

      const csvContent = [header, ...rows].join("\n");

      // Create file path
      const filename = `papers_table_${new Date().toISOString().replace(/[:.]/g, "-")}.csv`;
      const downloadsDir = PathUtils.join(
        Zotero.DataDirectory.dir,
        "seerai",
        "exports",
      );

      // Ensure directory exists
      if (!(await IOUtils.exists(downloadsDir))) {
        await IOUtils.makeDirectory(downloadsDir, { ignoreExisting: true });
      }

      const filepath = PathUtils.join(downloadsDir, filename);
      const encoder = new TextEncoder();
      await IOUtils.write(filepath, encoder.encode(csvContent));

      Zotero.debug(`[seerai] Table exported to: ${filepath}`);

      // Show success notification
      const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
      progressWindow.changeHeadline("Export Complete");
      progressWindow.addDescription(`Table exported to:\n${filepath}`);
      progressWindow.show();
      progressWindow.startCloseTimer(3000);
    } catch (e) {
      Zotero.debug(`[seerai] Error exporting table: ${e}`);
    }
  }

  /**
   * Copy all table data to clipboard as structured markdown
   */
  private static async copyAllTableData(doc: Document): Promise<void> {
    try {
      const tableData = await this.loadTableData();
      if (tableData.rows.length === 0) {
        return;
      }

      const columns =
        currentTableConfig?.columns.filter((c) => c.visible) ||
        defaultColumns.filter((c) => c.visible);

      const references: string[] = [];
      const paperDetails: string[] = [];

      tableData.rows.forEach((row, index) => {
        const refNum = index + 1;
        const title = row.data["title"] || "Untitled";
        const author = row.data["author"] || "Unknown";
        const year = row.data["year"] || "";
        const zoteroKey = row.data["zoteroKey"] || "";

        references.push(
          `[${refNum}]${zoteroKey ? ` ${zoteroKey} —` : ""} ${author}${year ? `, ${year}` : ""} — ${title}`,
        );

        let paperSection = `### [${refNum}] ${title}\n`;

        if (author) paperSection += `**Authors:** ${author}\n`;
        if (year) paperSection += `**Year:** ${year}\n`;
        if (row.data["zoteroKey"])
          paperSection += `**Zotero Key:** ${row.data["zoteroKey"]}\n`;
        if (row.data["DOI"]) paperSection += `**DOI:** ${row.data["DOI"]}\n`;

        columns.forEach((col) => {
          if (["title", "author", "year", "zoteroKey", "DOI"].includes(col.id))
            return;
          const value = row.data[col.id];
          if (value) {
            paperSection += `**${col.name}:** ${value}\n`;
          }
        });

        paperDetails.push(paperSection);
      });

      const textContent = [
        "## References",
        ...references,
        "",
        "## Detailed Data",
        "",
        ...paperDetails,
      ].join("\n");

      new ztoolkit.Clipboard().addText(textContent, "text/unicode").copy();

      const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
      progressWindow.changeHeadline("Copied to Clipboard");
      progressWindow.addDescription(
        `${tableData.rows.length} papers copied as structured markdown`,
      );
      progressWindow.show();
      progressWindow.startCloseTimer(2000);

      Zotero.debug("[seerai] Table data copied to clipboard");
    } catch (e) {
      Zotero.debug(`[seerai] Error copying table data: ${e}`);
    }
  }

  /**
   * Show popup near the button asking user to pick MD or CSV, then export table to workspace
   */
  private static async showExportToWorkspacePopup(
    doc: Document,
    anchorBtn: HTMLElement,
  ): Promise<void> {
    const existing = doc.getElementById("export-workspace-popup");
    if (existing) {
      existing.remove();
      return;
    }

    const tableData = await this.loadTableData();
    if (tableData.rows.length === 0) {
      doc.defaultView?.alert("No papers in table to export.");
      return;
    }

    const columns =
      currentTableConfig?.columns.filter((c) => c.visible) ||
      defaultColumns.filter((c) => c.visible);

    const popup = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "export-workspace-popup" },
      styles: {
        position: "fixed",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
        padding: "16px",
        zIndex: "10000",
        minWidth: "220px",
      },
    });

    const rect = anchorBtn.getBoundingClientRect();
    popup.style.top = `${rect.bottom + 4}px`;
    popup.style.left = `${rect.left}px`;

    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        fontSize: "12px",
        fontWeight: "600",
        marginBottom: "12px",
        color: "var(--text-primary)",
      },
    });
    header.textContent = `Export ${tableData.rows.length} rows as:`;
    popup.appendChild(header);

    const doExport = async (format: "md" | "csv") => {
      popup.remove();
      const tableName = currentTableConfig?.name || "Untitled Workspace";
      const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
      const safeName = tableName.replace(/[^a-zA-Z0-9_\- ]/g, "_");
      const filename = `tables/${safeName}_${timestamp}.${format}`;

      const content =
        format === "md"
          ? this.buildTableMarkdown(tableData, columns, tableName)
          : this.buildTableCSV(tableData, columns);

      try {
        const store = getWorkspaceStore();
        await store.writeFile(filename, content);
        const progressWindow = new Zotero.ProgressWindow({
          closeOnClick: true,
        });
        progressWindow.changeHeadline("Exported to Workspace");
        progressWindow.addDescription(`Table saved to workspace → ${filename}`);
        progressWindow.show();
        progressWindow.startCloseTimer(3000);
      } catch (e) {
        Zotero.debug(`[seerai] Error exporting table to workspace: ${e}`);
        doc.defaultView?.alert(`Failed to export table: ${e}`);
      }
    };

    const createBtn = (label: string, format: "md" | "csv"): HTMLElement => {
      const btn = ztoolkit.UI.createElement(doc, "button", {
        namespace: "html",
        properties: { innerText: label },
        styles: {
          display: "block",
          width: "100%",
          padding: "8px 16px",
          fontSize: "12px",
          fontWeight: "500",
          border: "1px solid var(--border-primary)",
          borderRadius: "4px",
          backgroundColor: "var(--background-primary)",
          color: "var(--text-primary)",
          cursor: "pointer",
          textAlign: "left",
          marginBottom: "4px",
        },
        listeners: [
          {
            type: "click",
            listener: () => doExport(format),
          },
        ],
      });
      return btn;
    };

    popup.appendChild(createBtn("Markdown (.md)", "md"));
    popup.appendChild(createBtn("CSV (.csv)", "csv"));

    if (doc.body) {
      doc.body.appendChild(popup);
    } else {
      doc.documentElement?.appendChild(popup);
    }

    const closeHandler = (e: Event) => {
      const target = e.target as Node;
      if (!popup.contains(target)) {
        popup.remove();
        doc.removeEventListener("mousedown", closeHandler);
      }
    };
    setTimeout(() => {
      doc.addEventListener("mousedown", closeHandler);
    }, 0);
  }

  /**
   * Build a markdown table string from table data
   */
  private static buildTableMarkdown(
    tableData: TableData,
    columns: TableColumn[],
    tableName: string,
  ): string {
    const lines: string[] = [];
    lines.push(`# ${tableName}`);
    lines.push("");
    lines.push(`Total papers: ${tableData.rows.length}`);
    lines.push("");
    lines.push("## References");
    lines.push("");

    const paperDetails: string[] = [];

    tableData.rows.forEach((row, index) => {
      const refNum = index + 1;
      const title = row.data["title"] || "Untitled";
      const author = row.data["author"] || "Unknown";
      const year = row.data["year"] || "";
      const zoteroKey = row.data["zoteroKey"] || "";

      lines.push(
        `[${refNum}]${zoteroKey ? ` ${zoteroKey} —` : ""} ${author}${year ? `, ${year}` : ""} — ${title}`,
      );

      let paperSection = `### [${refNum}] ${title}\n`;
      if (author) paperSection += `**Authors:** ${author}\n`;
      if (year) paperSection += `**Year:** ${year}\n`;
      if (row.data["zoteroKey"])
        paperSection += `**Zotero Key:** ${row.data["zoteroKey"]}\n`;
      if (row.data["DOI"]) paperSection += `**DOI:** ${row.data["DOI"]}\n`;

      columns.forEach((col) => {
        if (["title", "author", "year", "zoteroKey", "DOI"].includes(col.id))
          return;
        const value = row.data[col.id];
        if (value) {
          paperSection += `**${col.name}:** ${value}\n`;
        }
      });

      paperDetails.push(paperSection);
    });

    lines.push("");
    lines.push("## Detailed Data");
    lines.push("");
    lines.push(...paperDetails);

    lines.push("");
    return lines.join("\n");
  }

  /**
   * Build a CSV string from table data
   */
  private static buildTableCSV(
    tableData: TableData,
    columns: TableColumn[],
  ): string {
    const header = columns
      .map((c) => `"${(c.name || c.id).replace(/"/g, '""')}"`)
      .join(",");

    const rows = tableData.rows.map((row) => {
      return columns
        .map((col) => {
          const value = row.data[col.id] || "";
          return `"${value.replace(/"/g, '""')}"`;
        })
        .join(",");
    });

    return [header, ...rows].join("\n");
  }

  /**
   * Find existing "Tables" note for a given paper item
   * Returns the note item if found, null otherwise
   */
  private static findExistingTablesNote(
    parentItemId: number,
  ): Zotero.Item | null {
    try {
      const parentItem = Zotero.Items.get(parentItemId);
      if (!parentItem || !parentItem.isRegularItem()) return null;

      const noteIDs = parentItem.getNotes();
      for (const noteID of noteIDs) {
        const note = Zotero.Items.get(noteID);
        if (note) {
          const noteContent = note.getNote();
          // Check if note has the Tables marker
          if (noteContent.includes("<h2>📊 Tables")) {
            return note as Zotero.Item;
          }
        }
      }
      return null;
    } catch (e) {
      Zotero.debug(`[seerai] Error finding Tables note: ${e}`);
      return null;
    }
  }

  /**
   * Parse existing table data from a Tables note
   * Returns a map of columnId -> value
   */
  private static parseTablesNoteContent(
    noteContent: string,
  ): Record<string, string> {
    const data: Record<string, string> = {};
    try {
      // Extract table rows using regex
      // Format: <tr><td>ColumnName</td><td>Value</td></tr>
      const rowRegex =
        /<tr>\s*<td[^>]*>([^<]+)<\/td>\s*<td[^>]*>([\s\S]*?)<\/td>\s*<\/tr>/gi;
      let match;
      while ((match = rowRegex.exec(noteContent)) !== null) {
        const columnName = match[1].trim();
        const value = match[2].trim();
        // Use column name as key (we'll match by name)
        data[columnName] = value;
      }
    } catch (e) {
      Zotero.debug(`[seerai] Error parsing Tables note: ${e}`);
    }
    return data;
  }

  /**
   * Generate HTML table content for a row
   */
  private static generateTablesNoteHtml(
    paperTitle: string,
    row: TableRow,
    columns: TableColumn[],
  ): string {
    const timestamp = new Date().toLocaleString();

    let tableRows = "";
    for (const col of columns) {
      if (!col.visible) continue;
      // Skip non-data columns like title (already in header)
      if (col.id === "title") continue;

      const value = row.data[col.id] || "";
      const escapedValue = value
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/\n/g, "<br/>");

      tableRows += `    <tr><td style="padding: 8px; border: 1px solid #ddd; font-weight: 600;">${col.name}</td><td style="padding: 8px; border: 1px solid #ddd;">${escapedValue}</td></tr>\n`;
    }

    return `<h2>📊 Tables - ${paperTitle.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</h2>
<table style="border-collapse: collapse; width: 100%; margin: 10px 0;">
  <thead>
    <tr style="background: #f5f5f5;">
      <th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Column</th>
      <th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Value</th>
    </tr>
  </thead>
  <tbody>
${tableRows}  </tbody>
</table>
<p style="color: #888; font-size: 11px;"><em>Last updated: ${timestamp}</em></p>`;
  }

  /**
   * Save a single table row as a Zotero note attached to the source paper
   * If a Tables note already exists, merges new columns without duplicating
   */
  private static async saveRowAsNote(
    row: TableRow,
    columns: TableColumn[],
  ): Promise<boolean> {
    try {
      const parentItem = Zotero.Items.get(row.paperId);
      if (!parentItem || !parentItem.isRegularItem()) {
        Zotero.debug(
          `[seerai] Cannot save note: Invalid parent item ${row.paperId}`,
        );
        return false;
      }

      const paperTitle = (parentItem.getField("title") as string) || "Untitled";

      // Check for existing Tables note
      const existingNote = this.findExistingTablesNote(row.paperId);

      if (existingNote) {
        // Merge: Parse existing data and update with new columns
        const existingData = this.parseTablesNoteContent(
          existingNote.getNote(),
        );

        // Create merged row data - preserve existing values, add new ones
        const mergedData: TableRow = {
          ...row,
          data: { ...row.data },
        };

        // For each column, if existing note has a value and current row doesn't, use existing
        for (const colName in existingData) {
          const col = columns.find((c) => c.name === colName);
          if (
            col &&
            (!mergedData.data[col.id] || mergedData.data[col.id].trim() === "")
          ) {
            mergedData.data[col.id] = existingData[colName];
          }
        }

        // Generate updated HTML
        const newContent = this.generateTablesNoteHtml(
          paperTitle,
          mergedData,
          columns,
        );
        existingNote.setNote(newContent);
        await existingNote.saveTx();

        Zotero.debug(
          `[seerai] Updated existing Tables note for: ${paperTitle}`,
        );
      } else {
        // Create new note
        const note = new Zotero.Item("note");
        note.libraryID = parentItem.libraryID;
        note.parentID = parentItem.id;

        const noteContent = this.generateTablesNoteHtml(
          paperTitle,
          row,
          columns,
        );
        note.setNote(noteContent);
        await note.saveTx();

        Zotero.debug(`[seerai] Created new Tables note for: ${paperTitle}`);
      }

      return true;
    } catch (e) {
      Zotero.debug(`[seerai] Error saving row as note: ${e}`);
      return false;
    }
  }

  /**
   * Save all table rows as notes (batch operation)
   */
  private static async saveAllRowsAsNotes(doc: Document): Promise<void> {
    try {
      const tableData = await this.loadTableData();
      if (tableData.rows.length === 0) {
        Zotero.debug("[seerai] No rows to save as notes");
        return;
      }

      const columns = currentTableConfig?.columns || defaultColumns;

      // Show progress
      const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
      progressWindow.changeHeadline("Saving Table as Notes");
      progressWindow.addDescription("Processing...");
      progressWindow.show();

      let saved = 0;
      let failed = 0;

      for (const row of tableData.rows) {
        const success = await this.saveRowAsNote(row, columns);
        if (success) {
          saved++;
        } else {
          failed++;
        }
      }

      progressWindow.changeHeadline("Save Complete");
      progressWindow.addDescription(`Saved: ${saved} | Failed: ${failed}`);
      progressWindow.startCloseTimer(3000);

      Zotero.debug(
        `[seerai] Batch save complete: ${saved} saved, ${failed} failed`,
      );
    } catch (e) {
      Zotero.debug(`[seerai] Error in batch save: ${e}`);
    }
  }

  /**
   * Create the selection chips area
   */
  private static createSelectionArea(
    doc: Document,
    stateManager: ReturnType<typeof getChatStateManager>,
  ): HTMLElement {
    // Return hidden element to maintain layout compatibility if needed, but empty
    return ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "none" },
      properties: { id: "selection-area" },
    });
  }

  /**
   * Show tag picker as a beautiful inline dropdown panel for Chat
   */
  private static async showTagPicker(
    doc: Document,
    stateManager: ReturnType<typeof getChatStateManager>,
  ): Promise<void> {
    // Toggle existing dropdown
    const existing = doc.getElementById(
      "chat-tag-picker-dropdown",
    ) as HTMLElement;
    if (existing) {
      existing.style.opacity = "0";
      existing.style.transform = "translateY(-10px)";
      setTimeout(() => existing.remove(), 200);
      return;
    }

    const selectionArea = doc.getElementById("selection-area");
    if (!selectionArea || !selectionArea.parentNode) return;

    // Create dropdown
    const dropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "chat-tag-picker-dropdown" },
      styles: {
        backgroundColor: "var(--background-primary)",
        borderRadius: "8px",
        padding: "0",
        boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
        overflow: "hidden",
        border: "1px solid var(--border-primary)",
        transition: "all 0.2s ease-out",
        opacity: "0",
        transform: "translateY(-10px)",
        marginTop: "8px",
        marginLeft: "8px",
        marginRight: "8px",
      },
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        background:
          "linear-gradient(135deg, var(--highlight-primary) 0%, color-mix(in srgb, var(--highlight-primary) 80%, orange) 100%)", // Orange hint for tags
        padding: "10px 14px",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        gap: "12px",
      },
    });

    const headerTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "🏷️ Add Tags to Chat" },
      styles: {
        fontSize: "13px",
        fontWeight: "600",
        color: "var(--highlight-text)",
        textShadow: "0 1px 2px rgba(0,0,0,0.1)",
      },
    });
    header.appendChild(headerTitle);

    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "rgba(0,0,0,0.1)",
        border: "none",
        borderRadius: "50%",
        width: "22px",
        height: "22px",
        cursor: "pointer",
        color: "var(--highlight-text)",
        fontSize: "11px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            dropdown.style.opacity = "0";
            setTimeout(() => dropdown.remove(), 200);
          },
        },
      ],
    });
    header.appendChild(closeBtn);
    dropdown.appendChild(header);

    // Content
    const content = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "10px 14px",
        display: "flex",
        flexDirection: "column",
        gap: "8px",
      },
    });

    // Controls
    const controlsRow = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "6px", alignItems: "center" },
    });

    // Filter Select
    const filterSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        flex: "0 0 auto",
        minWidth: "120px",
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
        outline: "none",
      },
    }) as HTMLSelectElement;

    // Populate filter (using getAllCollections helper or populateFilterSelect)
    // Note: populateFilterSelect adds "All Libraries" etc. works well.
    this.populateFilterSelect(filterSelect);
    controlsRow.appendChild(filterSelect);

    // Search
    const searchInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "text", placeholder: "🔍 Search tags..." },
      styles: {
        flex: "1",
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        outline: "none",
      },
    }) as HTMLInputElement;
    controlsRow.appendChild(searchInput);
    content.appendChild(controlsRow);

    // List
    const listContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        maxHeight: "240px",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
      },
    });
    content.appendChild(listContainer);
    dropdown.appendChild(content);

    // Tags Logic
    let allTags: { tag: string }[] = [];

    const renderTags = () => {
      listContainer.innerHTML = "";
      const searchQuery = searchInput.value.toLowerCase();
      const filtered = allTags
        .filter((t) => t.tag.toLowerCase().includes(searchQuery))
        .slice(0, 50);

      if (filtered.length === 0) {
        listContainer.appendChild(
          ztoolkit.UI.createElement(doc, "div", {
            properties: { innerText: "No tags found." },
            styles: {
              padding: "20px",
              textAlign: "center",
              color: "var(--text-secondary)",
              fontSize: "12px",
            },
          }),
        );
        return;
      }

      filtered.forEach((t) => {
        const row = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "8px 10px",
            borderBottom: "1px solid var(--border-primary)",
            cursor: "pointer",
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
          },
        });
        row.addEventListener("mouseenter", () => {
          row.style.backgroundColor = "var(--background-primary)";
        });
        row.addEventListener("mouseleave", () => {
          row.style.backgroundColor = "";
        });

        const label = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: t.tag },
          styles: {
            fontSize: "12px",
            color: "var(--text-primary)",
            fontWeight: "500",
          },
        });
        row.appendChild(label);

        const addBtn = ztoolkit.UI.createElement(doc, "button", {
          namespace: "html",
          properties: { innerText: "+" },
          styles: {
            width: "24px",
            height: "24px",
            borderRadius: "50%",
            border: "1px solid var(--highlight-primary)",
            backgroundColor: "transparent",
            color: "var(--highlight-primary)",
            fontSize: "16px",
            fontWeight: "bold",
            cursor: "pointer",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            transition: "all 0.15s",
          },
          listeners: [
            {
              type: "click",
              listener: async (e: Event) => {
                e.stopPropagation();
                // Add tag to state
                stateManager.addSelection("tags", {
                  id: t.tag,
                  type: "tag",
                  title: t.tag,
                });

                // Add items with this tag
                const filterVal = filterSelect.value;
                let libId: number | null = null;
                let colId: number | null = null;
                if (filterVal.startsWith("lib_"))
                  libId = parseInt(filterVal.replace("lib_", ""));
                if (filterVal.startsWith("col_"))
                  colId = parseInt(filterVal.replace("col_", ""));

                await this.addItemsByTags([t.tag], colId, libId);
                this.reRenderSelectionArea();

                // Feedback
                row.style.backgroundColor = "var(--background-hover)";
                addBtn.replaceWith(
                  ztoolkit.UI.createElement(doc, "span", {
                    properties: { innerText: "✓" },
                    styles: {
                      fontSize: "14px",
                      color: "green",
                      fontWeight: "bold",
                    },
                  }),
                );
              },
            },
          ],
        });
        addBtn.addEventListener("mouseenter", () => {
          addBtn.style.backgroundColor = "var(--highlight-primary)";
          addBtn.style.color = "var(--highlight-text)";
        });
        addBtn.addEventListener("mouseleave", () => {
          addBtn.style.backgroundColor = "transparent";
          addBtn.style.color = "var(--highlight-primary)";
        });

        row.appendChild(addBtn);
        listContainer.appendChild(row);
      });
    };

    const loadTags = async () => {
      // ... Fetch tags logic ...
      listContainer.innerHTML = "";
      listContainer.appendChild(
        ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: "Loading tags..." },
          styles: {
            padding: "20px",
            textAlign: "center",
            color: "var(--text-secondary)",
            fontSize: "12px",
          },
        }),
      );

      const filterValue = filterSelect.value;
      try {
        let tags: { tag: string }[] = [];
        if (filterValue === "all") {
          const libraries = Zotero.Libraries.getAll();
          for (const lib of libraries) {
            const libTags = await Zotero.Tags.getAll(lib.libraryID);
            tags.push(...libTags.map((t: any) => ({ tag: t.tag })));
          }
        } else if (filterValue.startsWith("lib_")) {
          const libId = parseInt(filterValue.replace("lib_", ""), 10);
          const libTags = await Zotero.Tags.getAll(libId);
          tags = libTags.map((t: any) => ({ tag: t.tag }));
        } else if (filterValue.startsWith("col_")) {
          const colId = parseInt(filterValue.replace("col_", ""), 10);
          const col = Zotero.Collections.get(colId);
          if (col) {
            const itemIDs = col.getChildItems(true);
            const tagSet = new Set<string>();
            for (const id of itemIDs) {
              const it = Zotero.Items.get(id);
              if (it && it.isRegularItem()) {
                it.getTags().forEach((t: any) => tagSet.add(t.tag));
              }
            }
            tags = Array.from(tagSet).map((t) => ({ tag: t }));
          }
        }
        // Deduplicate
        const uniqueTags = new Map();
        tags.forEach((t) => uniqueTags.set(t.tag, t));
        allTags = Array.from(uniqueTags.values()).sort((a, b) =>
          a.tag.localeCompare(b.tag),
        );

        renderTags();
      } catch (e) {
        Zotero.debug(`Error loading tags: ${e}`);
        listContainer.innerText = "Error loading tags.";
      }
    };

    filterSelect.addEventListener("change", loadTags);
    searchInput.addEventListener("input", renderTags);

    selectionArea.parentNode.insertBefore(dropdown, selectionArea.nextSibling);

    // Init
    // Auto focus
    setTimeout(() => searchInput.focus(), 100);
    setTimeout(() => {
      dropdown.style.opacity = "1";
      dropdown.style.transform = "translateY(0)";
    }, 10);
    loadTags();
  }

  /**
   * Show paper picker as a beautiful inline dropdown panel for Chat
   */
  private static async showPaperPicker(
    doc: Document,
    stateManager: ReturnType<typeof getChatStateManager>,
  ): Promise<void> {
    // Toggle existing dropdown
    const existing = doc.getElementById(
      "chat-paper-picker-dropdown",
    ) as HTMLElement;
    if (existing) {
      existing.style.opacity = "0";
      existing.style.transform = "translateY(-10px)";
      setTimeout(() => existing.remove(), 200);
      return;
    }

    const selectionArea = doc.getElementById("selection-area");
    if (!selectionArea || !selectionArea.parentNode) return;

    // Create dropdown panel
    const dropdown = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "chat-paper-picker-dropdown" },
      styles: {
        backgroundColor: "var(--background-primary)",
        borderRadius: "8px",
        padding: "0",
        boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
        overflow: "hidden",
        border: "1px solid var(--border-primary)",
        transition: "all 0.2s ease-out",
        opacity: "0",
        transform: "translateY(-10px)",
        marginTop: "8px",
        marginLeft: "8px",
        marginRight: "8px",
      },
    });

    // Header
    const header = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        background:
          "linear-gradient(135deg, var(--highlight-primary) 0%, color-mix(in srgb, var(--highlight-primary) 80%, purple) 100%)",
        padding: "10px 14px",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        gap: "12px",
      },
    });

    const headerTitle = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "📚 Add Papers to Chat" },
      styles: {
        fontSize: "13px",
        fontWeight: "600",
        color: "var(--highlight-text)",
        textShadow: "0 1px 2px rgba(0,0,0,0.1)",
      },
    });
    header.appendChild(headerTitle);

    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "✕" },
      styles: {
        background: "rgba(0,0,0,0.1)",
        border: "none",
        borderRadius: "50%",
        width: "22px",
        height: "22px",
        cursor: "pointer",
        color: "var(--highlight-text)",
        fontSize: "11px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            dropdown.style.opacity = "0";
            dropdown.style.transform = "translateY(-10px)";
            setTimeout(() => dropdown.remove(), 200);
          },
        },
      ],
    });
    header.appendChild(closeBtn);
    dropdown.appendChild(header);

    // Content
    const content = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "10px 14px",
        display: "flex",
        flexDirection: "column",
        gap: "8px",
      },
    });

    // Controls
    const controlsRow = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "6px", alignItems: "center" },
    });

    // Filter Select
    const filterSelect = ztoolkit.UI.createElement(doc, "select", {
      styles: {
        flex: "0 0 auto",
        minWidth: "120px",
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
        outline: "none",
      },
    }) as HTMLSelectElement;
    this.populateFilterSelect(filterSelect);
    controlsRow.appendChild(filterSelect);

    // Search Input
    const searchInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "text", placeholder: "🔍 Search papers..." },
      styles: {
        flex: "1",
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        outline: "none",
      },
    }) as HTMLInputElement;
    controlsRow.appendChild(searchInput);
    content.appendChild(controlsRow);

    // List Container
    const listContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        maxHeight: "240px",
        overflowY: "auto",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
      },
    });
    content.appendChild(listContainer);

    // State
    let allFilteredItems: Zotero.Item[] = [];
    let displayedCount = 0;
    const BATCH_SIZE = 50;

    const renderPaperBatch = (
      items: Zotero.Item[],
      startIndex: number,
      count: number,
    ) => {
      const endIndex = Math.min(startIndex + count, items.length);
      for (let i = startIndex; i < endIndex; i++) {
        const paperItem = items[i];
        const paperTitle =
          (paperItem.getField("title") as string) || "Untitled";
        const creators = paperItem.getCreators();
        const authorStr =
          creators.length > 0
            ? creators.map((c) => c.lastName).join(", ")
            : "Unknown";
        const year = paperItem.getField("year") || "";

        const row = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            padding: "8px 10px",
            borderBottom: "1px solid var(--border-primary)",
            cursor: "pointer",
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            transition: "background-color 0.1s",
          },
        });
        row.addEventListener("mouseenter", () => {
          row.style.backgroundColor = "var(--background-primary)";
        });
        row.addEventListener("mouseleave", () => {
          row.style.backgroundColor = "";
        });

        const info = ztoolkit.UI.createElement(doc, "div", {
          styles: { flex: "1", overflow: "hidden", marginRight: "10px" },
        });

        const titleEl = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: paperTitle },
          styles: {
            fontSize: "12px",
            fontWeight: "500",
            whiteSpace: "normal",
            wordBreak: "break-word",
            lineHeight: "1.3",
            color: "var(--text-primary)",
          },
        });

        const metaEl = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: `${authorStr}${year ? ` • ${year}` : ""}` },
          styles: {
            fontSize: "11px",
            color: "var(--text-secondary)",
            marginTop: "1px",
          },
        });
        info.appendChild(titleEl);
        info.appendChild(metaEl);
        row.appendChild(info);

        // Check if already added
        const isAdded = stateManager
          .getStates()
          .items.some((it) => it.id === paperItem.id);

        if (isAdded) {
          const addedLabel = ztoolkit.UI.createElement(doc, "span", {
            properties: { innerText: "Added" },
            styles: {
              fontSize: "10px",
              color: "var(--text-secondary)",
              fontStyle: "italic",
            },
          });
          row.appendChild(addedLabel);
        } else {
          const addBtn = ztoolkit.UI.createElement(doc, "button", {
            namespace: "html",
            properties: { innerText: "+" },
            styles: {
              width: "24px",
              height: "24px",
              borderRadius: "50%",
              border: "1px solid var(--highlight-primary)",
              backgroundColor: "transparent",
              color: "var(--highlight-primary)",
              fontSize: "16px",
              fontWeight: "bold",
              cursor: "pointer",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              transition: "all 0.15s",
            },
            listeners: [
              {
                type: "click",
                listener: async (e: Event) => {
                  e.stopPropagation();
                  await this.addItemWithNotes(paperItem);
                  // Animate removal or change to added
                  row.style.backgroundColor = "var(--background-hover)";
                  addBtn.replaceWith(
                    ztoolkit.UI.createElement(doc, "span", {
                      properties: { innerText: "✓" },
                      styles: {
                        fontSize: "14px",
                        color: "green",
                        fontWeight: "bold",
                      },
                    }),
                  );
                  setTimeout(() => {
                    row.style.opacity = "0.5";
                  }, 200);
                },
              },
            ],
          });
          addBtn.addEventListener("mouseenter", () => {
            addBtn.style.backgroundColor = "var(--highlight-primary)";
            addBtn.style.color = "var(--highlight-text)";
          });
          addBtn.addEventListener("mouseleave", () => {
            addBtn.style.backgroundColor = "transparent";
            addBtn.style.color = "var(--highlight-primary)";
          });
          row.appendChild(addBtn);
        }
        listContainer.appendChild(row);
      }
      displayedCount = endIndex;
    };

    const loadMorePapers = () => {
      renderPaperBatch(allFilteredItems, displayedCount, BATCH_SIZE);
    };

    listContainer.addEventListener("scroll", () => {
      const { scrollTop, scrollHeight, clientHeight } = listContainer;
      if (scrollHeight - scrollTop - clientHeight < 50) {
        loadMorePapers();
      }
    });

    const loadPapers = async () => {
      listContainer.innerHTML = "";
      const loading = ztoolkit.UI.createElement(doc, "div", {
        properties: { innerText: "Loading..." },
        styles: {
          padding: "20px",
          textAlign: "center",
          color: "var(--text-secondary)",
          fontSize: "12px",
        },
      });
      listContainer.appendChild(loading);

      allFilteredItems = [];
      displayedCount = 0;
      const filterValue = filterSelect.value;
      const searchQuery = searchInput.value.toLowerCase();

      try {
        let items: Zotero.Item[] = [];
        if (filterValue === "all") {
          const libraries = Zotero.Libraries.getAll();
          for (const lib of libraries) {
            const libItems = await Zotero.Items.getAll(lib.libraryID);
            items.push(
              ...libItems.filter((i: Zotero.Item) => i.isRegularItem()),
            );
          }
        } else if (filterValue.startsWith("lib_")) {
          const libraryId = parseInt(filterValue.replace("lib_", ""), 10);
          items = await Zotero.Items.getAll(libraryId);
          items = items.filter((i) => i.isRegularItem());
        } else if (filterValue.startsWith("col_")) {
          const collectionId = parseInt(filterValue.replace("col_", ""), 10);
          const collection = Zotero.Collections.get(collectionId);
          if (collection) {
            const itemIDs = collection.getChildItems(true);
            for (const id of itemIDs) {
              const item = Zotero.Items.get(id);
              if (item && item.isRegularItem()) items.push(item);
            }
          }
        }

        allFilteredItems = items.filter((i) => {
          const title = ((i.getField("title") as string) || "").toLowerCase();
          const creators = i
            .getCreators()
            .map((c: any) => (c.lastName || c.name || "").toLowerCase())
            .join(" ");
          return title.includes(searchQuery) || creators.includes(searchQuery);
        });

        listContainer.innerHTML = "";
        if (allFilteredItems.length === 0) {
          listContainer.appendChild(
            ztoolkit.UI.createElement(doc, "div", {
              properties: { innerText: "No papers found." },
              styles: {
                padding: "20px",
                textAlign: "center",
                color: "var(--text-secondary)",
                fontSize: "12px",
              },
            }),
          );
        } else {
          renderPaperBatch(allFilteredItems, 0, BATCH_SIZE);
        }
      } catch (e) {
        listContainer.innerHTML = "";
        listContainer.appendChild(
          ztoolkit.UI.createElement(doc, "div", {
            properties: { innerText: "Error loading papers." },
            styles: { color: "red", padding: "10px" },
          }),
        );
        Zotero.debug(`[seerai] Error loading papers: ${e}`);
      }
    };

    filterSelect.addEventListener("change", loadPapers);
    searchInput.addEventListener("input", () => {
      clearTimeout((searchInput as any)._debounce);
      (searchInput as any)._debounce = setTimeout(loadPapers, 300);
    });

    dropdown.appendChild(content);

    // Done button logic (optional, users can just close)
    const buttonRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        padding: "8px 14px",
        borderTop: "1px solid var(--border-primary)",
        display: "flex",
        justifyContent: "flex-end",
      },
    });
    const doneBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Done" },
      styles: {
        padding: "6px 14px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "12px",
        backgroundColor: "var(--background-secondary)",
        cursor: "pointer",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            dropdown.style.opacity = "0";
            setTimeout(() => dropdown.remove(), 200);
          },
        },
      ],
    });
    buttonRow.appendChild(doneBtn);
    dropdown.appendChild(buttonRow);

    selectionArea.parentNode.insertBefore(dropdown, selectionArea.nextSibling);

    // Auto focus
    setTimeout(() => searchInput.focus(), 100);
    setTimeout(() => {
      dropdown.style.opacity = "1";
      dropdown.style.transform = "translateY(0)";
    }, 10);

    loadPapers();
  }
  // Remove existing picker if any

  /**
   * Add all items matching the given tags, optionally filtered by collection/library
   */
  private static async addItemsByTags(
    tagNames: string[],
    collectionId?: number | null,
    libraryId?: number | null,
  ) {
    let addedCount = 0;

    // If a collection is specified, get items from that collection
    if (collectionId) {
      try {
        const collection = Zotero.Collections.get(collectionId);
        if (collection) {
          const itemIDs = collection.getChildItems(true);
          const itemTagSet = new Set(tagNames);

          for (const itemId of itemIDs) {
            const item = Zotero.Items.get(itemId);
            if (item && item.isRegularItem()) {
              const itemTags = item
                .getTags()
                .map((t: { tag: string }) => t.tag);
              // Check if item has any of the selected tags
              if (itemTags.some((tag: string) => itemTagSet.has(tag))) {
                await this.addItemWithNotes(item);
                addedCount++;
              }
            }
          }
        }
      } catch (e) {
        Zotero.debug(`[seerai] Error adding items from collection: ${e}`);
      }
    } else {
      // Get items from specified library or all libraries
      const libraries = libraryId
        ? [{ libraryID: libraryId }]
        : Zotero.Libraries.getAll();

      for (const library of libraries) {
        for (const tagName of tagNames) {
          // Get items with this tag using Zotero Search
          try {
            const s = new Zotero.Search({ libraryID: library.libraryID });
            s.addCondition("tag", "is", tagName);
            s.addCondition("itemType", "isNot", "attachment");
            s.addCondition("itemType", "isNot", "note");
            const itemIDs = await s.search();

            for (const itemID of itemIDs) {
              const item = Zotero.Items.get(itemID);
              if (item && item.isRegularItem()) {
                await this.addItemWithNotes(item);
                addedCount++;
              }
            }
          } catch (e) {
            Zotero.debug(`[seerai] Error searching for tag "${tagName}": ${e}`);
          }
        }
      }
    }

    Zotero.debug(
      `[seerai] Added ${addedCount} items from ${tagNames.length} tag(s)`,
    );
  }

  /**
   * Add items from library selection
   */
  private static async addFromLibrarySelection() {
    const selectedItems =
      Zotero.getActiveZoteroPane()?.getSelectedItems() || [];

    if (selectedItems.length === 0) {
      Zotero.debug("[seerai] No items selected in library pane");
      return;
    }

    let added = 0;
    for (const item of selectedItems) {
      if (item.isRegularItem()) {
        await this.addItemWithNotes(item);
        added++;
      }
    }

    Zotero.debug(`[seerai] Added ${added} items with notes to chat context`);
  }

  /**
   * Get all collections from all libraries for filtering
   */
  private static async getAllCollections(): Promise<
    {
      id: number;
      name: string;
      libraryName: string;
      libraryId: number;
      depth: number;
    }[]
  > {
    const allCollections: {
      id: number;
      name: string;
      libraryName: string;
      libraryId: number;
      depth: number;
    }[] = [];
    const libraries = Zotero.Libraries.getAll();

    for (const library of libraries) {
      try {
        const collections = Zotero.Collections.getByLibrary(library.libraryID);
        if (collections && collections.length > 0) {
          // Build a hierarchical list with proper indentation
          const addCollectionsRecursive = (
            parentId: number | null,
            depth: number,
          ) => {
            for (const collection of collections) {
              const collectionParentId = collection.parentID || null;
              if (collectionParentId === parentId) {
                allCollections.push({
                  id: collection.id,
                  name: collection.name,
                  libraryName: library.name,
                  libraryId: library.libraryID,
                  depth: depth,
                });
                // Recursively add children
                addCollectionsRecursive(collection.id, depth + 1);
              }
            }
          };
          addCollectionsRecursive(null, 0);
        }
      } catch (e) {
        Zotero.debug(
          `[seerai] Error loading collections from library ${library.name}: ${e}`,
        );
      }
    }

    return allCollections;
  }

  /**
   * Create a removable chip element
   */
  private static createChip(
    doc: Document,
    label: string,
    config: typeof selectionConfigs.items,
    onRemove: () => void,
  ): HTMLElement {
    // Detect dark mode using Zotero's theme
    const isDark = getTheme() === "dark";

    // Get chip colors based on theme
    const chipColors = this.getChipColors(config.className, isDark);

    const chip = ztoolkit.UI.createElement(doc, "div", {
      properties: {
        className: `chip ${config.className}`,
      },
      styles: {
        display: "inline-flex",
        alignItems: "center",
        gap: "4px",
        padding: "3px 8px",
        borderRadius: "12px",
        fontSize: "11px",
        maxWidth: "180px",
        border: "1px solid",
        backgroundColor: chipColors.bg,
        borderColor: chipColors.border,
        color: chipColors.text,
      },
    });

    const icon = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: config.icon },
      styles: { fontSize: "10px" },
    });

    const displayLabel = label.length > 20 ? label.slice(0, 20) + "..." : label;
    const text = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: displayLabel, title: label },
      styles: {
        overflow: "hidden",
        textOverflow: "ellipsis",
        whiteSpace: "nowrap",
      },
    });

    const removeBtn = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: "✕" },
      styles: {
        cursor: "pointer",
        fontSize: "10px",
        color: isDark ? "#bbb" : "#666",
        marginLeft: "2px",
        padding: "2px",
        borderRadius: "50%",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            onRemove();
          },
        },
      ],
    });

    chip.appendChild(icon);
    chip.appendChild(text);
    chip.appendChild(removeBtn);
    return chip;
  }

  /**
   * Get chip colors based on chip type and dark mode
   */
  private static getChipColors(
    className: string,
    isDark: boolean,
  ): { bg: string; border: string; text: string } {
    const colors: Record<
      string,
      {
        light: { bg: string; border: string; text: string };
        dark: { bg: string; border: string; text: string };
      }
    > = {
      "chip-items": {
        light: { bg: "#e3f2fd", border: "#2196f3", text: "#1565c0" },
        dark: { bg: "#0d47a1", border: "#4dabf5", text: "#fff" },
      },
      "chip-creators": {
        light: { bg: "#f3e5f5", border: "#9c27b0", text: "#7b1fa2" },
        dark: { bg: "#4a148c", border: "#ab47bc", text: "#fff" },
      },
      "chip-tags": {
        light: { bg: "#fff3e0", border: "#ff9800", text: "#e65100" },
        dark: { bg: "#6d4d00", border: "#ffc94d", text: "#fff" },
      },
      "chip-collections": {
        light: { bg: "#e8f5e9", border: "#4caf50", text: "#2e7d32" },
        dark: { bg: "#1b5e20", border: "#66bb6a", text: "#fff" },
      },
      "chip-notes": {
        light: { bg: "#fffde7", border: "#ffeb3b", text: "#f57f17" },
        dark: { bg: "#fff59d", border: "#ffc107", text: "#000" },
      },
      "chip-notes-summary": {
        light: { bg: "#fffde7", border: "#ffeb3b", text: "#f57f17" },
        dark: { bg: "#fff59d", border: "#ffc107", text: "#000" },
      },
      "chip-attachments": {
        light: { bg: "#fce4ec", border: "#e91e63", text: "#c2185b" },
        dark: { bg: "#880e4f", border: "#f06292", text: "#fff" },
      },
      "chip-images": {
        light: { bg: "#e1f5fe", border: "#03a9f4", text: "#0277bd" },
        dark: { bg: "#01579b", border: "#4fc3f7", text: "#fff" },
      },
      "chip-tables": {
        light: { bg: "#f3e5f5", border: "#7b1fa2", text: "#4a148c" },
        dark: { bg: "#4a148c", border: "#ab47bc", text: "#fff" },
      },
    };

    const colorSet = colors[className] || colors["chip-items"];
    return isDark ? colorSet.dark : colorSet.light;
  }

  /**
   * Create the controls bar (Model Selector, Settings, Stop, Clear, Save)
   */
  // Controls bar removed (migrated to Chat Settings)

  /**
   * Populate model selector with configured models
   */
  private static populateModelSelector(select: HTMLSelectElement) {
    const doc = select.ownerDocument;
    if (!doc) return;
    select.innerHTML = "";

    const configs = getModelConfigs();
    const activeConfig = getActiveModelConfig();

    if (configs.length === 0) {
      // No custom configs - show default option
      const defaultOpt = doc.createElement("option");
      defaultOpt.value = "default";
      defaultOpt.textContent = "Default (from preferences)";
      select.appendChild(defaultOpt);
    } else {
      configs.forEach((cfg) => {
        const opt = doc.createElement("option");
        opt.value = cfg.id;
        opt.textContent = cfg.name;
        if (activeConfig && cfg.id === activeConfig.id) {
          opt.selected = true;
        }
        select.appendChild(opt);
      });
    }
  }

  /**
   * Show Firecrawl settings popover for inline configuration
   */
  private static showFirecrawlSettingsPopover(
    container: HTMLElement,
    doc: Document,
  ) {
    // Remove existing popover if any
    const existing = doc.getElementById("firecrawl-settings-popover");
    if (existing) {
      existing.remove();
      return;
    }

    const prefPrefix = "extensions.seerai";
    const currentLimit =
      (Zotero.Prefs.get(`${prefPrefix}.firecrawlSearchLimit`) as number) || 3;
    const currentConcurrent =
      (Zotero.Prefs.get(`${prefPrefix}.firecrawlMaxConcurrent`) as number) || 3;

    const popover = ztoolkit.UI.createElement(doc, "div", {
      properties: {
        id: "firecrawl-settings-popover",
        className: "firecrawl-popover",
      },
      // Styles handled by CSS class
    });

    // Title
    const title = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "🔥 Firecrawl Settings" },
      styles: {
        fontWeight: "bold",
        marginBottom: "12px",
        fontSize: "12px",
      },
    });
    popover.appendChild(title);

    // Search Result Count
    const limitRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        alignItems: "center",
        marginBottom: "8px",
        gap: "8px",
      },
    });
    const limitLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Search Results:" },
      styles: { fontSize: "11px", flex: "1" },
    });
    const limitInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "number",
        min: "1",
        max: "10",
        value: String(currentLimit),
        class: "firecrawl-input",
      },
      styles: {
        width: "50px",
        padding: "4px",
        fontSize: "11px",
        textAlign: "center",
      },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            const value = parseInt((e.target as HTMLInputElement).value, 10);
            if (value >= 1 && value <= 10) {
              Zotero.Prefs.set(`${prefPrefix}.firecrawlSearchLimit`, value);
              Zotero.debug(`[seerai] Firecrawl search limit set to: ${value}`);
            }
          },
        },
      ],
    }) as HTMLInputElement;
    limitRow.appendChild(limitLabel);
    limitRow.appendChild(limitInput);
    popover.appendChild(limitRow);

    // Max Concurrent
    const concurrentRow = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", alignItems: "center", gap: "8px" },
    });
    const concurrentLabel = ztoolkit.UI.createElement(doc, "label", {
      properties: { innerText: "Max Concurrent:" },
      styles: { fontSize: "11px", flex: "1" },
    });
    const concurrentInput = ztoolkit.UI.createElement(doc, "input", {
      attributes: {
        type: "number",
        min: "1",
        max: "10",
        value: String(currentConcurrent),
        class: "firecrawl-input",
      },
      styles: {
        width: "50px",
        padding: "4px",
        fontSize: "11px",
        textAlign: "center",
      },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            const value = parseInt((e.target as HTMLInputElement).value, 10);
            if (value >= 1 && value <= 10) {
              Zotero.Prefs.set(`${prefPrefix}.firecrawlMaxConcurrent`, value);
              Zotero.debug(
                `[seerai] Firecrawl max concurrent set to: ${value}`,
              );
            }
          },
        },
      ],
    }) as HTMLInputElement;
    concurrentRow.appendChild(concurrentLabel);
    concurrentRow.appendChild(concurrentInput);
    popover.appendChild(concurrentRow);

    container.appendChild(popover);

    // Close on outside click
    const closeHandler = (e: MouseEvent) => {
      if (!container.contains(e.target as Node)) {
        popover.remove();
        doc.removeEventListener("click", closeHandler);
      }
    };
    setTimeout(() => doc.addEventListener("click", closeHandler), 10);
  }

  /**
   * Show chat settings popover
   */
  private static showChatSettingsPopover(container: HTMLElement) {
    const doc = container.ownerDocument!;
    const stateManager = getChatStateManager();
    const options = stateManager.getOptions();

    // Remove existing popover if any
    const existing = container.querySelector("#chat-settings-popover");
    if (existing) {
      existing.remove();
      return;
    }

    const popover = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "chat-settings-popover" },
      styles: {
        position: "absolute",
        top: "80px",
        left: "10px",
        backgroundColor: "var(--background-primary)",
        border: "1px solid var(--border-primary)",
        borderRadius: "8px",
        padding: "12px",
        boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
        zIndex: "1000",
        minWidth: "200px",
        color: "var(--text-primary)",
      },
    });

    const title = ztoolkit.UI.createElement(doc, "div", {
      properties: { innerText: "Chat Settings" },
      styles: { fontWeight: "600", marginBottom: "12px", fontSize: "13px" },
    });

    // Include Notes toggle
    const notesRow = this.createToggleRow(
      doc,
      "Include Notes",
      options.includeNotes,
      (checked) => {
        stateManager.setOptions({ includeNotes: checked });
      },
    );

    // Include Abstracts toggle
    const abstractsRow = this.createToggleRow(
      doc,
      "Include Abstracts",
      options.includeAbstracts,
      (checked) => {
        stateManager.setOptions({ includeAbstracts: checked });
      },
    );

    // Include Images toggle (for vision models)
    const imagesRow = this.createToggleRow(
      doc,
      "Include Images (Vision)",
      options.includeImages,
      (checked) => {
        stateManager.setOptions({ includeImages: checked });
      },
    );

    // Close button
    const closeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Close" },
      styles: {
        marginTop: "12px",
        padding: "6px 12px",
        fontSize: "11px",
        border: "1px solid var(--border-primary)",
        borderRadius: "4px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-primary)",
        cursor: "pointer",
        width: "100%",
      },
      listeners: [
        {
          type: "click",
          listener: () => popover.remove(),
        },
      ],
    });

    popover.appendChild(title);
    popover.appendChild(notesRow);
    popover.appendChild(abstractsRow);
    popover.appendChild(imagesRow);
    popover.appendChild(closeBtn);

    // Find the chat container and append
    const chatContainer = container.querySelector("div");
    if (chatContainer) {
      (chatContainer as HTMLElement).style.position = "relative";
      chatContainer.appendChild(popover);
    }
  }

  /**
   * Create a toggle row for settings
   */
  private static createToggleRow(
    doc: Document,
    label: string,
    initialValue: boolean,
    onChange: (checked: boolean) => void,
  ): HTMLElement {
    const row = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        marginBottom: "8px",
      },
    });

    const labelEl = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: label },
      styles: { fontSize: "12px" },
    });

    const checkbox = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "checkbox" },
      styles: { cursor: "pointer" },
      listeners: [
        {
          type: "change",
          listener: (e: Event) => {
            onChange((e.target as HTMLInputElement).checked);
          },
        },
      ],
    }) as HTMLInputElement;
    checkbox.checked = initialValue;

    row.appendChild(labelEl);
    row.appendChild(checkbox);
    return row;
  }

  /**
   * Resolve current Zotero selection into a LibraryScope
   */
  private static resolveSelectionScope(): import("./chat/tools").LibraryScope {
    try {
      const zp = Zotero.getActiveZoteroPane();
      if (!zp) return { type: "all" };

      const collection = zp.getSelectedCollection();
      if (collection) {
        return {
          type: "collection",
          collectionId: collection.id,
          libraryID: collection.libraryID,
        } as any;
      }

      const libraryID = zp.getSelectedLibraryID();
      if (typeof libraryID === "number") {
        const groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
        if (groupID) {
          return { type: "group", groupId: groupID };
        }
        return { type: "user" };
      }
    } catch (e) {
      Zotero.debug(`[seerai] Error resolving selection scope: ${e}`);
    }
    return { type: "all" };
  }

  /**
   * Get current library scope preference
   */
  private static getScopePref(): string {
    try {
      return (
        (Zotero.Prefs.get("extensions.seerai.libraryScope") as string) ||
        "selection"
      );
    } catch (e) {
      return "selection";
    }
  }

  /**
   * Check if an item is within the current library scope
   */
  public static checkItemInScope(item: Zotero.Item, config: any): boolean {
    if (!config.libraryScope || config.libraryScope.type === "all") return true;

    if (config.libraryScope.type === "user") {
      return item.libraryID === Zotero.Libraries.userLibraryID;
    }

    if (config.libraryScope.type === "group") {
      const groupLibId = Zotero.Groups.getLibraryIDFromGroupID(
        config.libraryScope.groupId,
      );
      return item.libraryID === groupLibId;
    }

    if (config.libraryScope.type === "collection") {
      const scopedId = (config.libraryScope as any).collectionId;
      const collections = item.getCollections();

      // Check if item is directly in the collection or any sub-collection
      if (collections.includes(scopedId)) return true;

      // Recursive check for all parents of all collections the item is in
      for (const colId of collections) {
        let currentId: number | null = colId;
        while (currentId) {
          if (currentId === scopedId) return true;
          const col: Zotero.Collection | undefined =
            Zotero.Collections.get(currentId);
          currentId = col ? col.parentID : null;
        }
      }
      return false;
    }

    return false;
  }

  /**
   * Get human-readable label for a scope string
   */
  private static getScopeLabel(scope: string): string {
    if (scope === "selection") {
      const resolved = this.resolveSelectionScope();
      if (resolved.type === "collection") {
        const col = Zotero.Collections.get((resolved as any).collectionId);
        return col ? col.name : "Selection";
      } else if (resolved.type === "group") {
        const group = Zotero.Groups.get(resolved.groupId);
        return group ? group.name : "Selection";
      } else if (resolved.type === "user") {
        return "My Library";
      }
      return "Automatic";
    }
    if (scope === "user") return "My Library";
    if (scope === "all") return "All Libraries";
    if (scope.startsWith("group:")) {
      const groupId = parseInt(scope.split(":")[1], 10);
      try {
        const group = Zotero.Groups.get(groupId);
        return group ? group.name : "Group: " + groupId;
      } catch (e) {
        return "Group: " + groupId;
      }
    }
    if (scope.startsWith("collection:")) {
      const parts = scope.split(":");
      const colId = parseInt(parts[parts.length - 1], 10);
      try {
        const col = Zotero.Collections.get(colId);
        return col ? "Folder: " + col.name : "Collection";
      } catch (e) {
        return "Collection";
      }
    }
    return "All Libraries";
  }

  /**
   * Show dropdown to select agentic mode scope (library/collection)
   */
  private static showScopeDropdown(
    doc: Document,
    anchorEl: HTMLElement,
    onSelect: (scope: string) => void,
  ): void {
    // Remove any existing dropdown
    const existing = doc.getElementById("agentic-scope-dropdown");
    if (existing) existing.remove();

    const dropdown = doc.createElement("div");
    dropdown.id = "agentic-scope-dropdown";
    const rect = anchorEl.getBoundingClientRect();
    const docBody = doc.body || doc.documentElement;

    dropdown.style.cssText = `
    position: fixed;
    top: ${rect.top - 4}px;
    left: ${rect.left}px;
    transform: translateY(-100%);
    min-width: 240px;
    max-height: 500px;
    overflow-y: auto;
    background: var(--background-primary, #fff);
    border: 1px solid var(--border-primary, #ccc);
    border-radius: 8px;
    box-shadow: 0 -4px 12px rgba(0,0,0,0.15);
    z-index: 200000;
    padding: 4px 0;
    pointer-events: auto;
  `;

    const currentScope = Assistant.getScopePref();

    const addOption = (
      label: string,
      value: string,
      icon: string = "📁",
      level: number = 0,
    ) => {
      const opt = doc.createElement("div");
      const isSelected = currentScope === value;

      opt.style.cssText = `
        padding: 8px 12px;
        padding-left: ${12 + level * 16}px;
        cursor: pointer;
        display: flex;
        align-items: center;
        gap: 8px;
        font-size: 12px;
        color: ${isSelected ? "#fff" : "var(--text-primary, #333)"};
        background-color: ${isSelected ? "var(--accent-blue, #007AFF)" : "transparent"};
        font-weight: ${isSelected ? "600" : "normal"};
      `;

      opt.innerHTML = `<span>${icon}</span><span style="flex-grow: 1">${label}</span>`;

      opt.addEventListener("mouseenter", () => {
        if (!isSelected)
          opt.style.backgroundColor = "var(--fill-quinary, #f5f5f5)";
      });
      opt.addEventListener("mouseleave", () => {
        if (!isSelected) opt.style.backgroundColor = "transparent";
      });
      opt.addEventListener("click", () => {
        onSelect(value);
        dropdown.remove();
      });
      dropdown.appendChild(opt);

      if (isSelected) {
        setTimeout(() => opt.scrollIntoView({ block: "nearest" }), 50);
      }
    };

    const addCollectionTree = (
      libraryID: number,
      parentID: number | null,
      level: number,
    ) => {
      try {
        const collections = parentID
          ? Zotero.Collections.getByParent(parentID)
          : Zotero.Collections.getByLibrary(libraryID);

        for (const col of collections) {
          addOption(
            col.name,
            `collection:${col.libraryID}:${col.id}`,
            "📂",
            level,
          );
          addCollectionTree(libraryID, col.id, level + 1);
        }
      } catch (e) {
        Zotero.debug(`[seerai] Error rendering collection tree: ${e}`);
      }
    };

    // 1. All Libraries (Global Override)
    addOption("All Libraries (Global)", "all", "🌐");

    // Separator
    const sep1 = doc.createElement("div");
    sep1.style.cssText =
      "height: 1px; background: var(--border-primary, #ccc); margin: 4px 0;";
    dropdown.appendChild(sep1);

    // 2. Personal Library
    addOption("My Library", "user", "📚");
    addCollectionTree(Zotero.Libraries.userLibraryID, null, 1);

    // 3. Group Libraries
    try {
      const groups = Zotero.Groups.getAll();
      for (const group of groups) {
        // Separator for groups if not the first one
        const groupSep = doc.createElement("div");
        groupSep.style.cssText =
          "height: 1px; background: var(--border-primary, #ccc); margin: 4px 12px; opacity: 0.5;";
        dropdown.appendChild(groupSep);

        addOption(group.name, `group:${group.groupID}`, "👥");
        addCollectionTree(group.libraryID, null, 1);
      }
    } catch (e) {
      Zotero.debug(`[seerai] Error getting groups: ${e}`);
    }

    // Agent Iteration Limit & Auto-OCR moved to Chat Settings (gear icon)

    const mountPoint = doc.body || doc.documentElement;
    if (mountPoint) {
      mountPoint.appendChild(dropdown);
    }

    // Close on mousedown outside
    const closeHandler = (e: MouseEvent) => {
      if (
        !dropdown.contains(e.target as Node) &&
        !anchorEl.contains(e.target as Node)
      ) {
        dropdown.remove();
        doc.removeEventListener("mousedown", closeHandler);
      }
    };
    setTimeout(() => doc.addEventListener("mousedown", closeHandler), 0);
  }

  // Position dropdown

  /**
   * Create the input area with send button and image paste support
   */

  private static createInputArea(
    doc: Document,
    messagesArea: HTMLElement,
    stateManager: ReturnType<typeof getChatStateManager>,
  ): HTMLElement {
    // Track pasted images (persisted across re-renders)
    const pastedImages = currentPastedImages;

    // Container for everything
    const inputContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "4px",
        flexShrink: "0",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    // Unified Context Chips Area
    let contextChipsArea: HTMLElement | null = null;
    try {
      contextChipsArea = createContextChipsArea(doc);
    } catch (e) {
      Zotero.debug(`[seerai] Error creating context chips area: ${e}`);
    }
    const contextManager = ChatContextManager.getInstance();

    // Image preview area (hidden by default)
    const imagePreviewArea = ztoolkit.UI.createElement(doc, "div", {
      properties: { id: "image-preview-area" },
      styles: {
        display: "none",
        flexWrap: "wrap",
        gap: "6px",
        padding: "8px",
        backgroundColor: "var(--image-preview-background)",
        borderRadius: "6px",
        border: "1px dashed var(--image-preview-border)",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    const updateImagePreview = () => {
      imagePreviewArea.innerHTML = "";
      if (pastedImages.length === 0) {
        (imagePreviewArea as HTMLElement).style.display = "none";
        return;
      }
      (imagePreviewArea as HTMLElement).style.display = "flex";

      const label = ztoolkit.UI.createElement(doc, "div", {
        properties: {
          innerText: `🖼️ ${pastedImages.length} image(s) attached:`,
        },
        styles: {
          width: "100%",
          fontSize: "11px",
          color: "var(--image-preview-text)",
          marginBottom: "4px",
        },
      });
      imagePreviewArea.appendChild(label);

      pastedImages.forEach((img, idx) => {
        const thumbnail = ztoolkit.UI.createElement(doc, "div", {
          styles: {
            position: "relative",
            width: "60px",
            height: "60px",
            borderRadius: "4px",
            overflow: "hidden",
            border: "1px solid var(--border-primary)",
          },
        });

        const imgEl = ztoolkit.UI.createElement(doc, "img", {
          attributes: { src: img.image },
          styles: { width: "100%", height: "100%", objectFit: "cover" },
        });

        const removeBtn = ztoolkit.UI.createElement(doc, "div", {
          properties: { innerText: "✕" },
          styles: {
            position: "absolute",
            top: "2px",
            right: "2px",
            width: "16px",
            height: "16px",
            backgroundColor: "rgba(255,0,0,0.7)",
            color: "#fff",
            borderRadius: "50%",
            fontSize: "10px",
            textAlign: "center",
            lineHeight: "16px",
            cursor: "pointer",
          },
          listeners: [
            {
              type: "click",
              listener: () => {
                pastedImages.splice(idx, 1);
                updateImagePreview();
              },
            },
          ],
        });

        thumbnail.appendChild(imgEl);
        thumbnail.appendChild(removeBtn);
        imagePreviewArea.appendChild(thumbnail);
      });
    };

    // Input area: toolbar row + text input row (stacked vertically)
    const inputArea = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexDirection: "column",
        gap: "4px",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    // Toolbar row (buttons split: left group + right group)
    const toolbarRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexWrap: "wrap",
        gap: "4px",
        alignItems: "center",
        justifyContent: "space-between",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    const toolbarLeft = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexWrap: "wrap",
        gap: "4px",
        alignItems: "center",
        minWidth: "0",
      },
    });

    const toolbarRight = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        flexWrap: "wrap",
        gap: "4px",
        alignItems: "center",
        minWidth: "0",
      },
    });

    // Text input row (textarea + send button, always full width)
    const textInputRow = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "6px",
        alignItems: "flex-end",
        minWidth: "0",
        maxWidth: "100%",
        boxSizing: "border-box",
      },
    });

    const input = ztoolkit.UI.createElement(doc, "textarea", {
      attributes: {
        placeholder: "",
        rows: "1",
        disabled: this.isStreaming ? "true" : undefined,
      },
      properties: {
        value: currentDraftText,
        className: "seerai-chat-input",
      },
      styles: {
        flex: "1",
        minWidth: "0",
        padding: "6px 10px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        fontSize: "13px",
        resize: "vertical",
        height: "32px",
        minHeight: "32px",
        maxHeight: "150px", // Reduced max height to prevent it from taking too much space
        fontFamily: "inherit",
        lineHeight: "1.4",
        boxSizing: "border-box",
        overflow: "auto", // Changed from hidden to auto to allow scrolling immediately if needed
      },
      listeners: [
        {
          type: "keydown",
          listener: (e: KeyboardEvent) => {
            // Handle Ctrl+V/Cmd+V for explicit image paste
            if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "v") {
              // Use clipboard API to check for images
              // This ensures Ctrl+V works the same as Ctrl+Shift+V for images
              if (
                typeof navigator !== "undefined" &&
                navigator.clipboard &&
                navigator.clipboard.read
              ) {
                navigator.clipboard
                  .read()
                  .then(async (clipboardItems) => {
                    for (const clipboardItem of clipboardItems) {
                      for (const type of clipboardItem.types) {
                        if (type.startsWith("image/")) {
                          // Found an image in clipboard
                          e.preventDefault();
                          try {
                            const blob = await clipboardItem.getType(type);
                            const reader = new FileReader();
                            reader.onload = () => {
                              const dataUrl = reader.result as string;
                              pastedImages.push({
                                id: Date.now().toString(),
                                image: dataUrl,
                                mimeType: type,
                              });
                              updateImagePreview();
                              Zotero.debug(
                                `[seerai] Pasted image via Ctrl+V: ${type}`,
                              );
                            };
                            reader.readAsDataURL(blob);
                          } catch (err) {
                            Zotero.debug(
                              `[seerai] Clipboard read error: ${err}`,
                            );
                          }
                          return;
                        }
                      }
                    }
                    // No image found, let default paste happen (text)
                  })
                  .catch((err) => {
                    // Clipboard API failed, fall back to default paste event handler
                    Zotero.debug(
                      `[seerai] Clipboard API unavailable, using fallback: ${err}`,
                    );
                  });
              }
              // If clipboard API not available, paste event handler will catch it
              return;
            }

            if (e.key === "Enter") {
              // If dropdown is open, let the dropdown handler handle Enter
              if (isDropdownOpen()) {
                return; // Don't send message, dropdown will handle selection
              }
              if (
                !e.shiftKey &&
                !e.ctrlKey &&
                !e.altKey &&
                !e.metaKey &&
                !this.isStreaming
              ) {
                e.preventDefault(); // Prevent newline
                this.handleSendWithStreamingAndImages(
                  input as unknown as HTMLInputElement,
                  messagesArea,
                  stateManager,
                  pastedImages,
                  () => {
                    pastedImages.length = 0;
                    updateImagePreview();
                  },
                );
              } else if (e.ctrlKey || e.altKey || e.metaKey) {
                // Explicitly insert newline for modifiers that might not default to it
                e.preventDefault();
                const start = input.selectionStart || 0;
                const end = input.selectionEnd || 0;
                const val = input.value;
                input.value =
                  val.substring(0, start) + "\n" + val.substring(end);
                input.selectionStart = input.selectionEnd = start + 1;
                // Trigger input event to auto-resize (XUL-compatible)
                const resizeEvt = doc.createEvent("Event");
                resizeEvt.initEvent("input", true, true);
                input.dispatchEvent(resizeEvt);
              }
              // Shift+Enter falls through to default behavior (newline)
            }
          },
        },
        {
          type: "input",
          listener: () => {
            // Auto-expand
            const el = input as unknown as HTMLTextAreaElement;
            currentDraftText = el.value; // Store draft
            el.style.height = "auto";
            const newHeight = Math.max(32, el.scrollHeight);
            el.style.height = newHeight + "px";
            // Show scrollbar if content exceeds single line
            el.style.overflow = newHeight > 32 ? "auto" : "hidden";
          },
        },
        {
          type: "paste",
          listener: async (e: ClipboardEvent) => {
            const items = e.clipboardData?.items;
            if (!items) return;

            for (const item of Array.from(items)) {
              if (item.type.startsWith("image/")) {
                e.preventDefault();
                const blob = item.getAsFile();
                if (blob) {
                  const reader = new FileReader();
                  reader.onload = () => {
                    const dataUrl = reader.result as string;
                    pastedImages.push({
                      id: Date.now().toString(),
                      image: dataUrl,
                      mimeType: item.type,
                    });
                    updateImagePreview();
                    Zotero.debug(`[seerai] Pasted image: ${item.type}`);
                  };
                  reader.readAsDataURL(blob);
                }
              }
            }
          },
        },
      ],
    }) as unknown as HTMLInputElement;

    const sendBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "➤ Send",
        disabled: this.isStreaming,
      },
      styles: {
        padding: "0 16px",
        height: "32px",
        backgroundColor: "var(--accent-color, #007AFF)",
        color: "#fff",
        border: "none",
        borderRadius: "6px",
        cursor: "pointer",
        fontWeight: "600",
        fontSize: "13px",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        flexShrink: "0",
        whiteSpace: "nowrap",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            if (!this.isStreaming) {
              this.handleSendWithStreamingAndImages(
                input as unknown as HTMLInputElement,
                messagesArea,
                stateManager,
                pastedImages,
                () => {
                  pastedImages.length = 0;
                  updateImagePreview();
                },
              );
            }
          },
        },
      ],
    });

    // Initial UI sync for restored drafts
    if (currentPastedImages.length > 0) {
      updateImagePreview();
    }
    if (currentDraftText) {
      setTimeout(() => {
        // XUL-compatible: do not use `new Event()`
        const draftEvt = doc.createEvent("Event");
        draftEvt.initEvent("input", true, true);
        input.dispatchEvent(draftEvt);
      }, 0);
    }

    // Stop button
    const stopBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "⏹", id: "stop-btn", title: "Stop Generation" },
      styles: {
        padding: "0 12px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--button-stop-border, #d32f2f)",
        borderRadius: "6px",
        backgroundColor: "var(--button-stop-background, #ffebee)",
        color: "var(--button-stop-text, #c62828)",
        cursor: "pointer",
        display: this.isStreaming ? "inline-flex" : "none",
        alignItems: "center",
        justifyContent: "center",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            openAIService.abortRequest();
            // Force reset immediately to prevent UI lock if stream hangs
            this.isStreaming = false;
            activeAgentSession = null;
            input.disabled = false;
            stopBtn.style.display = "none";
            input.focus();

            // Append "Generation stopped" message immediately if possible
            const streamingMsg = messagesArea.querySelector(
              ".message-bubble.assistant:last-child",
            );
            if (streamingMsg) {
              const contentDiv = streamingMsg.querySelector(
                "[data-content]",
              ) as HTMLElement;
              if (contentDiv) {
                const indicator = contentDiv.querySelector(".typing-indicator");
                if (indicator) {
                  indicator.remove();
                  const stopMsg = doc.createElement("span");
                  stopMsg.style.color = "#c62828";
                  stopMsg.textContent = "Generation stopped";
                  contentDiv.appendChild(stopMsg);
                }
              }
            }
          },
        },
      ],
    });

    // Clear button with two-click confirmation
    let clearConfirmState = false;
    let clearConfirmTimeout: ReturnType<typeof setTimeout> | null = null;

    const clearBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "🗑", title: "Clear Chat" },
      styles: {
        padding: "0 12px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        transition: "all 0.2s ease",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            if (!clearConfirmState) {
              // First click: enter confirmation state
              clearConfirmState = true;
              (clearBtn as HTMLElement).innerText = "Clear?";
              (clearBtn as HTMLElement).style.backgroundColor = "#ffebee";
              (clearBtn as HTMLElement).style.borderColor = "#c62828";
              (clearBtn as HTMLElement).style.color = "#c62828";
              (clearBtn as HTMLElement).title = "Click again to confirm";

              // Reset after 3 seconds if not clicked
              clearConfirmTimeout = setTimeout(() => {
                clearConfirmState = false;
                (clearBtn as HTMLElement).innerText = "🗑";
                (clearBtn as HTMLElement).style.backgroundColor =
                  "var(--background-secondary)";
                (clearBtn as HTMLElement).style.borderColor =
                  "var(--border-primary)";
                (clearBtn as HTMLElement).style.color = "var(--text-secondary)";
                (clearBtn as HTMLElement).title = "Clear Chat";
              }, 3000);
            } else {
              // Second click: perform clear
              if (clearConfirmTimeout) clearTimeout(clearConfirmTimeout);
              clearConfirmState = false;

              conversationMessages = [];
              if (messagesArea) messagesArea.replaceChildren();
              try {
                await getMessageStore().clearMessages();
              } catch (e) {
                Zotero.debug(`[seerai] Error clearing message store: ${e}`);
              }

              // Reset button appearance
              (clearBtn as HTMLElement).innerText = "🗑";
              (clearBtn as HTMLElement).style.backgroundColor =
                "var(--background-secondary)";
              (clearBtn as HTMLElement).style.borderColor =
                "var(--border-primary)";
              (clearBtn as HTMLElement).style.color = "var(--text-secondary)";
              (clearBtn as HTMLElement).title = "Clear Chat";
            }
          },
        },
      ],
    });

    // Save button
    const saveBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "💾", title: "Save Chat" },
      styles: {
        padding: "0 12px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.saveConversationAsNote();
          },
        },
      ],
    });

    // Settings / Config button
    const settingsBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "⚙️",
        title: "Chat Settings (Model, Mode, Web Search)",
      },
      styles: {
        width: "32px",
        height: "32px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        fontSize: "14px",
        transition: "all 0.15s ease",
      },
      listeners: [
        {
          type: "click",
          listener: (e: MouseEvent) => {
            Zotero.debug("[seerai] Settings button clicked");
            e.stopPropagation();
            showChatSettings(doc, settingsBtn as HTMLElement, {
              onModeChange: async (mode) => {
                // If switching to default or explore mode, auto-add current item
                if (mode !== "lock" && currentItem) {
                  if (mode === "default") {
                    stateManager.clearAll();
                  }
                  await this.addItemWithNotes(currentItem);
                }
              },
            });
          },
        },
        {
          type: "mouseenter",
          listener: () => {
            (settingsBtn as HTMLElement).style.backgroundColor =
              "var(--background-secondary)";
            (settingsBtn as HTMLElement).style.borderColor =
              "var(--border-secondary)";
          },
        },
        {
          type: "mouseleave",
          listener: () => {
            (settingsBtn as HTMLElement).style.backgroundColor =
              "var(--background-primary)";
            (settingsBtn as HTMLElement).style.borderColor =
              "var(--border-primary)";
          },
        },
      ],
    });

    const settingsContainer = doc.createElement("div");
    settingsContainer.style.cssText = "position: relative; margin-right: 4px;";
    settingsContainer.appendChild(settingsBtn);

    // Agentic mode toggle button
    let agenticEnabled = isAgenticModeEnabled();

    const updateAgenticBtnStyle = (btn: HTMLElement, enabled: boolean) => {
      btn.innerText = enabled ? "🤖" : "💬";
      btn.title = enabled
        ? `Agentic Mode: ON - Scope: ${this.getScopeLabel(this.getScopePref())}`
        : "Agentic Mode: OFF (click to enable)";
      btn.style.backgroundColor = enabled
        ? "var(--accent-blue, #007AFF)"
        : "var(--background-secondary)";
      btn.style.color = enabled ? "#fff" : "var(--text-secondary)";
      btn.style.borderColor = enabled
        ? "var(--accent-blue, #007AFF)"
        : "var(--border-primary)";
    };

    const agenticBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: agenticEnabled ? "🤖" : "💬",
        title: "Toggle Agentic Mode (tool calling)",
      },
      styles: {
        width: "32px",
        height: "32px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px 0 0 6px",
        backgroundColor: agenticEnabled
          ? "var(--accent-blue, #007AFF)"
          : "var(--background-secondary)",
        color: agenticEnabled ? "#fff" : "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        fontSize: "14px",
        transition: "all 0.15s ease",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            agenticEnabled = !agenticEnabled;
            try {
              Zotero.Prefs.set("extensions.seerai.agenticMode", agenticEnabled);
            } catch (e) {
              Zotero.debug(`[seerai] Error saving agentic mode pref: ${e}`);
            }
            updateAgenticBtnStyle(agenticBtn as HTMLElement, agenticEnabled);
            // Update scope button visibility
            (scopeBtn as HTMLElement).style.display = agenticEnabled
              ? "flex"
              : "none";
            Zotero.debug(`[seerai] Agentic mode toggled: ${agenticEnabled}`);
          },
        },
      ],
    });

    // Scope selector button (dropdown trigger)
    const scopeBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "▼",
        title: `Scope: ${this.getScopeLabel(this.getScopePref())}`,
      },
      styles: {
        width: "20px",
        height: "32px",
        border: "1px solid var(--accent-blue, #007AFF)",
        borderLeft: "none",
        borderRadius: "0 6px 6px 0",
        backgroundColor: "var(--accent-blue, #007AFF)",
        color: "#fff",
        cursor: "pointer",
        display: agenticEnabled ? "flex" : "none",
        alignItems: "center",
        justifyContent: "center",
        fontSize: "10px",
        transition: "all 0.15s ease",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            this.showScopeDropdown(
              doc,
              scopeBtn as HTMLElement,
              (newScope: string) => {
                try {
                  Zotero.Prefs.set("extensions.seerai.libraryScope", newScope);
                  (scopeBtn as HTMLElement).title =
                    `Scope: ${this.getScopeLabel(newScope)}`;
                  updateAgenticBtnStyle(
                    agenticBtn as HTMLElement,
                    agenticEnabled,
                  );
                  Zotero.debug(`[seerai] Agent scope changed to: ${newScope}`);
                } catch (err) {
                  Zotero.debug(`[seerai] Error saving scope: ${err}`);
                }
              },
            );
          },
        },
      ],
    });

    const agenticContainer = doc.createElement("div");
    agenticContainer.style.cssText =
      "position: relative; display: flex; margin-right: 4px;";
    agenticContainer.appendChild(agenticBtn);
    agenticContainer.appendChild(scopeBtn);

    // Prompt Library button
    const promptsBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "📚", title: "Prompt Library" },
      styles: {
        width: "32px",
        height: "32px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-primary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        fontSize: "14px",
        transition: "all 0.15s ease",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            showPromptPicker(doc, promptsBtn as HTMLElement, {
              onSelect: (template) => {
                // Insert template text into input
                const currentVal = input.value;
                if (currentVal) {
                  input.value = currentVal + " " + template.template;
                } else {
                  input.value = template.template;
                }
                input.focus();
                // Trigger input event for autocomplete (Zotero-compatible)
                const inputEvent = doc.createEvent("Event");
                inputEvent.initEvent("input", true, true);
                input.dispatchEvent(inputEvent);

                // Trigger first placeholder
                triggerNextPlaceholder(doc, input);
              },
            });
          },
        },
        {
          type: "mouseenter",
          listener: () => {
            (promptsBtn as HTMLElement).style.backgroundColor =
              "var(--background-secondary)";
            (promptsBtn as HTMLElement).style.borderColor =
              "var(--border-secondary)";
          },
        },
        {
          type: "mouseleave",
          listener: () => {
            (promptsBtn as HTMLElement).style.backgroundColor =
              "var(--background-primary)";
            (promptsBtn as HTMLElement).style.borderColor =
              "var(--border-primary)";
          },
        },
      ],
    });

    const promptsContainer = doc.createElement("div");
    promptsContainer.style.cssText = "position: relative;";
    promptsContainer.appendChild(promptsBtn);

    // Placeholder menu button (for manually inserting placeholders)
    const placeholderBtn = createPlaceholderMenuButton(doc, input);

    // Initialize placeholder autocomplete on input with chip insertion
    initPlaceholderAutocomplete(
      doc,
      input,
      (value, itemType, itemId, trigger, data) => {
        const type = itemType as ContextItemType;

        if (type === "workspace") {
          const wsDir = (data as any)?.workspaceDir as string | undefined;
          const wsType = (data as any)?.workspaceType as string | undefined;
          const convId = (data as any)?.conversationId as string | undefined;
          if (wsDir) {
            (async () => {
              try {
                // 1. Chat history (if chat workspace)
                if (wsType === "chat" && convId) {
                  try {
                    const msgStore = getMessageStore();
                    const origId = msgStore.getConversationId();
                    msgStore.setConversationId(convId);
                    const msgs = await msgStore.loadMessages();
                    msgStore.setConversationId(origId);

                    if (msgs.length > 0) {
                      let transcript = `# Chat History: ${value}\n\n`;
                      for (const m of msgs) {
                        const role = m.role === "user" ? "User" : "Assistant";
                        transcript += `## ${role}\n${m.content}\n\n---\n\n`;
                      }
                      contextManager.addItem(
                        `ws-chat-history-${convId}`,
                        "file",
                        `Chat: ${value}`,
                        "command",
                        {
                          filename: `${value}.md`,
                          extractedContent: transcript,
                          charCount: transcript.length,
                          estimatedTokens:
                            ChatStateManager.countTokens(transcript),
                          fileCategory: "chat_history",
                        },
                      );
                    }
                  } catch (e) {
                    Zotero.debug(`[seerai] Error loading chat history: ${e}`);
                  }
                }

                // 2. Scan workspace directory and add each file
                async function scanDir(
                  dir: string,
                  relPath: string,
                ): Promise<void> {
                  let children: string[];
                  try {
                    children = await IOUtils.getChildren(dir);
                  } catch {
                    return;
                  }
                  for (const fp of children) {
                    const name = fp.split("/").pop() || "";
                    if (name.startsWith(".") || name === "node_modules")
                      continue;
                    let isDir = false;
                    try {
                      isDir = (await IOUtils.stat(fp)).type === "directory";
                    } catch {
                      continue;
                    }
                    if (isDir) {
                      await scanDir(fp, relPath ? `${relPath}/${name}` : name);
                    } else {
                      const r = relPath ? `${relPath}/${name}` : name;
                      try {
                        const bytes = await IOUtils.read(fp);
                        let content: string;
                        if (name.toLowerCase().endsWith(".docx")) {
                          const ab = bytes.buffer.slice(
                            bytes.byteOffset,
                            bytes.byteOffset + bytes.byteLength,
                          ) as ArrayBuffer;
                          const { markdown } = await convertDocxToMarkdown(ab);
                          content = markdown;
                        } else {
                          content = new TextDecoder().decode(bytes);
                        }
                        const safe = stripBase64Data(content);
                        contextManager.addItem(
                          `ws-file-${r}`,
                          "file",
                          r,
                          "command",
                          {
                            filename: r,
                            extractedContent: safe,
                            charCount: safe.length,
                            estimatedTokens: ChatStateManager.countTokens(safe),
                            fileCategory: "workspace_file",
                          },
                        );
                      } catch {
                        /* skip unreadable */
                      }
                    }
                  }
                }
                await scanDir(wsDir, "");
              } catch (e) {
                Zotero.debug(`[seerai] Workspace expansion failed: ${e}`);
              }
            })();
          }
        } else {
          contextManager.addItem(
            itemId || value,
            type,
            value,
            "command",
            data || { itemKey: String(itemId) },
          );
        }

        // Clear the trigger text from input
        input.value = input.value.replace(/\[[^\]]+\]\s*$/, "").trim();

        // Trigger next placeholder
        triggerNextPlaceholder(doc, input);
      },
    );

    // Toolbar row: left group and right group
    toolbarLeft.appendChild(agenticContainer);
    toolbarLeft.appendChild(settingsContainer);
    toolbarLeft.appendChild(promptsContainer);
    toolbarLeft.appendChild(placeholderBtn);

    // ── ☁️ Google Drive Button ──
    const driveBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "\u2601\uFE0F",
        title: "Google Drive - browse, load to context, import to workspace",
      },
      styles: {
        padding: "0 10px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
    }) as HTMLButtonElement;

    driveBtn.addEventListener("click", (e: Event) => {
      e.stopPropagation();
      Zotero.debug("[seerai] Drive button clicked");
      const chatId = getMessageStore().getConversationId();
      if (!chatId) {
        Zotero.debug("[seerai] Drive button: no chatId, aborting");
        return;
      }
      showDriveModal(doc, driveBtn as HTMLElement, chatId).catch((err) => {
        Zotero.debug(`[seerai] Drive modal error: ${err}`);
      });
    });

    toolbarLeft.appendChild(driveBtn);

    // ── 📎 File Upload Button ──
    // Opens a file picker for PDF/audio/text/markdown, adds to context system
    const uploadBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "\u{1F4CE}",
        title: "Attach file (PDF, audio, text, markdown)",
      },
      styles: {
        padding: "0 12px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
    }) as HTMLButtonElement;

    uploadBtn.addEventListener("click", async () => {
      try {
        const win = Zotero.getMainWindow() as any;

        // Use Zotero's FilePicker (XPCOM nsIFilePicker wrapper)
        const fp = new win.FilePicker();
        fp.init(win, "Attach File to Chat", fp.modeOpen);

        // Add filters for supported file types
        fp.appendFilter(
          "All Supported Files",
          "*.pdf;*.txt;*.md;*.markdown;*.csv;*.json;*.xml;*.html;*.htm;*.rtf;*.docx;*.doc;*.mp3;*.wav;*.ogg;*.webm;*.m4a;*.flac;*.aac",
        );
        fp.appendFilter(
          "Documents",
          "*.pdf;*.txt;*.md;*.markdown;*.csv;*.json;*.xml;*.html;*.htm;*.rtf;*.docx;*.doc",
        );
        fp.appendFilter(
          "Audio Files",
          "*.mp3;*.wav;*.ogg;*.webm;*.m4a;*.flac;*.aac",
        );
        fp.appendFilters(fp.filterAll);

        const result = await fp.show();
        if (result !== fp.returnOK) return;

        const filePath = fp.file;
        if (!filePath) return;

        // Extract filename from path
        const filename = filePath.split(/[/\\]/).pop() || "unknown";
        const ext = filename.split(".").pop()?.toLowerCase() || "";

        // Determine file category for metadata
        const audioExts = ["mp3", "wav", "ogg", "webm", "m4a", "flac", "aac"];
        const textExts = [
          "txt",
          "md",
          "markdown",
          "csv",
          "json",
          "xml",
          "html",
          "htm",
        ];
        const richDocExts = ["rtf", "docx", "doc"];
        const fileCategory = audioExts.includes(ext)
          ? "audio"
          : ext === "pdf"
            ? "pdf"
            : textExts.includes(ext)
              ? "text"
              : richDocExts.includes(ext)
                ? "richdoc"
                : "other";

        // Generate unique ID for this file context item
        const fileId = `file_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;

        // ── Extract content immediately at upload time ──
        let extractedContent = "";
        let fileSize = 0;
        let extractionError = "";

        try {
          const contentBytes = await IOUtils.read(filePath);
          fileSize = contentBytes.length;

          if (fileCategory === "audio") {
            // Audio: transcribe via STT API and store the resulting text
            try {
              const activeModelCfg = getActiveModelConfig();
              if (!activeModelCfg?.sttConfig?.model) {
                extractionError =
                  "Speech-to-Text not configured. Add an STT model in settings to transcribe audio files.";
              } else {
                Zotero.debug(
                  `[seerai] Transcribing audio file: ${filename} (${contentBytes.length} bytes)`,
                );
                const sttResult = await openAIService.speechToText(
                  contentBytes,
                  { filename: filename },
                );
                if (
                  sttResult.transcription &&
                  sttResult.transcription.length > 0
                ) {
                  extractedContent = sttResult.transcription;
                  Zotero.debug(
                    `[seerai] Audio transcribed: ${extractedContent.length} chars`,
                  );
                } else {
                  extractionError =
                    "Audio transcription returned empty result. The file may be silent or too short.";
                }
              }
            } catch (sttErr: any) {
              extractionError = `Audio transcription failed: ${sttErr.message || sttErr}`;
              Zotero.debug(
                `[seerai] STT error for "${filename}": ${sttErr.message || sttErr}`,
              );
            }
          } else if (fileCategory === "pdf") {
            // PDF: use Zotero's built-in PDFWorker (pdf.js) for reliable text extraction.
            // PDFWorker._query sends the raw buffer to the pdf.js web worker which handles
            // all PDF decompression (FlateDecode etc.) and text extraction properly.
            try {
              const pdfWorker = (Zotero as any).PDFWorker as any;
              if (pdfWorker && pdfWorker._query && pdfWorker._enqueue) {
                // Must copy buffer — the transfer in postMessage detaches it
                const buf = new Uint8Array(contentBytes).buffer.slice(0);
                const pdfResult = await pdfWorker._enqueue(async () => {
                  return await pdfWorker._query(
                    "getFulltext",
                    { buf, maxPages: 0 },
                    [buf],
                  );
                }, false);
                if (pdfResult && pdfResult.text && pdfResult.text.length > 0) {
                  extractedContent = pdfResult.text;
                  Zotero.debug(
                    `[seerai] PDF extracted via PDFWorker: ${extractedContent.length} chars, ${pdfResult.extractedPages}/${pdfResult.totalPages} pages`,
                  );
                } else {
                  extractionError =
                    "PDF text extraction returned no content. The PDF may be image-based — try OCR.";
                }
              } else {
                extractionError =
                  "Zotero PDFWorker not available. Add this PDF as a Zotero item for text extraction.";
              }
            } catch (pdfErr: any) {
              Zotero.debug(
                `[seerai] PDFWorker extraction failed for "${filename}": ${pdfErr.message || pdfErr}`,
              );
              extractionError = `PDF extraction failed: ${pdfErr.message || pdfErr}. Try adding as a Zotero item and using OCR.`;
            }

            // Cap at 200K chars for PDFs
            const maxPdfChars = 200_000;
            if (extractedContent.length > maxPdfChars) {
              extractedContent =
                extractedContent.substring(0, maxPdfChars) +
                `\n[... truncated, ${extractedContent.length - maxPdfChars} chars omitted ...]`;
            }
          } else if (fileCategory === "text") {
            // Text-based files: use IOUtils.readUTF8 for reliable UTF-8 reading
            try {
              extractedContent = await IOUtils.readUTF8(filePath);
            } catch {
              // Fallback to binary read + TextDecoder if readUTF8 fails (e.g. non-UTF8)
              extractedContent = new TextDecoder("utf-8", {
                fatal: false,
              }).decode(contentBytes);
            }

            // Strip HTML tags for .html/.htm files
            if (ext === "html" || ext === "htm") {
              extractedContent = extractedContent
                .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
                .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
                .replace(/<[^>]*>/g, " ")
                .replace(/&nbsp;/gi, " ")
                .replace(/&amp;/gi, "&")
                .replace(/&lt;/gi, "<")
                .replace(/&gt;/gi, ">")
                .replace(/&quot;/gi, '"')
                .replace(/&#39;/gi, "'")
                .replace(/\s{3,}/g, "  ")
                .trim();
            }

            Zotero.debug(
              `[seerai] Text file read: ${extractedContent.length} chars from ${filename}`,
            );

            // Cap at 200K chars
            const maxTextChars = 200_000;
            if (extractedContent.length > maxTextChars) {
              extractedContent =
                extractedContent.substring(0, maxTextChars) +
                `\n[... truncated, ${extractedContent.length - maxTextChars} chars omitted ...]`;
            }
          } else if (fileCategory === "richdoc") {
            // Rich document formats: RTF, DOCX, DOC

            if (ext === "rtf") {
              // RTF: Strip RTF control codes to extract plain text
              let rtfText = new TextDecoder("utf-8", {
                fatal: false,
              }).decode(contentBytes);

              // Remove RTF header/footer
              rtfText = rtfText.replace(/^\{\\rtf1?[^}]*\}?/, "");

              // Remove font table, color table, stylesheet, info blocks
              rtfText = rtfText.replace(
                /\{\\(fonttbl|colortbl|stylesheet|info|header|footer|headerf|footerf)\b[^{}]*(\{[^{}]*\}[^{}]*)*\}/g,
                "",
              );

              // Remove picture data
              rtfText = rtfText.replace(
                /\{\\pict[^{}]*(\{[^{}]*\}[^{}]*)*\}/g,
                "",
              );

              // Remove \' hex escapes → actual characters
              rtfText = rtfText.replace(/\\'([0-9a-fA-F]{2})/g, (_m, hex) =>
                String.fromCharCode(parseInt(hex, 16)),
              );

              // Remove Unicode escapes \uNNNNN with their replacement char
              rtfText = rtfText.replace(/\\u(-?\d+)[?]?/g, (_m, code) => {
                const cp = parseInt(code, 10);
                return cp >= 0
                  ? String.fromCharCode(cp)
                  : String.fromCharCode(cp + 65536);
              });

              // Convert paragraph/line breaks to newlines
              rtfText = rtfText.replace(/\\par\b/g, "\n");
              rtfText = rtfText.replace(/\\line\b/g, "\n");
              rtfText = rtfText.replace(/\\tab\b/g, "\t");

              // Remove all remaining RTF control words (\word, \word123, \*)
              rtfText = rtfText.replace(/\\[a-z]+(-?\d+)?\s?/gi, "");
              rtfText = rtfText.replace(/\\\*/g, "");

              // Remove braces
              rtfText = rtfText.replace(/[{}]/g, "");

              // Clean up whitespace
              extractedContent = rtfText
                .replace(/\r\n/g, "\n")
                .replace(/\r/g, "\n")
                .replace(/\n{3,}/g, "\n\n")
                .replace(/[ \t]{2,}/g, " ")
                .trim();

              Zotero.debug(
                `[seerai] RTF extracted: ${extractedContent.length} chars from ${filename}`,
              );
            } else if (ext === "docx") {
              // DOCX: ZIP archive containing word/document.xml
              // Use Zotero's built-in ZIP reading via jar: protocol or manual extraction
              try {
                // In Gecko, we can use the nsIZipReader XPCOM component
                const zipReader = (Components.classes as any)[
                  "@mozilla.org/libjar/zip-reader;1"
                ].createInstance(Components.interfaces.nsIZipReader);

                // Open the DOCX file as a ZIP
                const fileObj = (Components.classes as any)[
                  "@mozilla.org/file/local;1"
                ].createInstance(Components.interfaces.nsIFile);
                fileObj.initWithPath(filePath);
                zipReader.open(fileObj);

                try {
                  // Read word/document.xml which contains the main body text
                  if (zipReader.hasEntry("word/document.xml")) {
                    const inputStream =
                      zipReader.getInputStream("word/document.xml");
                    const scriptableStream = (Components.classes as any)[
                      "@mozilla.org/scriptableinputstream;1"
                    ].createInstance(
                      Components.interfaces.nsIScriptableInputStream,
                    );
                    scriptableStream.init(inputStream);

                    const xmlBytes = scriptableStream.read(
                      scriptableStream.available(),
                    );
                    scriptableStream.close();
                    inputStream.close();

                    // Parse XML and extract text from <w:t> elements
                    const docXml = xmlBytes;

                    // Extract text from <w:t ...>text</w:t> tags
                    const textMatches = docXml.match(
                      /<w:t[^>]*>([^<]*)<\/w:t>/g,
                    );
                    if (textMatches && textMatches.length > 0) {
                      // Also detect paragraph boundaries <w:p> for newlines
                      // Process the full XML to preserve structure
                      extractedContent = docXml
                        // Add newlines at paragraph boundaries
                        .replace(/<\/w:p>/g, "\n</w:p>")
                        // Remove all XML tags except w:t content
                        .replace(/<w:t[^>]*>([^<]*)<\/w:t>/g, "$1")
                        .replace(/<[^>]*>/g, "")
                        // Decode XML entities
                        .replace(/&amp;/g, "&")
                        .replace(/&lt;/g, "<")
                        .replace(/&gt;/g, ">")
                        .replace(/&quot;/g, '"')
                        .replace(/&apos;/g, "'")
                        // Clean up whitespace
                        .replace(/\n{3,}/g, "\n\n")
                        .trim();

                      Zotero.debug(
                        `[seerai] DOCX extracted: ${extractedContent.length} chars from ${filename}`,
                      );
                    } else {
                      extractionError =
                        "DOCX file has no text content in word/document.xml";
                    }
                  } else {
                    extractionError =
                      "Invalid DOCX: word/document.xml not found";
                  }
                } finally {
                  zipReader.close();
                }
              } catch (docxErr: any) {
                extractionError = `DOCX extraction failed: ${docxErr.message || docxErr}`;
                Zotero.debug(
                  `[seerai] DOCX error for "${filename}": ${docxErr.message || docxErr}`,
                );
              }
            } else if (ext === "doc") {
              // Legacy .doc (OLE2 binary format) — very complex to parse without a library.
              // Attempt basic text extraction: .doc files contain text runs as readable ASCII/UTF-16LE
              // embedded in the binary structure. Extract the longest coherent text sections.
              try {
                const rawText = new TextDecoder("utf-8", {
                  fatal: false,
                }).decode(contentBytes);

                // Strategy: find long runs of printable ASCII (doc files embed text)
                const textRuns = rawText.match(/[\x20-\x7E\n\r\t]{20,}/g);
                if (textRuns && textRuns.length > 0) {
                  // Filter out runs that look like binary metadata
                  const goodRuns = textRuns.filter((run) => {
                    // Skip runs that are mostly punctuation/symbols
                    const alphaRatio =
                      (run.match(/[a-zA-Z]/g) || []).length / run.length;
                    return alphaRatio > 0.3 && run.length > 30;
                  });
                  extractedContent = goodRuns.join("\n").trim();
                }

                if (extractedContent.length < 100) {
                  // Try UTF-16LE decoding (Word stores text in UTF-16LE)
                  const utf16Text = new TextDecoder("utf-16le", {
                    fatal: false,
                  }).decode(contentBytes);
                  const utf16Runs = utf16Text.match(
                    /[\x20-\x7E\u00A0-\uFFFF]{20,}/g,
                  );
                  if (utf16Runs && utf16Runs.length > 0) {
                    const goodUtf16Runs = utf16Runs.filter((run) => {
                      const alphaRatio =
                        (run.match(/[a-zA-Z\u00C0-\u024F]/g) || []).length /
                        run.length;
                      return alphaRatio > 0.3 && run.length > 30;
                    });
                    const utf16Content = goodUtf16Runs.join("\n").trim();
                    if (utf16Content.length > extractedContent.length) {
                      extractedContent = utf16Content;
                    }
                  }
                }

                if (extractedContent.length < 100) {
                  extractedContent = "";
                  extractionError =
                    "Legacy .doc format has limited support. Save as .docx or .rtf for better results.";
                } else {
                  Zotero.debug(
                    `[seerai] DOC extracted (best-effort): ${extractedContent.length} chars from ${filename}`,
                  );
                }
              } catch (docErr: any) {
                extractionError = `DOC extraction failed: ${docErr.message || docErr}`;
              }
            }

            // Cap rich docs at 200K chars
            const maxRichChars = 200_000;
            if (extractedContent.length > maxRichChars) {
              extractedContent =
                extractedContent.substring(0, maxRichChars) +
                `\n[... truncated, ${extractedContent.length - maxRichChars} chars omitted ...]`;
            }
          } else {
            extractionError = `Unsupported file type: .${ext}`;
          }
        } catch (readErr: any) {
          extractionError = `Failed to read file: ${readErr.message || readErr}`;
          Zotero.debug(
            `[seerai] File read error for "${filePath}": ${readErr}`,
          );
        }

        // Strip base64-encoded image data from extracted content
        // This prevents massive context bloat from embedded images in markdown/HTML files
        extractedContent = stripBase64Data(extractedContent);

        // Estimate tokens (~4 chars per token is a common heuristic)
        const charCount = extractedContent.length;
        const estimatedTokens = Math.ceil(charCount / 4);

        // Chip displays just the filename; details go in metadata for tooltip/context counter
        const displayName = filename;

        // Add to context manager as a "file" type with extracted content in metadata
        contextManager.addItem(
          fileId,
          "file" as ContextItemType,
          displayName,
          "toolbar",
          {
            filePath: filePath,
            fileCategory: fileCategory,
            fileExtension: ext,
            filename: filename,
            fileSize: fileSize,
            extractedContent: extractedContent,
            charCount: charCount,
            estimatedTokens: estimatedTokens,
            extractionError: extractionError || undefined,
          },
        );

        Zotero.debug(
          `[seerai] File attached: ${filename} (${fileCategory}, ${fileSize} bytes, ${charCount} chars, ~${estimatedTokens} tokens)${extractionError ? ` [${extractionError}]` : ""}`,
        );
      } catch (err: any) {
        Zotero.debug(`[seerai] File upload error: ${err.message || err}`);
      }
    });

    toolbarLeft.appendChild(uploadBtn);

    // ── 🎨 Image Generation Button (Two-part: emoji sends, ▲ opens settings) ──
    // Persisted image generation settings
    let imageGenSize: "256x256" | "512x512" | "1024x1024" = "1024x1024";
    let imageGenSeed: number | undefined;

    const imageGenContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        alignItems: "center",
        position: "relative",
      },
    });

    const imageGenBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "\u{1F3A8}",
        title: "Generate Image from prompt",
      },
      styles: {
        padding: "0 8px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px 0 0 6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
    }) as HTMLButtonElement;

    const imageGenDropupBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "\u25B2", title: "Image generation settings" },
      styles: {
        padding: "0 4px",
        height: "32px",
        fontSize: "8px",
        border: "1px solid var(--border-primary)",
        borderLeft: "none",
        borderRadius: "0 6px 6px 0",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
    }) as HTMLButtonElement;

    // Image settings dropup panel
    imageGenDropupBtn.addEventListener("click", (e) => {
      e.stopPropagation();
      e.preventDefault();
      // Remove any existing dropup
      const existing = doc.querySelector(".seerai-image-settings-dropup");
      if (existing) {
        existing.remove();
        return;
      }

      const activeConfig = getActiveModelConfig();
      const currentModel = activeConfig?.imageConfig?.model || "not configured";

      const panel = doc.createElement("div");
      panel.className = "seerai-image-settings-dropup";
      panel.style.cssText = `
        position: absolute; bottom: 38px; left: 0; z-index: 1000;
        background: var(--background-primary); border: 1px solid var(--border-primary);
        border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        padding: 12px; min-width: 220px; font-size: 12px;
      `;

      // Build panel content via DOM (not innerHTML — Gecko XHTML parser chokes on emojis)
      const titleDiv = doc.createElement("div");
      titleDiv.style.cssText =
        "font-weight: 600; margin-bottom: 8px; font-size: 13px;";
      titleDiv.textContent = "Image Settings";

      const modelDiv = doc.createElement("div");
      modelDiv.style.cssText =
        "margin-bottom: 6px; color: var(--text-secondary); font-size: 11px;";
      modelDiv.textContent = "Model: " + currentModel;

      const sizeLabel = doc.createElement("label");
      sizeLabel.style.cssText =
        "display: block; margin-bottom: 4px; font-weight: 500;";
      sizeLabel.textContent = "Size:";

      const sizeSelect = doc.createElement("select") as HTMLSelectElement;
      sizeSelect.id = "img-size-select";
      sizeSelect.style.cssText =
        "width: 100%; padding: 4px 6px; border: 1px solid var(--border-primary); border-radius: 4px; margin-bottom: 8px; font-size: 12px; background: var(--background-primary); color: var(--text-primary);";
      const sizeOptions: [string, string][] = [
        ["256x256", "256x256"],
        ["512x512", "512x512"],
        ["1024x1024", "1024x1024 (default)"],
      ];
      for (const [val, label] of sizeOptions) {
        const opt = doc.createElement("option");
        opt.value = val;
        opt.textContent = label;
        if (imageGenSize === val) opt.selected = true;
        sizeSelect.appendChild(opt);
      }

      const seedLabel = doc.createElement("label");
      seedLabel.style.cssText =
        "display: block; margin-bottom: 4px; font-weight: 500;";
      seedLabel.textContent = "Seed (optional):";

      const seedInput = doc.createElement("input") as HTMLInputElement;
      seedInput.id = "img-seed-input";
      seedInput.type = "number";
      seedInput.placeholder = "Random";
      seedInput.value = imageGenSeed !== undefined ? String(imageGenSeed) : "";
      seedInput.style.cssText =
        "width: 100%; padding: 4px 6px; border: 1px solid var(--border-primary); border-radius: 4px; font-size: 12px; box-sizing: border-box; background: var(--background-primary); color: var(--text-primary);";

      const hintDiv = doc.createElement("div");
      hintDiv.style.cssText =
        "margin-top: 6px; font-size: 10px; color: var(--text-tertiary);";
      hintDiv.textContent = "Prompt = textarea text + context";

      panel.appendChild(titleDiv);
      panel.appendChild(modelDiv);
      panel.appendChild(sizeLabel);
      panel.appendChild(sizeSelect);
      panel.appendChild(seedLabel);
      panel.appendChild(seedInput);
      panel.appendChild(hintDiv);

      imageGenContainer.appendChild(panel);

      // Bind change events
      sizeSelect.addEventListener("change", () => {
        imageGenSize = sizeSelect.value as typeof imageGenSize;
      });
      seedInput.addEventListener("change", () => {
        const val = seedInput.value.trim();
        imageGenSeed = val ? parseInt(val, 10) : undefined;
      });

      // Close on outside click (use mousedown — Gecko renders <select> popups
      // outside the DOM tree, so click targets from option lists fail panel.contains())
      const closeHandler = (ev: Event) => {
        const target = ev.target as Node;
        if (
          !panel.contains(target) &&
          target !== imageGenDropupBtn &&
          !imageGenContainer.contains(target)
        ) {
          // Double-check: if a panel input/select is focused, don't close
          const active = doc.activeElement;
          if (active && panel.contains(active)) return;
          panel.remove();
          doc.removeEventListener("mousedown", closeHandler);
        }
      };
      setTimeout(() => doc.addEventListener("mousedown", closeHandler), 0);
    });

    // Image generation send handler
    imageGenBtn.addEventListener("click", async () => {
      const promptText = (input as unknown as HTMLTextAreaElement).value.trim();
      if (!promptText || this.isStreaming) return;

      const activeConfig = getActiveModelConfig();
      if (!activeConfig?.imageConfig?.model) {
        this.appendMessage(
          messagesArea,
          "Assistant",
          "Image generation not configured. Go to Settings and add an Image model in your model configuration.",
        );
        return;
      }

      // Show user message
      const userMsg: ChatMessage = {
        id: Date.now().toString(),
        role: "user",
        content: `\u{1F3A8} [Image Generation] ${promptText}`,
        timestamp: new Date(),
      };
      conversationMessages.push(userMsg);
      try {
        await getMessageStore().appendMessage(userMsg);
      } catch (e) {
        Zotero.debug(`[seerai] Error saving image gen user message: ${e}`);
      }
      this.appendMessage(
        messagesArea,
        "You",
        `\u{1F3A8} [Image Generation] ${promptText}`,
        userMsg.id,
        true,
      );
      (input as unknown as HTMLTextAreaElement).value = "";
      currentDraftText = "";

      // Gather rich context for LLM prompt refinement (full content is fine for LLM, just not for image API)
      const contextManager = ChatContextManager.getInstance();
      const contextItems = contextManager.getItems();
      let contextForLLM = "";
      if (contextItems.length > 0) {
        const parts: string[] = [];
        for (const item of contextItems) {
          if (item.type === "file" && item.metadata?.extractedContent) {
            parts.push(
              `[File: ${item.displayName}]\n${(item.metadata.extractedContent as string).substring(0, 2000)}`,
            );
          } else if (item.type === "paper") {
            parts.push(`[Paper: ${item.displayName}]`);
          } else if (item.type === "topic") {
            parts.push(`[Topic: ${item.displayName}]`);
          }
        }
        contextForLLM = parts.join("\n\n");
      }

      // Show generating indicator
      const genMsgId = `img_${Date.now()}`;
      const genDiv = this.appendMessage(
        messagesArea,
        "Assistant",
        "",
        genMsgId,
        false,
      );
      const genContent = genDiv.querySelector("[data-content]") as HTMLElement;
      // Build generating indicator via DOM (not innerHTML — Gecko XHTML parser chokes on emojis)
      genContent.innerHTML = "";
      const imgStatusRow = doc.createElement("div");
      imgStatusRow.style.cssText =
        "display: flex; align-items: center; gap: 6px; color: var(--text-secondary); font-style: italic;";
      const imgStatusIcon = doc.createElement("span");
      imgStatusIcon.textContent = "\u{1F3A8} Refining prompt with AI";
      imgStatusRow.appendChild(imgStatusIcon);
      const imgDots = doc.createElement("span");
      const dot1 = doc.createElement("span");
      dot1.className = "dot";
      dot1.style.cssText = "animation: blink 1.4s infinite .2s;";
      dot1.textContent = ".";
      const dot2 = doc.createElement("span");
      dot2.className = "dot";
      dot2.style.cssText = "animation: blink 1.4s infinite .4s;";
      dot2.textContent = ".";
      const dot3 = doc.createElement("span");
      dot3.className = "dot";
      dot3.style.cssText = "animation: blink 1.4s infinite .6s;";
      dot3.textContent = ".";
      imgDots.appendChild(dot1);
      imgDots.appendChild(dot2);
      imgDots.appendChild(dot3);
      imgStatusRow.appendChild(imgDots);
      genContent.appendChild(imgStatusRow);

      try {
        // Step 1: Use chat LLM to refine the vague prompt into a detailed visual description
        let imagePrompt = promptText;
        try {
          const imgModel = activeConfig.imageConfig.model;
          const systemMsg = {
            role: "system" as const,
            content: `You are an expert image prompt engineer. The user wants to generate an image using an AI image generation model (model: ${imgModel}, size: ${imageGenSize}). Your job is to convert their request into a detailed, concrete visual description that the image generation model can render well.

Rules:
- Output ONLY the image prompt, nothing else. No explanations, no preamble.
- Be specific about visual elements: layout, colors, style, composition.
- If the user references a paper/document, extract the key themes and translate them into a visually compelling scene. Do NOT try to reproduce text or data tables — image models cannot render text accurately.
- For "infographic" requests: describe a visually appealing illustration that represents the key concepts, NOT actual data tables or text blocks.
- Keep the prompt under 400 characters. Shorter, more focused prompts work better.
- Do NOT include quotes, special characters, or meta-instructions.
- Do NOT include text that the image should display — image models render text poorly.`,
          };

          let userContent = promptText;
          if (contextForLLM) {
            userContent = `${promptText}\n\nContext from attached materials:\n${contextForLLM}`;
          }

          const refinedPrompt = await openAIService.chatCompletion([
            systemMsg,
            { role: "user" as const, content: userContent },
          ]);

          if (refinedPrompt && refinedPrompt.trim().length > 10) {
            imagePrompt = refinedPrompt.trim().substring(0, 800);
            Zotero.debug(
              `[seerai] Image prompt refined by LLM: "${imagePrompt.substring(0, 100)}..."`,
            );
          }
        } catch (refineErr: any) {
          // If refinement fails, fall back to original prompt — don't block generation
          Zotero.debug(
            `[seerai] Image prompt refinement failed, using original: ${refineErr.message || refineErr}`,
          );
        }

        // Update status to "Generating image..."
        imgStatusIcon.textContent = "\u{1F3A8} Generating image";

        // Step 2: Call the image generation API with the refined prompt
        const result = await openAIService.generateImage(imagePrompt, {
          size: imageGenSize,
          seed: imageGenSeed,
          response_format: "url",
        });

        // Render generated image(s) inline
        genContent.innerHTML = "";
        const imgContainer = doc.createElement("div");
        imgContainer.style.cssText =
          "display: flex; flex-direction: column; gap: 8px;";

        for (const img of result.images) {
          const imgSrc =
            img.url ||
            (img.b64_json ? `data:image/png;base64,${img.b64_json}` : "");
          if (!imgSrc) continue;

          const wrapper = doc.createElement("div");
          wrapper.style.cssText =
            "position: relative; display: inline-block; max-width: 100%;";

          const imgEl = doc.createElement("img");
          imgEl.src = imgSrc;
          imgEl.alt = promptText.substring(0, 100);
          imgEl.style.cssText = `
            max-width: 100%; max-height: 400px; border-radius: 8px;
            border: 1px solid var(--border-primary); cursor: pointer;
          `;
          imgEl.title = "Click to open in new tab";
          imgEl.addEventListener("click", () => {
            const win = Zotero.getMainWindow();
            if (win) (win as any).open(imgSrc, "_blank");
          });

          // Download button
          const downloadBtn = doc.createElementNS(
            HTML_NS,
            "button",
          ) as HTMLButtonElement;
          downloadBtn.innerText = "\u{2B07}\u{FE0F}";
          downloadBtn.title = "Save image";
          downloadBtn.style.cssText = `
            position: absolute; top: 6px; right: 6px;
            padding: 4px 8px; border: none; border-radius: 4px;
            background: rgba(0,0,0,0.6); color: white; cursor: pointer;
            font-size: 14px; opacity: 0.7; transition: opacity 0.2s;
          `;
          downloadBtn.addEventListener("mouseenter", () => {
            downloadBtn.style.opacity = "1";
          });
          downloadBtn.addEventListener("mouseleave", () => {
            downloadBtn.style.opacity = "0.7";
          });
          downloadBtn.addEventListener("click", async (e) => {
            e.stopPropagation();
            try {
              const win = Zotero.getMainWindow() as any;
              const fp = new win.FilePicker();
              fp.init(win, "Save Generated Image", fp.modeSave);
              fp.defaultString = `seerai_image_${Date.now()}.png`;
              fp.appendFilter("PNG Images", "*.png");
              fp.appendFilter("JPEG Images", "*.jpg;*.jpeg");
              const res = await fp.show();
              if (res === fp.returnOK || res === fp.returnReplace) {
                if (img.url) {
                  // Download from URL
                  const resp = await fetch(img.url, {});
                  const arrayBuf = await resp.arrayBuffer();
                  await IOUtils.write(fp.file, new Uint8Array(arrayBuf));
                } else if (img.b64_json) {
                  // Decode base64
                  const binaryStr = atob(img.b64_json);
                  const bytes = new Uint8Array(binaryStr.length);
                  for (let i = 0; i < binaryStr.length; i++)
                    bytes[i] = binaryStr.charCodeAt(i);
                  await IOUtils.write(fp.file, bytes);
                }
                Zotero.debug(`[seerai] Image saved to: ${fp.file}`);
              }
            } catch (saveErr: any) {
              Zotero.debug(`[seerai] Image save error: ${saveErr.message}`);
            }
          });

          wrapper.appendChild(imgEl);
          wrapper.appendChild(downloadBtn);
          imgContainer.appendChild(wrapper);
        }

        // Cost info
        if (result.cost !== undefined) {
          const costDiv = doc.createElement("div");
          costDiv.style.cssText =
            "font-size: 10px; color: var(--text-tertiary); margin-top: 4px;";
          costDiv.textContent = `Cost: $${result.cost.toFixed(4)}${result.remainingBalance !== undefined ? ` | Balance: $${result.remainingBalance.toFixed(2)}` : ""}`;
          imgContainer.appendChild(costDiv);
        }

        genContent.appendChild(imgContainer);

        // Store the result in conversation
        const assistantMsg: ChatMessage = {
          id: genMsgId,
          role: "assistant",
          content: `🎨 Generated image for: "${promptText.substring(0, 100)}"`,
          timestamp: new Date(),
        };
        conversationMessages.push(assistantMsg);
        try {
          await getMessageStore().appendMessage(assistantMsg);
        } catch (e) {
          Zotero.debug(`[seerai] Error saving image gen result message: ${e}`);
        }
      } catch (err: any) {
        genContent.innerHTML = "";
        const mdContainer = doc.createElement("div");
        mdContainer.className = "markdown-content";
        mdContainer.setAttribute(
          "data-raw",
          `Image generation failed: ${err.message || err}`,
        );
        mdContainer.innerHTML = parseMarkdown(
          `**Image generation failed:**\n\n${err.message || err}`,
        );
        genContent.appendChild(mdContainer);
        Zotero.debug(`[seerai] Image generation error: ${err.message || err}`);
      }

      messagesArea.scrollTop = messagesArea.scrollHeight;
    });

    imageGenContainer.appendChild(imageGenBtn);
    imageGenContainer.appendChild(imageGenDropupBtn);

    // ── 🎬 Video Generation Button (Two-part: emoji sends, ▲ opens settings) ──
    // Persisted video generation settings
    let videoGenDuration = "5";
    let videoGenAspectRatio: "16:9" | "9:16" | "1:1" | "4:3" | "3:4" = "16:9";
    let videoGenResolution: "480p" | "720p" | "1080p" = "720p";
    let videoGenNegativePrompt = "";

    const videoGenContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        alignItems: "center",
        position: "relative",
      },
    });

    const videoGenBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "\u{1F3AC}",
        title: "Generate Video from prompt",
      },
      styles: {
        padding: "0 8px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px 0 0 6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
    }) as HTMLButtonElement;

    const videoGenDropupBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "\u25B2", title: "Video generation settings" },
      styles: {
        padding: "0 4px",
        height: "32px",
        fontSize: "8px",
        border: "1px solid var(--border-primary)",
        borderLeft: "none",
        borderRadius: "0 6px 6px 0",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
    }) as HTMLButtonElement;

    // Video settings dropup panel
    videoGenDropupBtn.addEventListener("click", (e) => {
      e.stopPropagation();
      e.preventDefault();
      const existing = doc.querySelector(".seerai-video-settings-dropup");
      if (existing) {
        existing.remove();
        return;
      }

      const activeConfig = getActiveModelConfig();
      const currentModel = activeConfig?.videoConfig?.model || "not configured";

      const panel = doc.createElement("div");
      panel.className = "seerai-video-settings-dropup";
      panel.style.cssText = `
        position: absolute; bottom: 38px; left: 0; z-index: 1000;
        background: var(--background-primary); border: 1px solid var(--border-primary);
        border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        padding: 12px; min-width: 240px; font-size: 12px;
      `;

      // Build panel content via DOM (not innerHTML — Gecko XHTML parser chokes on emojis)
      const titleDiv = doc.createElement("div");
      titleDiv.style.cssText =
        "font-weight: 600; margin-bottom: 8px; font-size: 13px;";
      titleDiv.textContent = "Video Settings";

      const modelDiv = doc.createElement("div");
      modelDiv.style.cssText =
        "margin-bottom: 6px; color: var(--text-secondary); font-size: 11px;";
      modelDiv.textContent = "Model: " + currentModel;

      // Duration input
      const durLabel = doc.createElement("label");
      durLabel.style.cssText =
        "display: block; margin-bottom: 4px; font-weight: 500;";
      durLabel.textContent = "Duration (seconds):";

      const durInput = doc.createElement("input") as HTMLInputElement;
      durInput.type = "text";
      durInput.value = videoGenDuration;
      durInput.placeholder = "5";
      durInput.style.cssText =
        "width: 100%; padding: 4px 6px; border: 1px solid var(--border-primary); border-radius: 4px; margin-bottom: 8px; font-size: 12px; box-sizing: border-box; background: var(--background-primary); color: var(--text-primary);";

      // Aspect Ratio select
      const aspectLabel = doc.createElement("label");
      aspectLabel.style.cssText =
        "display: block; margin-bottom: 4px; font-weight: 500;";
      aspectLabel.textContent = "Aspect Ratio:";

      const aspectSelect = doc.createElement("select") as HTMLSelectElement;
      aspectSelect.style.cssText =
        "width: 100%; padding: 4px 6px; border: 1px solid var(--border-primary); border-radius: 4px; margin-bottom: 8px; font-size: 12px; background: var(--background-primary); color: var(--text-primary);";
      const aspectOptions: [string, string][] = [
        ["16:9", "16:9 (Landscape)"],
        ["9:16", "9:16 (Portrait)"],
        ["1:1", "1:1 (Square)"],
        ["4:3", "4:3"],
        ["3:4", "3:4"],
      ];
      for (const [val, label] of aspectOptions) {
        const opt = doc.createElement("option");
        opt.value = val;
        opt.textContent = label;
        if (videoGenAspectRatio === val) opt.selected = true;
        aspectSelect.appendChild(opt);
      }

      // Resolution select
      const resLabel = doc.createElement("label");
      resLabel.style.cssText =
        "display: block; margin-bottom: 4px; font-weight: 500;";
      resLabel.textContent = "Resolution:";

      const resSelect = doc.createElement("select") as HTMLSelectElement;
      resSelect.style.cssText =
        "width: 100%; padding: 4px 6px; border: 1px solid var(--border-primary); border-radius: 4px; margin-bottom: 8px; font-size: 12px; background: var(--background-primary); color: var(--text-primary);";
      const resOptions: [string, string][] = [
        ["480p", "480p"],
        ["720p", "720p (default)"],
        ["1080p", "1080p"],
      ];
      for (const [val, label] of resOptions) {
        const opt = doc.createElement("option");
        opt.value = val;
        opt.textContent = label;
        if (videoGenResolution === val) opt.selected = true;
        resSelect.appendChild(opt);
      }

      // Negative prompt input
      const negLabel = doc.createElement("label");
      negLabel.style.cssText =
        "display: block; margin-bottom: 4px; font-weight: 500;";
      negLabel.textContent = "Negative Prompt:";

      const negPromptInput = doc.createElement("input") as HTMLInputElement;
      negPromptInput.type = "text";
      negPromptInput.value = videoGenNegativePrompt;
      negPromptInput.placeholder = "blur, distort, low quality";
      negPromptInput.style.cssText =
        "width: 100%; padding: 4px 6px; border: 1px solid var(--border-primary); border-radius: 4px; font-size: 12px; box-sizing: border-box; background: var(--background-primary); color: var(--text-primary);";

      const hintDiv = doc.createElement("div");
      hintDiv.style.cssText =
        "margin-top: 6px; font-size: 10px; color: var(--text-tertiary);";
      hintDiv.textContent =
        "Video is async -- polling status will show in chat";

      panel.appendChild(titleDiv);
      panel.appendChild(modelDiv);
      panel.appendChild(durLabel);
      panel.appendChild(durInput);
      panel.appendChild(aspectLabel);
      panel.appendChild(aspectSelect);
      panel.appendChild(resLabel);
      panel.appendChild(resSelect);
      panel.appendChild(negLabel);
      panel.appendChild(negPromptInput);
      panel.appendChild(hintDiv);

      videoGenContainer.appendChild(panel);

      // Bind change events
      durInput.addEventListener("change", () => {
        videoGenDuration = durInput.value.trim() || "5";
      });
      aspectSelect.addEventListener("change", () => {
        videoGenAspectRatio = aspectSelect.value as typeof videoGenAspectRatio;
      });
      resSelect.addEventListener("change", () => {
        videoGenResolution = resSelect.value as typeof videoGenResolution;
      });
      negPromptInput.addEventListener("change", () => {
        videoGenNegativePrompt = negPromptInput.value.trim();
      });

      // Close on outside click (use mousedown — Gecko renders <select> popups
      // outside the DOM tree, so click targets from option lists fail panel.contains())
      const closeHandler = (ev: Event) => {
        const target = ev.target as Node;
        if (
          !panel.contains(target) &&
          target !== videoGenDropupBtn &&
          !videoGenContainer.contains(target)
        ) {
          // Double-check: if a panel input/select is focused, don't close
          const active = doc.activeElement;
          if (active && panel.contains(active)) return;
          panel.remove();
          doc.removeEventListener("mousedown", closeHandler);
        }
      };
      setTimeout(() => doc.addEventListener("mousedown", closeHandler), 0);
    });

    // Video generation send handler
    videoGenBtn.addEventListener("click", async () => {
      const promptText = (input as unknown as HTMLTextAreaElement).value.trim();
      if (!promptText || this.isStreaming) return;

      const activeConfig = getActiveModelConfig();
      if (!activeConfig?.videoConfig?.model) {
        this.appendMessage(
          messagesArea,
          "Assistant",
          "Video generation not configured. Go to Settings and add a Video model in your model configuration.",
        );
        return;
      }

      // Show user message
      const userMsg: ChatMessage = {
        id: Date.now().toString(),
        role: "user",
        content: `\u{1F3AC} [Video Generation] ${promptText}`,
        timestamp: new Date(),
      };
      conversationMessages.push(userMsg);
      try {
        await getMessageStore().appendMessage(userMsg);
      } catch (e) {
        Zotero.debug(`[seerai] Error saving video gen user message: ${e}`);
      }
      this.appendMessage(
        messagesArea,
        "You",
        `\u{1F3AC} [Video Generation] ${promptText}`,
        userMsg.id,
        true,
      );
      (input as unknown as HTMLTextAreaElement).value = "";
      currentDraftText = "";

      // Gather rich context for LLM prompt refinement
      const contextMgr = ChatContextManager.getInstance();
      const ctxItems = contextMgr.getItems();
      let contextForVideoLLM = "";
      if (ctxItems.length > 0) {
        const parts: string[] = [];
        for (const item of ctxItems) {
          if (item.type === "file" && item.metadata?.extractedContent) {
            parts.push(
              `[File: ${item.displayName}]\n${(item.metadata.extractedContent as string).substring(0, 2000)}`,
            );
          } else if (item.type === "paper") {
            parts.push(`[Paper: ${item.displayName}]`);
          } else if (item.type === "topic") {
            parts.push(`[Topic: ${item.displayName}]`);
          }
        }
        contextForVideoLLM = parts.join("\n\n");
      }

      // Show generating indicator with live status updates
      const genMsgId = `vid_${Date.now()}`;
      const genDiv = this.appendMessage(
        messagesArea,
        "Assistant",
        "",
        genMsgId,
        false,
      );
      const genContent = genDiv.querySelector("[data-content]") as HTMLElement;
      const statusTextEl = doc.createElement("span");
      statusTextEl.textContent = "Refining prompt with AI";
      genContent.innerHTML = "";
      const statusRow = doc.createElement("div");
      statusRow.style.cssText =
        "display: flex; align-items: center; gap: 6px; color: var(--text-secondary); font-style: italic;";
      // Build via DOM (not innerHTML — Gecko XHTML parser chokes on emojis)
      const vidStatusIcon = doc.createElement("span");
      vidStatusIcon.textContent = "\u{1F3AC}";
      statusRow.appendChild(vidStatusIcon);
      statusRow.appendChild(statusTextEl);
      const dotsSpan = doc.createElement("span");
      const vDot1 = doc.createElement("span");
      vDot1.className = "dot";
      vDot1.style.cssText = "animation: blink 1.4s infinite .2s;";
      vDot1.textContent = ".";
      const vDot2 = doc.createElement("span");
      vDot2.className = "dot";
      vDot2.style.cssText = "animation: blink 1.4s infinite .4s;";
      vDot2.textContent = ".";
      const vDot3 = doc.createElement("span");
      vDot3.className = "dot";
      vDot3.style.cssText = "animation: blink 1.4s infinite .6s;";
      vDot3.textContent = ".";
      dotsSpan.appendChild(vDot1);
      dotsSpan.appendChild(vDot2);
      dotsSpan.appendChild(vDot3);
      statusRow.appendChild(dotsSpan);
      genContent.appendChild(statusRow);

      try {
        // Step 1: Use chat LLM to refine the vague prompt into a detailed video description
        let videoPrompt = promptText;
        try {
          const vidModel = activeConfig.videoConfig.model;
          const systemMsg = {
            role: "system" as const,
            content: `You are an expert video prompt engineer. The user wants to generate a video using an AI video generation model (model: ${vidModel}, duration: ${videoGenDuration}s, aspect ratio: ${videoGenAspectRatio}, resolution: ${videoGenResolution}). Your job is to convert their request into a detailed, concrete scene description that the video model can render well.

Rules:
- Output ONLY the video prompt, nothing else. No explanations, no preamble.
- Be specific about: scene composition, camera movement, lighting, colors, motion, subjects, and mood.
- The video is only ${videoGenDuration} seconds long — describe a single cohesive scene, not a multi-scene narrative.
- If the user references a paper/document, extract the key themes and translate them into a visually compelling scene.
- Keep the prompt under 400 characters. Shorter, more focused prompts work better.
- Do NOT include quotes, special characters, or meta-instructions.
- Do NOT describe text overlays — video models cannot render text accurately.`,
          };

          let userContent = promptText;
          if (contextForVideoLLM) {
            userContent = `${promptText}\n\nContext from attached materials:\n${contextForVideoLLM}`;
          }

          const refinedPrompt = await openAIService.chatCompletion([
            systemMsg,
            { role: "user" as const, content: userContent },
          ]);

          if (refinedPrompt && refinedPrompt.trim().length > 10) {
            videoPrompt = refinedPrompt.trim().substring(0, 800);
            Zotero.debug(
              `[seerai] Video prompt refined by LLM: "${videoPrompt.substring(0, 100)}..."`,
            );
          }
        } catch (refineErr: any) {
          // If refinement fails, fall back to original prompt
          Zotero.debug(
            `[seerai] Video prompt refinement failed, using original: ${refineErr.message || refineErr}`,
          );
        }

        // Update status to "Submitting..."
        statusTextEl.textContent = "Submitting";

        // Step 2: Call the video generation API with the refined prompt
        const result = await openAIService.generateVideo(
          videoPrompt,
          {
            duration: videoGenDuration,
            aspect_ratio: videoGenAspectRatio,
            resolution: videoGenResolution,
            negative_prompt: videoGenNegativePrompt || undefined,
          },
          // Live status callback
          (status, attempt, maxAttempts) => {
            statusTextEl.textContent = `${status} (poll ${attempt}/${maxAttempts})`;
            messagesArea.scrollTop = messagesArea.scrollHeight;
          },
        );

        // Render result
        genContent.innerHTML = "";
        const vidContainer = doc.createElement("div");
        vidContainer.style.cssText =
          "display: flex; flex-direction: column; gap: 8px;";

        if (result.videoUrl) {
          // Inline video player
          const videoEl = doc.createElement("video");
          videoEl.src = result.videoUrl;
          videoEl.controls = true;
          videoEl.autoplay = false;
          videoEl.preload = "metadata";
          videoEl.style.cssText = `
            max-width: 100%; max-height: 400px; border-radius: 8px;
            border: 1px solid var(--border-primary);
          `;

          // Thumbnail poster
          if (result.thumbnailUrl) {
            videoEl.poster = result.thumbnailUrl;
          }

          vidContainer.appendChild(videoEl);

          // Action buttons row
          const actionsRow = doc.createElement("div");
          actionsRow.style.cssText =
            "display: flex; gap: 8px; align-items: center;";

          // Open in browser
          const openBtn = doc.createElementNS(
            HTML_NS,
            "button",
          ) as HTMLButtonElement;
          openBtn.innerText = "\u{1F517} Open";
          openBtn.title = "Open video in browser";
          openBtn.style.cssText = `
            padding: 4px 10px; border: 1px solid var(--border-primary); border-radius: 4px;
            background: var(--background-secondary); color: var(--text-secondary);
            cursor: pointer; font-size: 11px;
          `;
          openBtn.addEventListener("click", () => {
            const win = Zotero.getMainWindow();
            if (win) (win as any).open(result.videoUrl, "_blank");
          });
          actionsRow.appendChild(openBtn);

          // Save video
          const saveVideoBtn = doc.createElementNS(
            HTML_NS,
            "button",
          ) as HTMLButtonElement;
          saveVideoBtn.innerText = "\u{2B07}\u{FE0F} Save";
          saveVideoBtn.title = "Save video to disk";
          saveVideoBtn.style.cssText = `
            padding: 4px 10px; border: 1px solid var(--border-primary); border-radius: 4px;
            background: var(--background-secondary); color: var(--text-secondary);
            cursor: pointer; font-size: 11px;
          `;
          saveVideoBtn.addEventListener("click", async () => {
            try {
              const win = Zotero.getMainWindow() as any;
              const fp = new win.FilePicker();
              fp.init(win, "Save Generated Video", fp.modeSave);
              fp.defaultString = `seerai_video_${Date.now()}.mp4`;
              fp.appendFilter("MP4 Video", "*.mp4");
              fp.appendFilter("WebM Video", "*.webm");
              const res = await fp.show();
              if (res === fp.returnOK || res === fp.returnReplace) {
                const resp = await fetch(result.videoUrl!, {});
                const arrayBuf = await resp.arrayBuffer();
                await IOUtils.write(fp.file, new Uint8Array(arrayBuf));
                Zotero.debug(`[seerai] Video saved to: ${fp.file}`);
              }
            } catch (saveErr: any) {
              Zotero.debug(`[seerai] Video save error: ${saveErr.message}`);
            }
          });
          actionsRow.appendChild(saveVideoBtn);

          vidContainer.appendChild(actionsRow);
        } else {
          // No video URL — show metadata
          const noUrlDiv = doc.createElement("div");
          noUrlDiv.style.cssText =
            "color: var(--text-secondary); font-style: italic;";
          noUrlDiv.textContent = `Video generation completed (runId: ${result.runId}) but no video URL was returned. Check the provider dashboard.`;
          vidContainer.appendChild(noUrlDiv);
        }

        // Cost info
        if (result.cost !== undefined) {
          const costDiv = doc.createElement("div");
          costDiv.style.cssText =
            "font-size: 10px; color: var(--text-tertiary); margin-top: 4px;";
          costDiv.textContent = `Cost: $${result.cost.toFixed(4)}${result.remainingBalance !== undefined ? ` | Balance: $${result.remainingBalance.toFixed(2)}` : ""}`;
          vidContainer.appendChild(costDiv);
        }

        genContent.appendChild(vidContainer);

        // Store the result in conversation
        const assistantMsg: ChatMessage = {
          id: genMsgId,
          role: "assistant",
          content: `🎬 Generated video for: "${promptText.substring(0, 100)}"${result.videoUrl ? ` — ${result.videoUrl}` : ""}`,
          timestamp: new Date(),
        };
        conversationMessages.push(assistantMsg);
        try {
          await getMessageStore().appendMessage(assistantMsg);
        } catch (e) {
          Zotero.debug(`[seerai] Error saving video gen result message: ${e}`);
        }
      } catch (err: any) {
        genContent.innerHTML = "";
        const mdContainer = doc.createElement("div");
        mdContainer.className = "markdown-content";
        mdContainer.setAttribute(
          "data-raw",
          `Video generation failed: ${err.message || err}`,
        );
        mdContainer.innerHTML = parseMarkdown(
          `**Video generation failed:**\n\n${err.message || err}`,
        );
        genContent.appendChild(mdContainer);
        Zotero.debug(`[seerai] Video generation error: ${err.message || err}`);
      }

      messagesArea.scrollTop = messagesArea.scrollHeight;
    });

    videoGenContainer.appendChild(videoGenBtn);
    videoGenContainer.appendChild(videoGenDropupBtn);

    // ── Combined Media Generation Button (Image + Video in one split button) ──
    // This replaces the separate imageGen/videoGen buttons in textInputRow,
    // freeing the text input row to just textarea + send button.
    let mediaGenMode: "image" | "video" = "image";

    const mediaGenContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        alignItems: "center",
        position: "relative",
      },
    });

    const mediaGenMainBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "\u{1F3A8}",
        title: "Generate Image from prompt",
      },
      styles: {
        padding: "0 8px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px 0 0 6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
    }) as HTMLButtonElement;

    // Main button click → trigger the appropriate gen handler
    mediaGenMainBtn.addEventListener("click", () => {
      if (mediaGenMode === "image") {
        imageGenBtn.click();
      } else {
        videoGenBtn.click();
      }
    });

    const mediaGenDropBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "\u25BC", title: "Media generation options" },
      styles: {
        padding: "0 4px",
        height: "32px",
        fontSize: "8px",
        border: "1px solid var(--border-primary)",
        borderLeft: "none",
        borderRadius: "0 6px 6px 0",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      },
    }) as HTMLButtonElement;

    // Dropdown menu for mode selection + settings access
    mediaGenDropBtn.addEventListener("click", (e) => {
      e.stopPropagation();
      e.preventDefault();
      const existing = doc.querySelector(".seerai-media-gen-dropdown");
      if (existing) {
        existing.remove();
        return;
      }

      const dropdown = doc.createElement("div");
      dropdown.className = "seerai-media-gen-dropdown";
      dropdown.style.cssText = `
        position: absolute; bottom: 38px; left: 0; z-index: 1000;
        background: var(--background-primary); border: 1px solid var(--border-primary);
        border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        padding: 4px; min-width: 180px; font-size: 12px;
      `;

      // Helper to build a menu row
      const makeRow = (
        label: string,
        isActive: boolean,
        onClick: () => void,
      ) => {
        const row = doc.createElement("div");
        row.style.cssText = `
          padding: 6px 10px; cursor: pointer; border-radius: 4px;
          display: flex; align-items: center; gap: 6px;
          background: ${isActive ? "var(--accent-color, #007AFF)" : "transparent"};
          color: ${isActive ? "#fff" : "var(--text-primary)"};
        `;
        row.textContent = label;
        row.addEventListener("mouseenter", () => {
          if (!isActive) row.style.background = "var(--background-secondary)";
        });
        row.addEventListener("mouseleave", () => {
          if (!isActive) row.style.background = "transparent";
        });
        row.addEventListener("click", (ev) => {
          ev.stopPropagation();
          onClick();
          dropdown.remove();
        });
        return row;
      };

      // Mode selection rows
      dropdown.appendChild(
        makeRow("\u{1F3A8} Image Generation", mediaGenMode === "image", () => {
          mediaGenMode = "image";
          (mediaGenMainBtn as HTMLElement).textContent = "\u{1F3A8}";
          (mediaGenMainBtn as HTMLElement).title = "Generate Image from prompt";
        }),
      );
      dropdown.appendChild(
        makeRow("\u{1F3AC} Video Generation", mediaGenMode === "video", () => {
          mediaGenMode = "video";
          (mediaGenMainBtn as HTMLElement).textContent = "\u{1F3AC}";
          (mediaGenMainBtn as HTMLElement).title = "Generate Video from prompt";
        }),
      );

      // Divider
      const divider = doc.createElement("div");
      divider.style.cssText =
        "height: 1px; background: var(--border-primary); margin: 4px 0;";
      dropdown.appendChild(divider);

      // Settings access rows
      dropdown.appendChild(
        makeRow("\u2699 Image Settings...", false, () => {
          imageGenDropupBtn.click();
        }),
      );
      dropdown.appendChild(
        makeRow("\u2699 Video Settings...", false, () => {
          videoGenDropupBtn.click();
        }),
      );

      mediaGenContainer.appendChild(dropdown);

      // Close on click-outside
      const closeHandler = (ev: MouseEvent) => {
        if (!dropdown.contains(ev.target as Node)) {
          dropdown.remove();
          doc.removeEventListener("mousedown", closeHandler);
        }
      };
      setTimeout(() => doc.addEventListener("mousedown", closeHandler), 0);
    });

    mediaGenContainer.appendChild(mediaGenMainBtn);
    mediaGenContainer.appendChild(mediaGenDropBtn);

    // Add media gen button to toolbar (frees up textInputRow)
    // webSearchBtn will be appended to toolbarLeft after it's created below
    toolbarLeft.appendChild(mediaGenContainer);

    toolbarRow.appendChild(toolbarLeft);

    // New Chat button
    const newChatBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "\u2795", title: "New Chat" },
      styles: {
        padding: "0 10px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: "var(--background-secondary)",
        color: "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        transition: "all 0.2s ease",
      },
      listeners: [
        {
          type: "click",
          listener: async () => {
            await this.createNewChat();
          },
        },
      ],
    });

    toolbarRight.appendChild(stopBtn);
    toolbarRight.appendChild(newChatBtn);
    toolbarRight.appendChild(clearBtn);
    toolbarRight.appendChild(saveBtn);
    toolbarRow.appendChild(toolbarRight);

    // Web Search toggle button
    const webSearchBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: {
        innerText: "\uD83D\uDD0D",
        title: "Toggle web search (enriches AI context with web results)",
      },
      styles: {
        padding: "0 8px",
        height: "32px",
        fontSize: "13px",
        border: "1px solid var(--border-primary)",
        borderRadius: "6px",
        backgroundColor: stateManager.getOptions().webSearchEnabled
          ? "var(--accent-color, #007AFF)"
          : "var(--background-secondary)",
        color: stateManager.getOptions().webSearchEnabled
          ? "#fff"
          : "var(--text-secondary)",
        cursor: "pointer",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        flexShrink: "0",
        transition: "background-color 0.2s, color 0.2s",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            const current = stateManager.getOptions().webSearchEnabled;
            const newState = !current;
            stateManager.setOptions({ webSearchEnabled: newState });

            // Update button visual state
            const btn = webSearchBtn as HTMLElement;
            if (newState) {
              btn.style.backgroundColor = "var(--accent-color, #007AFF)";
              btn.style.color = "#fff";
              btn.style.borderColor = "var(--accent-color, #007AFF)";
              btn.title = "Web search ON - click to disable";
            } else {
              btn.style.backgroundColor = "var(--background-secondary)";
              btn.style.color = "var(--text-secondary)";
              btn.style.borderColor = "var(--border-primary)";
              btn.title =
                "Toggle web search (enriches AI context with web results)";
            }

            Zotero.debug(`[seerai] Web search toggled: ${newState}`);
          },
        },
      ],
    }) as HTMLButtonElement;

    // Set initial border color for active state
    if (stateManager.getOptions().webSearchEnabled) {
      (webSearchBtn as HTMLElement).style.borderColor =
        "var(--accent-color, #007AFF)";
      (webSearchBtn as HTMLElement).title = "Web search ON - click to disable";
    }

    // Add web search toggle to toolbar row (beside media gen button)
    toolbarLeft.appendChild(webSearchBtn);

    // Text input row: just textarea + send button (image/video/search moved to toolbar)
    textInputRow.appendChild(input);
    textInputRow.appendChild(sendBtn);

    // Assemble: toolbar row on top, text input row below
    inputArea.appendChild(toolbarRow);
    inputArea.appendChild(textInputRow);

    if (contextChipsArea) {
      inputContainer.appendChild(contextChipsArea);
    }
    inputContainer.appendChild(imagePreviewArea);
    inputContainer.appendChild(inputArea);
    return inputContainer;
  }

  /**
   * Handle an inline permission request from the agent engine.
   * Finds the UI element for the tool call and appends Allow/Deny buttons.
   */
  private static async handleInlinePermissionRequest(
    toolCallId: string,
    toolName: string,
  ): Promise<boolean> {
    Zotero.debug(
      `[seerai] handleInlinePermissionRequest started for ${toolName} (${toolCallId})`,
    );

    if (!activeAgentSession) {
      Zotero.debug("[seerai] Permission Error: No activeAgentSession found");
      return false;
    }

    if (!activeAgentSession.contentDiv) {
      Zotero.debug(
        "[seerai] Permission Error: activeAgentSession.contentDiv is null",
      );
      return false;
    }

    Zotero.debug(
      `[seerai] session.toolResults count: ${activeAgentSession.toolResults.length}`,
    );
    activeAgentSession.toolResults.forEach((r) =>
      Zotero.debug(`[seerai] candidate tool id: ${r.toolCall.id}`),
    );

    const tr = activeAgentSession.toolResults.find(
      (t) => t.toolCall.id === toolCallId,
    );
    const toolElement = tr?.uiElement;

    if (!toolElement) {
      Zotero.debug(
        `[seerai] Permission Error: Could not find UI element for tool call ${toolCallId} in session results`,
      );
      return false;
    }

    Zotero.debug(
      "[seerai] Found toolElement, looking for .tool-details-content",
    );

    // Find the details content area to inject buttons
    const detailsContent = toolElement.querySelector(".tool-details-content");
    if (!detailsContent) {
      Zotero.debug(
        "[seerai] Permission Error: Could not find .tool-details-content in toolElement",
      );
      return false;
    }

    Zotero.debug("[seerai] Found detailsContent, creating permission buttons");

    // Automatically expand the parent tool list and the individual tool card
    if (activeAgentSession.toolProcessState?.container) {
      (activeAgentSession.toolProcessState.container as any).open = true;
    }
    if (
      toolElement.tagName === "DETAILS" ||
      (toolElement as any).open !== undefined
    ) {
      (toolElement as any).open = true;
    }

    // Create the permission UI container
    const doc = toolElement.ownerDocument;
    if (!doc) return false;
    const permissionContainer = doc.createElement("div");
    permissionContainer.className = "permission-request-ui";
    permissionContainer.style.cssText = `
          margin-top: 10px;
          padding: 8px;
          background: var(--background-secondary, #f5f5f5);
          border-radius: 6px;
          border-left: 3px solid #ff9800;
          animation: fade-in 0.3s ease;
      `;

    const promptText = doc.createElement("div");
    promptText.style.cssText =
      "margin-bottom: 8px; font-weight: 500; font-size: 0.9em;";
    promptText.textContent = "⚠️ Permission Required";

    const subText = doc.createElement("div");
    subText.style.cssText =
      "margin-bottom: 8px; font-size: 0.85em; color: var(--text-secondary);";
    subText.textContent = "The agent wants to execute this tool. Allow?";

    const btnContainer = doc.createElement("div");
    btnContainer.style.cssText =
      "display: flex; gap: 8px; justify-content: flex-end;";

    const denyBtn = doc.createElementNS(HTML_NS, "button") as HTMLButtonElement;
    denyBtn.textContent = "Deny";
    denyBtn.style.cssText = `
          padding: 4px 12px;
          border: 1px solid var(--border-primary);
          border-radius: 4px;
          background: var(--background-primary);
          cursor: pointer;
          font-size: 0.85em;
      `;

    const allowBtn = doc.createElementNS(
      HTML_NS,
      "button",
    ) as HTMLButtonElement;
    allowBtn.textContent = "Allow";
    allowBtn.style.cssText = `
          padding: 4px 12px;
          border: none;
          border-radius: 4px;
          background: var(--highlight-primary, #1976d2);
          color: white;
          cursor: pointer;
          font-size: 0.85em;
      `;

    permissionContainer.appendChild(promptText);
    permissionContainer.appendChild(subText);
    btnContainer.appendChild(denyBtn);
    btnContainer.appendChild(allowBtn);
    permissionContainer.appendChild(btnContainer);

    detailsContent.appendChild(permissionContainer);

    return new Promise<boolean>((resolve) => {
      const cleanup = () => {
        permissionContainer.remove();
      };

      allowBtn.onclick = (e) => {
        e.preventDefault();
        e.stopPropagation();
        cleanup();
        resolve(true);
      };

      denyBtn.onclick = (e) => {
        e.preventDefault();
        e.stopPropagation();
        cleanup();
        resolve(false);
      };
    });
  }

  /**
   * Handle send with streaming response and pasted images (vision mode)
   */
  private static async handleSendWithStreamingAndImages(
    input: HTMLInputElement,
    messagesArea: HTMLElement,
    stateManager: ReturnType<typeof getChatStateManager>,
    pastedImages: { id: string; image: string; mimeType: string }[],
    clearImages: () => void,
  ) {
    // Reset abort state for a new request
    openAIService.resetAbortState();

    // Auto-refresh drive context files (check for newer versions on Drive)
    const activeChatId = getMessageStore().getConversationId();
    if (activeChatId) {
      try {
        await refreshDriveContextForChat(activeChatId);
      } catch (e) {
        Zotero.debug(`[seerai] Drive refresh error: ${e}`);
      }
    }

    const text = input.value.trim();
    // Allow sending with just images
    if ((!text && pastedImages.length === 0) || this.isStreaming) return;

    // Store and display user message
    const userMsg: ChatMessage = {
      id: Date.now().toString(),
      role: "user",
      content:
        pastedImages.length > 0
          ? `${text} [+${pastedImages.length} image(s)]`
          : text,
      timestamp: new Date(),
    };
    conversationMessages.push(userMsg);

    // Persist user message
    try {
      await getMessageStore().appendMessage(userMsg);
    } catch (e) {
      Zotero.debug(`[seerai] Error saving user message: ${e}`);
    }

    const displayText =
      pastedImages.length > 0
        ? `${text || "(no text)"} 🖼️ ${pastedImages.length} image(s)`
        : text;
    this.appendMessage(messagesArea, "You", displayText, userMsg.id, true);

    input.value = "";
    currentDraftText = ""; // Clear persisted draft
    input.disabled = true;
    this.isStreaming = true;

    // Show stop button
    const stopBtn = messagesArea.ownerDocument?.getElementById(
      "stop-btn",
    ) as HTMLElement;
    if (stopBtn) stopBtn.style.display = "inline-block";

    // Create streaming message placeholder with loading indicator
    const loadingHtml = `
            <div class="typing-indicator" style="display: flex; align-items: center; gap: 4px; color: var(--text-secondary); font-style: italic;">
                <span>Thinking</span>
                <span class="dot" style="animation: blink 1.4s infinite .2s;">.</span>
                <span class="dot" style="animation: blink 1.4s infinite .4s;">.</span>
    <span class="dot" style="animation: blink 1.4s infinite .6s;">.</span>
            </div>
            <style>
                @keyframes blink { 0% { opacity: .2; } 20% { opacity: 1; } 100% { opacity: .2; } }
            </style>
        `;
    const streamingDiv = this.appendMessage(
      messagesArea,
      "Assistant",
      "",
      undefined,
      false,
      undefined,
      1, // Start at turn 1 for new agent sessions
    );
    const contentDiv = streamingDiv.querySelector(
      "[data-content]",
    ) as HTMLElement;
    contentDiv.innerHTML = loadingHtml;

    // Initialize active agent session
    activeAgentSession = {
      text,
      fullResponse: "",
      toolResults: [],
      isThinking: true,
      iterationCount: 1, // Start at turn 1
      messagesArea: messagesArea,
      contentDiv: contentDiv,
      toolContainer: null,
      isToolDetailsOpen: false, // Default closed until tools run
      currentTextBlock: null,
      turnTextSoFar: "",
      previousTurnsTextLength: 0,
      completedTodos: 0,
      totalTodos: 0,
      lastToolName: "",
      lastToolSummary: "",
      cumulativeTokens: 0,
    };

    const session = activeAgentSession;

    const smartScrollToBottom = () => {
      if (!messagesArea) return;
      const threshold = 100; // pixels from bottom
      const isNearBottom =
        messagesArea.scrollHeight - messagesArea.scrollTop <=
        messagesArea.clientHeight + threshold;
      if (isNearBottom) {
        messagesArea.scrollTop = messagesArea.scrollHeight;
      }
    };

    const refreshAgentStats = () => {
      if (!session || !session.toolProcessState) return;

      const maxIterations =
        getAgentConfigFromPrefs().maxAgentIterations || Infinity;

      session.toolProcessState.updateStats({
        turns: session.iterationCount,
        totalTools: session.toolResults.length,
        estimatedTokens: session.cumulativeTokens,
        completedTodos: session.completedTodos,
        totalTodos: session.totalTodos,
        lastToolName: session.lastToolName,
        lastToolSummary: session.lastToolSummary,
        isThinking: session.isThinking,
      });
    };

    const observer: AgentUIObserver = {
      onToken: (token: string, fullResponse: string) => {
        if (!Assistant.isStreaming) return;
        session.fullResponse = fullResponse;
        if (session.contentDiv && session.currentTextBlock) {
          // Process streaming content to handle think tags
          const { displayContent, isThinking: stillThinking } =
            processStreamingContent(fullResponse);

          // If we're still in a think block with no visible content, keep the thinking indicator
          if (stillThinking && session.isThinking) {
            return;
          }

          if (session.isThinking && displayContent) {
            // Clear initial typing indicator surgically
            session.contentDiv.querySelector(".typing-indicator")?.remove();
            session.isThinking = false;
          }

          // Extract only new text for this turn
          const turnText = fullResponse.slice(session.previousTurnsTextLength);
          if (turnText && turnText !== session.turnTextSoFar) {
            session.turnTextSoFar = turnText;
            // Process only the current turn's text for think tags
            const turnProcessed = processStreamingContent(turnText);
            const turnDisplay = turnProcessed.displayContent;
            if (turnDisplay) {
              this.smartRender(session.currentTextBlock!, turnDisplay);
            }
          }
        }
        if (session.messagesArea) {
          smartScrollToBottom();
        }
      },
      onIterationStarted: (iteration: number) => {
        if (!Assistant.isStreaming) return;
        session.isThinking = true;
        session.iterationCount = iteration;
        session.previousTurnsTextLength = session.fullResponse.length;
        session.turnTextSoFar = "";

        // Start a new text block for this turn
        if (session.contentDiv) {
          const doc = session.contentDiv.ownerDocument!;
          const block = doc.createElementNS(HTML_NS, "div") as HTMLElement;
          block.className = "markdown-content agent-turn-text";
          // Insert new text block before the tool process container or at end
          if (session.toolProcessState?.container) {
            session.contentDiv.insertBefore(
              block,
              session.toolProcessState.container,
            );
          } else {
            session.contentDiv.appendChild(block);
          }
          session.currentTextBlock = block;
        }

        if (session.toolProcessState) {
          session.toolProcessState.setThinking();
        }
        refreshAgentStats();
      },
      onPreApiCall: (estimatedInputTokens: number) => {
        session.cumulativeTokens = Math.max(
          session.cumulativeTokens,
          estimatedInputTokens,
        );
        refreshAgentStats();
      },
      onToolCallStarted: (toolCall: ToolCall) => {
        if (!Assistant.isStreaming) return;
        session.isThinking = false;

        if (session.messagesArea && session.contentDiv) {
          const doc = session.messagesArea.ownerDocument!;

          // Initial creation of the process container logic
          if (!session.toolProcessState) {
            session.contentDiv.querySelector(".typing-indicator")?.remove();

            const processUI = createToolProcessUI(doc);
            session.toolProcessState = processUI;
            session.contentDiv.appendChild(processUI.container);

            processUI.setThinking();

            (processUI.container as any).open = true;
            session.isToolDetailsOpen = true;

            processUI.container.addEventListener("toggle", (e: Event) => {
              if (session) {
                session.isToolDetailsOpen = (
                  e.target as HTMLDetailsElement
                ).open;
              }
            });

            session.toolContainer = processUI.container.querySelector(
              ".tool-list-container",
            ) as HTMLElement;
          }

          // Update label to show which tool is being executed
          session.toolProcessState.setExecutingTool(toolCall.function.name);
          session.lastToolName = toolCall.function.name;
          session.lastToolSummary = "";
          refreshAgentStats();

          const toolUI = createToolExecutionUI(doc, toolCall);

          // Append tool card inside the process container's list
          if (session.toolContainer) {
            session.toolContainer.appendChild(toolUI);
          } else {
            session.contentDiv.appendChild(toolUI);
          }

          // Auto-expand the process container so tool cards are visible
          if (session.toolProcessState?.container) {
            (session.toolProcessState.container as any).open = true;
          }

          session.toolResults.push({ toolCall, uiElement: toolUI });
          smartScrollToBottom();
        } else {
          session.toolResults.push({ toolCall });
        }
      },
      onToolCallCompleted: (toolCall: ToolCall, result: ToolResult) => {
        if (!Assistant.isStreaming) return;
        const tr = session.toolResults.find(
          (t) => t.toolCall.id === toolCall.id,
        );
        if (tr) tr.result = result;

        if (session.messagesArea && session.contentDiv && tr?.uiElement) {
          const doc = session.messagesArea.ownerDocument!;
          const newUI = createToolExecutionUI(doc, toolCall, result);
          if (tr.uiElement.parentNode) {
            tr.uiElement.parentNode.replaceChild(newUI, tr.uiElement);
          }
          tr.uiElement = newUI;
          smartScrollToBottom();
        }

        // Render standalone question panel for workspace_question
        if (
          toolCall.function.name === "workspace_question" &&
          result.success &&
          result.data &&
          session.contentDiv
        ) {
          const data = result.data as Record<string, unknown>;
          if (data.questions && Array.isArray(data.questions)) {
            const questions = data.questions as any[];
            const doc = session.contentDiv.ownerDocument!;
            const panel = createQuestionPanel(doc, questions);
            session.contentDiv.insertBefore(
              panel,
              session.contentDiv.firstChild,
            );
            smartScrollToBottom();
          }
        }

        // CRITICAL: Update the counter label in real-time
        if (session.toolProcessState) {
          session.toolProcessState.updateProgress(
            session.iterationCount,
            session.toolResults.length,
          );
        }

        if (result.success && result.data) {
          session.lastToolSummary =
            result.summary || JSON.stringify(result.data).slice(0, 60);
          const data = result.data as Record<string, unknown>;
          if (data.todos && Array.isArray(data.todos)) {
            const todos = data.todos as Array<{ status: string }>;
            session.totalTodos = todos.length;
            session.completedTodos = todos.filter(
              (t) => t.status === "completed" || t.status === "cancelled",
            ).length;
          }
        }

        const msgTokens = ChatStateManager.countTokens(
          JSON.stringify(result.data || result.error || ""),
        );
        session.cumulativeTokens += msgTokens;
        refreshAgentStats();
      },
      onMessageUpdate: (content: string) => {
        if (!Assistant.isStreaming) return;
        const cleanedContent = stripThinkTags(content);
        session.fullResponse = cleanedContent;
        if (session.contentDiv && session.currentTextBlock) {
          if (cleanedContent) {
            session.contentDiv.querySelector(".typing-indicator")?.remove();
          }
          session.currentTextBlock.innerHTML = parseMarkdown(cleanedContent);
        }
      },
      onComplete: async (content: string, iterationCount?: number) => {
        // NOTE: Removed early return check for isStreaming - we MUST render final content
        // even if streaming was stopped, otherwise the response is hidden from user

        // Strip think tags from the final content
        const cleanedContent = stripThinkTags(content);
        session.fullResponse = cleanedContent;
        session.isThinking = false;

        // Remove the thinking indicator if it still exists
        if (session.contentDiv) {
          const typingIndicator =
            session.contentDiv.querySelector(".typing-indicator");
          if (typingIndicator) {
            typingIndicator.remove();
          }

          // Render into the current turn's text block if available
          if (cleanedContent && session.currentTextBlock) {
            this.smartRender(session.currentTextBlock, cleanedContent, true);
          }
        }

        // Finalize tool process UI if exists
        if (session.toolProcessState) {
          // Use the authoritative iteration count from handleAgenticChat
          // This fixes race condition where onIterationStarted increments count
          // for iterations that don't complete with tool calls
          const finalCount =
            iterationCount !== undefined
              ? iterationCount
              : session.iterationCount;
          session.iterationCount = finalCount; // Sync session state to authoritative value
          const toolCount = session.toolResults.length;
          session.toolProcessState.setCompleted(finalCount, toolCount);
        }

        const assistantMsg: ChatMessage = {
          id: Date.now().toString(),
          role: "assistant",
          content: cleanedContent,
          timestamp: new Date(),
          iterationCount: session.iterationCount,
          toolResults:
            session.toolResults.length > 0 ? session.toolResults : undefined,
        };
        conversationMessages.push(assistantMsg);

        // Persist assistant message
        try {
          await getMessageStore().appendMessage(assistantMsg);

          // NEW: Auto-titling if this is the first message
          if (conversationMessages.length <= 2) {
            await this.updateConversationTitle(text);
          }
        } catch (e) {
          Zotero.debug(`[seerai] Error saving assistant message: ${e}`);
        }

        // Auto-play TTS if enabled
        if (
          getChatStateManager().getOptions().autoPlayTts &&
          isTtsConfigured() &&
          cleanedContent
        ) {
          autoPlayTtsResponse(cleanedContent).catch((e) =>
            Zotero.debug(`[seerai] Auto-play TTS failed: ${e}`),
          );
        }

        // Cleanup session
        activeAgentSession = null;
        this.isStreaming = false;
        input.disabled = false;
        if (stopBtn) stopBtn.style.display = "none";

        // Clear pasted images after successful send
        clearImages();
      },
      onError: (error: Error) => {
        if (!Assistant.isStreaming) return;
        Zotero.debug(`[seerai] Agent error: ${error}`);
        if (session.contentDiv) {
          if (session.toolProcessState) {
            session.toolProcessState.setFailed(error.message);
          } else {
            const errIndicator =
              session.contentDiv.ownerDocument!.createElement("div");
            errIndicator.style.cssText =
              "margin-top:8px;padding:4px 8px;border-radius:4px;font-size:12px;color:#c62828;background:rgba(198,40,40,0.08);";
            errIndicator.textContent = `Error: ${error.message}`;
            session.contentDiv.appendChild(errIndicator);
          }
        }
        activeAgentSession = null;
        this.isStreaming = false;
        input.disabled = false;
        if (stopBtn) stopBtn.style.display = "none";
      },
    };

    let isFirstToken = true;

    // RAG state — hoisted so catch block can reference them for error recovery
    let ragActive = false;
    const embeddingService = getEmbeddingService();
    const ragConfig = getRAGConfig();

    try {
      // Build context from ChatContextManager
      const contextManager = ChatContextManager.getInstance();
      const options = stateManager.getOptions();

      // Fetch Table Configs if needed (for table items)
      const contextItems = contextManager.getItems();

      // Check for tables
      let storedTables: any[] = [];
      const hasTable = contextItems.some((i) => i.type === "table");
      if (hasTable) {
        try {
          storedTables = await getTableStore().getAllTables();
        } catch (e) {
          Zotero.debug(`[seerai] Error fetching tables for context: ${e}`);
        }
      }

      let context = "=== Context ===\n";
      if (contextItems.length === 0) {
        context +=
          "(No specific context provided. Answer based on general knowledge and web search if enabled.)";
      }

      for (const item of contextItems) {
        if (item.type === "paper") {
          const zoteroItem = Zotero.Items.get(item.id as number);
          if (!zoteroItem) continue;

          context += `\n\n--- Paper: ${item.displayName} ---`;

          const year = zoteroItem.getField("date");
          if (year) context += ` (${year})`;

          const creators = zoteroItem
            .getCreators()
            .map((c: any) => `${c.firstName} ${c.lastName}`);
          if (creators.length > 0)
            context += `\nAuthors: ${creators.join(", ")}`;

          const abstract = zoteroItem.getField("abstractNote");
          if (abstract) context += `\nAbstract: ${abstract}`;

          // Fetch PDF/note content for this item
          try {
            if (zoteroItem.isRegularItem()) {
              // Fetch text (priority logic inside getPdfTextForItem: Notes -> Indexed PDF -> Metadata)
              // We pass includeNotes=true, includeMetadata=true.
              const itemContent = await Assistant.getPdfTextForItem(
                zoteroItem,
                0,
                true,
                true,
              );
              if (itemContent) {
                context += `\n\nContent:\n${itemContent}`;
              }
            }
          } catch (e) {
            Zotero.debug(
              `[seerai] Error fetching content for item ${item.id}: ${e}`,
            );
          }
        } else if (item.type === "table") {
          const tableConfig = storedTables.find((t) => t.id === item.id);
          if (tableConfig) {
            context += `\n\n--- Table: ${tableConfig.name} ---`;

            // List column definitions
            const columnNames = tableConfig.columns.map(
              (c: any) => c.name || c.title,
            );
            context += `\nColumns: ${columnNames.join(", ")}`;
            context += `\nTotal papers: ${tableConfig.addedPaperIds.length}`;

            // Include actual table data (paper rows with all generated column values)
            const generatedData = tableConfig.generatedData || {};

            if (tableConfig.addedPaperIds.length > 0) {
              context += `\n\n=== Table Data ===`;

              // Process all papers in the table (no limit)
              for (const paperId of tableConfig.addedPaperIds) {
                const zoteroItem = Zotero.Items.get(paperId);
                if (!zoteroItem) continue;

                const title = zoteroItem.getField("title") || "Untitled";
                const creators = zoteroItem.getCreators();
                const authorStr =
                  creators.length > 0
                    ? creators
                        .map((c: any) => c.lastName || c.name)
                        .slice(0, 3)
                        .join(", ") + (creators.length > 3 ? " et al." : "")
                    : "";
                const year =
                  zoteroItem.getField("year") ||
                  zoteroItem.getField("date")?.substring(0, 4) ||
                  "";

                context += `\n\n--- Paper: ${title} ---`;
                if (authorStr) context += `\nAuthors: ${authorStr}`;
                if (year) context += ` (${year})`;

                // Include all generated column values for this paper
                const paperData = generatedData[paperId];
                if (paperData && Object.keys(paperData).length > 0) {
                  context += `\n`;
                  for (const column of tableConfig.columns) {
                    const columnId = column.id;
                    const columnName = column.name || column.title || columnId;
                    const value = paperData[columnId];

                    // Skip standard columns that are just metadata
                    if (
                      ["title", "author", "year", "sources"].includes(columnId)
                    )
                      continue;

                    if (value) {
                      context += `\n${columnName}: ${value}`;
                    }
                  }
                }
              }
            }
          }
        } else if (item.type === "tag") {
          const tagName = item.displayName;
          context += `\n\n--- Tag: ${tagName} ---`;

          // Fetch all papers with this tag and include their content (all libraries)
          try {
            const itemIDs = await Assistant.searchAllLibraries(
              "tag",
              "is",
              tagName,
            );

            if (itemIDs.length === 0) {
              context += `\n(No papers found with this tag)`;
            } else {
              context += `\nPapers with this tag: ${itemIDs.length}`;

              // Process all papers with this tag
              for (const itemID of itemIDs) {
                const zoteroItem = Zotero.Items.get(itemID);
                if (!zoteroItem || !zoteroItem.isRegularItem()) continue;

                const title = zoteroItem.getField("title");
                context += `\n\n--- Paper: ${title} ---`;

                const year = zoteroItem.getField("date");
                if (year) context += ` (${year})`;

                const creators = zoteroItem
                  .getCreators()
                  .map((c: any) => `${c.firstName} ${c.lastName}`);
                if (creators.length > 0)
                  context += `\nAuthors: ${creators.join(", ")}`;

                const abstract = zoteroItem.getField("abstractNote");
                if (abstract) context += `\nAbstract: ${abstract}`;

                // Fetch content with hierarchy: All Notes + (Indexed PDF only if no same-title note)
                try {
                  const itemContent = await Assistant.getPdfTextForItem(
                    zoteroItem,
                    0,
                    true,
                    true,
                  );
                  if (itemContent) {
                    context += `\n\nContent:\n${itemContent}`;
                  }
                } catch (e) {
                  Zotero.debug(
                    `[seerai] Error fetching content for tagged item ${itemID}: ${e}`,
                  );
                }
              }
            }
          } catch (e) {
            Zotero.debug(
              `[seerai] Error fetching papers for tag "${tagName}": ${e}`,
            );
            context += `\n(Error fetching papers for this tag)`;
          }
        } else if (item.type === "collection") {
          const collectionId = item.id as number;
          const collectionName = item.displayName;
          context += `\n\n--- Collection: ${collectionName} ---`;

          // Fetch all papers in this collection and include their content
          try {
            const collection = Zotero.Collections.get(collectionId);
            if (collection) {
              const itemIDs = collection.getChildItems(true); // true = recursive

              // Filter to regular items only
              const regularItems: Zotero.Item[] = [];
              for (const id of itemIDs) {
                const zItem = Zotero.Items.get(id);
                if (zItem && zItem.isRegularItem()) {
                  regularItems.push(zItem);
                }
              }

              if (regularItems.length === 0) {
                context += `\n(No papers found in this collection)`;
              } else {
                context += `\nPapers in collection: ${regularItems.length}`;

                // Process all papers in this collection
                for (const zoteroItem of regularItems) {
                  const title = zoteroItem.getField("title");
                  context += `\n\n--- Paper: ${title} ---`;

                  const year = zoteroItem.getField("date");
                  if (year) context += ` (${year})`;

                  const creators = zoteroItem
                    .getCreators()
                    .map((c: any) => `${c.firstName} ${c.lastName}`);
                  if (creators.length > 0)
                    context += `\nAuthors: ${creators.join(", ")}`;

                  const abstract = zoteroItem.getField("abstractNote");
                  if (abstract) context += `\nAbstract: ${abstract}`;

                  // Fetch content with hierarchy: All Notes + (Indexed PDF only if no same-title note)
                  try {
                    const itemContent = await Assistant.getPdfTextForItem(
                      zoteroItem,
                      0,
                      true,
                      true,
                    );
                    if (itemContent) {
                      context += `\n\nContent:\n${itemContent}`;
                    }
                  } catch (e) {
                    Zotero.debug(
                      `[seerai] Error fetching content for collection item ${zoteroItem.id}: ${e}`,
                    );
                  }
                }
              }
            } else {
              context += `\n(Collection not found)`;
            }
          } catch (e) {
            Zotero.debug(
              `[seerai] Error fetching papers for collection "${collectionName}": ${e}`,
            );
            context += `\n(Error fetching papers for this collection)`;
          }
        } else if (item.type === "author") {
          const authorName = item.displayName;
          context += `\n\n--- Author: ${authorName} ---`;

          // Fetch all papers by this author and include their content (all libraries)
          try {
            const itemIDs = await Assistant.searchAllLibraries(
              "creator",
              "contains",
              authorName,
            );

            if (itemIDs.length === 0) {
              context += `\n(No papers found by this author)`;
            } else {
              context += `\nPapers by this author: ${itemIDs.length}`;

              // Process all papers by this author
              for (const itemID of itemIDs) {
                const zoteroItem = Zotero.Items.get(itemID);
                if (!zoteroItem || !zoteroItem.isRegularItem()) continue;

                const title = zoteroItem.getField("title");
                context += `\n\n--- Paper: ${title} ---`;

                const year = zoteroItem.getField("date");
                if (year) context += ` (${year})`;

                const creators = zoteroItem
                  .getCreators()
                  .map((c: any) => `${c.firstName} ${c.lastName}`);
                if (creators.length > 0)
                  context += `\nAuthors: ${creators.join(", ")}`;

                const abstract = zoteroItem.getField("abstractNote");
                if (abstract) context += `\nAbstract: ${abstract}`;

                // Fetch content with hierarchy: All Notes + (Indexed PDF only if no same-title note)
                try {
                  const itemContent = await Assistant.getPdfTextForItem(
                    zoteroItem,
                    0,
                    true,
                    true,
                  );
                  if (itemContent) {
                    context += `\n\nContent:\n${itemContent}`;
                  }
                } catch (e) {
                  Zotero.debug(
                    `[seerai] Error fetching content for author item ${itemID}: ${e}`,
                  );
                }
              }
            }
          } catch (e) {
            Zotero.debug(
              `[seerai] Error fetching papers for author "${authorName}": ${e}`,
            );
            context += `\n(Error fetching papers for this author)`;
          }
        } else if (item.type === "topic") {
          // Topic is a user-specified keyword/focus area
          context += `\n\nFocus Topic: "${item.displayName}" - Please consider this topic when answering.`;
        } else if (item.type === "file") {
          const filename =
            (item.metadata?.filename as string) || item.displayName;
          const workspacePath = item.metadata?.workspacePath as
            | string
            | undefined;

          let resolvedContent: string | undefined;
          if (workspacePath) {
            try {
              const store = getWorkspaceStore();
              const absPath = PathUtils.join(store.workspaceDir, workspacePath);
              if (absPath.toLowerCase().endsWith(".docx")) {
                const raw = await Zotero.File.getBinaryContentsAsync(absPath);
                const ab = raw as unknown as ArrayBuffer;
                const result = await convertDocxToMarkdown(ab);
                resolvedContent = stripBase64Data(result.markdown);
              } else {
                const raw = await IOUtils.read(absPath);
                resolvedContent = stripBase64Data(
                  new TextDecoder().decode(raw),
                );
              }
            } catch (e) {
              Zotero.debug(
                `[seerai] Failed to resolve workspace file ${filename}: ${e}`,
              );
            }
          }

          const category = item.metadata?.fileCategory as string | undefined;
          const extractedContent = item.metadata?.extractedContent as
            | string
            | undefined;
          const charCount = item.metadata?.charCount as number | undefined;
          const extractionError = item.metadata?.extractionError as
            | string
            | undefined;

          const safeContent =
            resolvedContent ??
            (extractedContent ? stripBase64Data(extractedContent) : undefined);

          context += `\n\n--- File: ${filename} ---`;

          if (safeContent && safeContent.length > 0) {
            if (category === "audio") {
              context += `\n[Transcribed audio: ${filename}]\n${safeContent}`;
            } else {
              context += `\n${safeContent}`;
            }
            Zotero.debug(
              `[seerai] File context injected: ${filename} (${charCount ?? safeContent?.length ?? 0} chars, ${category})`,
            );
          } else if (extractionError) {
            context += `\n(${extractionError})`;
          } else {
            context += `\n(No text content could be extracted from this file)`;
          }
        } else if (item.type === "workspace") {
          const workspaceDir = item.metadata?.workspaceDir as
            | string
            | undefined;
          const workspaceType = item.metadata?.workspaceType as
            | string
            | undefined;
          const convId = item.metadata?.conversationId as string | undefined;

          context += `\n\n--- Workspace: ${item.displayName}${workspaceType === "chat" && convId ? ` (chat ${convId.slice(0, 8)})` : ""} ---`;

          if (!workspaceDir) {
            context += `\n(Workspace directory not found)`;
          } else {
            try {
              const files: Array<{ path: string; content: string }> = [];

              async function scanDir(
                dir: string,
                relativePath: string,
              ): Promise<void> {
                let children: string[];
                try {
                  children = await IOUtils.getChildren(dir);
                } catch {
                  return;
                }
                for (const fullPath of children) {
                  const name = fullPath.split("/").pop() || "";
                  if (name.startsWith(".") || name === "node_modules") continue;
                  let isDir = false;
                  try {
                    isDir = (await IOUtils.stat(fullPath)).type === "directory";
                  } catch {
                    continue;
                  }
                  if (isDir) {
                    await scanDir(
                      fullPath,
                      relativePath ? `${relativePath}/${name}` : name,
                    );
                  } else {
                    const rel = relativePath ? `${relativePath}/${name}` : name;
                    try {
                      const bytes = await IOUtils.read(fullPath);
                      const text = new TextDecoder().decode(bytes);
                      files.push({ path: rel, content: text });
                    } catch {
                      /* skip unreadable */
                    }
                  }
                }
              }

              await scanDir(workspaceDir, "");

              if (files.length === 0) {
                context += `\n(No files in this workspace)`;
              } else {
                context += `\nFiles: ${files.length}`;
                for (const file of files) {
                  const safe = stripBase64Data(file.content);
                  const maxChars = 50000;
                  const truncated =
                    safe.length > maxChars
                      ? safe.slice(0, maxChars) +
                        `\n... [truncated, ${safe.length - maxChars} more chars]`
                      : safe;
                  context += `\n\n--- Workspace File: ${file.path} ---\n${truncated}`;
                }
              }
            } catch (e) {
              Zotero.debug(`[seerai] Error reading workspace files: ${e}`);
              context += `\n(Error reading workspace files)`;
            }
          }
        }
      }

      // ── RAG: Check if semantic search should replace full-text context ──
      let ragStats: {
        chunksRetrieved: number;
        tokensUsed: number;
        queryTimeMs: number;
      } | null = null;

      // Determine if RAG should activate based on context size
      const estimatedTokens = ChatStateManager.countTokens(context);
      const activeModelForRAG = getActiveModelConfig();

      // Read per-model RAG settings, falling back to global pref defaults
      const modelRagAlwaysUse = activeModelForRAG?.ragAlwaysUse ?? false;
      const modelRagThreshold =
        activeModelForRAG?.ragTokenThreshold ?? ragConfig.tokenThreshold;

      // Check per-conversation override first, then global pref.
      // If the user explicitly disabled the toggle, RAG never activates.
      const ragEnabledOverride = options.ragEnabled;
      const effectiveRAGEnabled =
        ragEnabledOverride !== undefined
          ? ragEnabledOverride
          : ragConfig.enabled;

      // Check if RAG should activate (embedding must be configured AND
      // the user must not have disabled it). shouldActivateRAG checks
      // always-use flag and token threshold.
      if (
        effectiveRAGEnabled &&
        embeddingService.isConfigured() &&
        shouldActivateRAG(estimatedTokens, modelRagAlwaysUse, modelRagThreshold)
      ) {
        // Declare ragProgressUI outside try so catch block can signal failure
        let ragProgressUI: ReturnType<typeof createRAGProgressUI> | null = null;
        try {
          Zotero.debug(
            `[seerai] RAG activating: estimated ${estimatedTokens} tokens, ` +
              `threshold=${modelRagThreshold}, alwaysUse=${modelRagAlwaysUse}`,
          );

          // ── Replace "Thinking..." with live RAG progress UI ──
          if (contentDiv) {
            const ragDoc = contentDiv.ownerDocument!;
            contentDiv.querySelector(".typing-indicator")?.remove();
            ragProgressUI = createRAGProgressUI(ragDoc);
            contentDiv.appendChild(ragProgressUI.container);
          }

          // Expand non-paper context items (tags, collections, authors → paper IDs)
          // and collect passthrough content (tables, files, topics)
          const { expandedItems, passthroughContext } =
            await Assistant.expandContextItemsForRAG(contextItems);

          Zotero.debug(
            `[seerai] RAG expanded: ${contextItems.length} context items → ${expandedItems.length} papers, ` +
              `${passthroughContext.length > 0 ? "with" : "no"} passthrough content`,
          );

          const ragResult = await retrieveContext(
            text,
            expandedItems,
            Assistant.extractContentForRAG,
            {
              topK: activeModelForRAG?.ragTopK || ragConfig.topK,
              maxTokens: ragConfig.tokenThreshold || estimatedTokens,
              minScore:
                activeModelForRAG?.ragMinScore !== undefined
                  ? activeModelForRAG.ragMinScore / 100
                  : ragConfig.minScore,
              passthroughContext,
              // Compute token budget from model's context window for smart allocation
              tokenBudget: (() => {
                const contextLength =
                  activeModelForRAG?.contextLength || 128000;
                // Estimate system prompt overhead (~2k tokens for base prompt + tool instructions)
                const systemPromptOverhead = 2000;
                // Estimate conversation history tokens
                const historyTokens = conversationMessages
                  .filter(
                    (m: ChatMessage) =>
                      m.role !== "system" && m.role !== "error",
                  )
                  .reduce(
                    (sum: number, m: ChatMessage) =>
                      sum + ChatStateManager.countTokens(m.content),
                    0,
                  );
                const reservedOutput = options.maxTokens || 4096;
                return computeTokenBudget(
                  contextLength,
                  systemPromptOverhead,
                  historyTokens,
                  0, // webResultTokens — computed later, but RAG runs first
                  reservedOutput,
                );
              })(),
              adaptiveRetrieval: true,
              onProgress: ragProgressUI
                ? (event: RAGProgressEvent) => ragProgressUI!.update(event)
                : undefined,
            },
          );

          if (
            ragResult.context &&
            (ragResult.chunks.length > 0 || passthroughContext.length > 0)
          ) {
            // Replace full-text context with RAG-retrieved passages + passthrough
            context = ragResult.context;
            ragActive = true;
            ragStats = {
              chunksRetrieved: ragResult.stats.chunksRetrieved,
              tokensUsed: ragResult.stats.tokensUsed,
              queryTimeMs: ragResult.stats.queryTimeMs,
            };
            Zotero.debug(
              `[seerai] RAG replaced context: ${ragResult.stats.chunksRetrieved} chunks, ` +
                `${ragResult.stats.tokensUsed} tokens (was ${estimatedTokens}), ` +
                `${ragResult.stats.queryTimeMs}ms`,
            );
          } else {
            Zotero.debug(
              "[seerai] RAG returned no results, keeping full-text context",
            );
          }
        } catch (e) {
          Zotero.debug(`[seerai] RAG failed, falling back to full-text: ${e}`);
          // Signal failure to progress UI so it collapses cleanly
          if (ragProgressUI) {
            ragProgressUI.update({
              step: "complete",
              message: "Retrieval failed — using full context",
            });
          }
          // Keep original context on failure
        }
      }

      // ── Re-add "Thinking..." indicator after RAG completes ──
      // The original indicator was removed when RAG progress UI was inserted.
      // Re-add it so the user sees activity while web search / prompt assembly runs.
      if (ragActive && contentDiv) {
        const thinkDoc = contentDiv.ownerDocument!;
        const thinkIndicator = thinkDoc.createElement("div");
        thinkIndicator.className = "typing-indicator";
        thinkIndicator.style.cssText =
          "display: flex; align-items: center; gap: 4px; color: var(--text-secondary); font-style: italic; margin-top: 8px;";
        thinkIndicator.innerHTML = `
          <span>Thinking</span>
          <span class="dot" style="animation: blink 1.4s infinite .2s;">.</span>
          <span class="dot" style="animation: blink 1.4s infinite .4s;">.</span>
          <span class="dot" style="animation: blink 1.4s infinite .6s;">.</span>
        `;
        contentDiv.appendChild(thinkIndicator);
      }

      // Web Search Context
      let webContext = "";
      
      const isAgentic = isAgenticModeEnabled();
      const isSearchOnly = !isAgentic && options.webSearchEnabled;
      const willUseTools = isAgentic || isSearchOnly;

      // Only do background search if tools are COMPLETELY disabled and we want search
      if (options.webSearchEnabled && !willUseTools && isActiveProviderConfigured()) {
        try {
          const provider = getActiveProvider();
          Zotero.debug(`[seerai] Fetching web search context for: ${text}`);
          const webResults = await provider.webSearch(
            text,
            provider.getSearchLimit(),
          );

          if (webResults.length > 0) {
            webContext = "\n\n=== Web Search Results ===";
            for (const result of webResults) {
              webContext += `\n\n--- ${result.title || "Web Page"} ---`;
              webContext += `\nSource: ${result.url}`;
              if (result.description) {
                webContext += `\n${result.description}`;
              }
              if (result.markdown) {
                // Include full web content (no limit)
                webContext += `\n${result.markdown}`;
              }
            }
            Zotero.debug(
              `[seerai] Added ${webResults.length} web results to context`,
            );
          }
        } catch (e) {
          Zotero.debug(`[seerai] Web search failed: ${e}`);
        }
      }

      const scopePref = this.getScopePref();
      const scopeLabel = this.getScopeLabel(scopePref);

      // Build custom prompt prefix from per-conversation config
      let customPromptPrefix = "";
      const convSystemPrompt = options.systemPrompt;
      const convSkillIds = options.skillIds;

      if (convSystemPrompt || (convSkillIds && convSkillIds.length > 0)) {
        customPromptPrefix = "\n\n[Custom Instructions]\n";
        if (convSystemPrompt) {
          customPromptPrefix += `${convSystemPrompt}\n`;
        }
        if (convSkillIds && convSkillIds.length > 0) {
          try {
            const skills = await getPromptsByCategory("skills");
            const activeSkills = skills.filter((s) =>
              convSkillIds.includes(s.id),
            );
            for (const skill of activeSkills) {
              customPromptPrefix += `\n--- Skill: ${skill.name} ---\n${skill.template}\n`;
            }
          } catch (e) {
            Zotero.debug(`[seerai] Error loading skills: ${e}`);
          }
        }
      }

      const workspaceTree = await buildWorkspaceTreePrompt();

      let agentInstructions = "";
      if (willUseTools) {
        agentInstructions = `You are a research agent with direct access to ${isAgentic ? "the user's Zotero library, web search, and a file workspace" : "web search"}. To help the user, call the tools at your disposal. Do not describe what you could do; use the tools to actually do it. When the task is complete, simply provide a text-only response with your final answer — the conversation will end naturally. If you need to explicitly signal completion after tool work, you can call the 'task_complete' tool instead.

${isAgentic ? buildToolPromptSections() : "Web Search (use the 'web' tool with the 'action' parameter):\n- web({ action: \"search\", query: \"...\" }) — search the web\n- web({ action: \"read\", url: \"...\" }) — read a specific webpage"}
${webContext ? " When using web search results, cite the source URL." : ""}`;
      }

      const systemPrompt =
        customPromptPrefix +
        `You are a helpful research assistant for Zotero. You help users understand and analyze their academic papers, notes, and research data tables.

Current Library/Folder Scope: ${scopeLabel} ${isAgentic ? "(Tools will only find items within this scope)." : ""}

${context}${webContext}

${agentInstructions}
${isAgentic ? WORKSPACE_SYSTEM_PROMPT : ""}
${isAgentic ? workspaceTree : ""}`;

      // Merge pasted images with Zotero item images
      const manuallyPastedParts: VisionMessageContentPart[] = pastedImages.map(
        (img) => ({
          type: "image_url",
          image_url: {
            url: img.image,
            detail: "auto",
          },
        }),
      );

      // Check if we should include images (vision mode)
      let messages: (OpenAIMessage | VisionMessage)[];

      // ── Conversation history budgeting ────────────────────────────────────
      // Limit conversation history to prevent it from consuming the entire
      // context window. Keep at most 20% of the model's context for history,
      // always preserving the most recent messages.
      const historyModelCtx = activeModelForRAG?.contextLength || 128000;
      const historyBudgetTokens = Math.min(historyModelCtx * 0.2, 40000);
      const historyMessages = conversationMessages.filter(
        (m: ChatMessage) => m.role !== "system" && m.role !== "error",
      );

      // Walk backwards from most recent, accumulating tokens
      let historyTokensUsed = 0;
      let historyStartIndex = historyMessages.length;
      for (let i = historyMessages.length - 1; i >= 0; i--) {
        const msgTokens = ChatStateManager.countTokens(
          historyMessages[i].content,
        );
        if (historyTokensUsed + msgTokens > historyBudgetTokens) break;
        historyTokensUsed += msgTokens;
        historyStartIndex = i;
      }

      // If we trimmed history, create a budgeted version
      const budgetedHistory = historyMessages.slice(historyStartIndex);
      if (budgetedHistory.length < historyMessages.length) {
        Zotero.debug(
          `[seerai] History budgeted: ${historyMessages.length} → ${budgetedHistory.length} messages ` +
            `(${historyTokensUsed} tokens, budget=${historyBudgetTokens})`,
        );
      }

      if (options.includeImages || manuallyPastedParts.length > 0) {
        // Get Zotero items for image extraction (Only from selected PAPER items)
        const zoteroItems: Zotero.Item[] = [];
        for (const item of contextItems) {
          if (item.type === "paper") {
            const zItem = Zotero.Items.get(item.id as number);
            if (zItem) zoteroItems.push(zItem);
          }
        }

        // Get image content parts from Zotero items if enabled
        let imageParts: VisionMessageContentPart[] = [];
        if (options.includeImages) {
          imageParts = await createImageContentParts(zoteroItems, 5);
        }

        // Combine with manually pasted images
        const allImageParts = [...manuallyPastedParts, ...imageParts];

        if (allImageParts.length > 0) {
          Zotero.debug(
            `[seerai] Including ${allImageParts.length} images in request (${manuallyPastedParts.length} pasted, ${imageParts.length} from papers)`,
          );

          // Build vision message content
          const visionContent: VisionMessageContentPart[] = [
            { type: "text", text: text },
            ...allImageParts,
          ];

          // Build messages array
          messages = [
            { role: "system", content: systemPrompt },
            // Use budgeted history (excludes current message which we're building with vision)
            ...budgetedHistory
              .slice(0, -1) // Remove the text-only version of current message
              .map((m) => ({
                role: m.role as "user" | "assistant",
                content: m.content,
              })),
            { role: "user", content: visionContent },
          ];
        } else {
          // No images found even though vision mode checked, use standard messages
          messages = [
            { role: "system", content: systemPrompt },
            ...budgetedHistory.map((m) => ({
              role: m.role as "user" | "assistant",
              content: m.content,
            })),
          ];
        }
      } else {
        // Standard text-only messages
        messages = [
          { role: "system", content: systemPrompt },
          ...budgetedHistory.map((m) => ({
            role: m.role as "user" | "assistant",
            content: m.content,
          })),
        ];
      }

      let fullResponse = "";

      // Get active model config for API call
      const activeModel = getActiveModelConfig();
      const chatOptions = getChatStateManager().getOptions();

      // ── Pre-flight: Token budget check ──────────────────────────────────────
      // Estimate total input tokens and verify they fit within the model's
      // context window BEFORE making the API call.
      // Apply a 10% safety margin because our char-based token heuristic
      // can underestimate actual tokenizer output by 20-30%.
      const modelContextLength = activeModel?.contextLength || 128000;
      const reservedOutputTokens = chatOptions.maxTokens || 4096;
      const preFlightSafetyMargin = Math.ceil(modelContextLength * 0.1);
      const totalInputTokens = messages.reduce((sum, m) => {
        const content =
          typeof m.content === "string" ? m.content : JSON.stringify(m.content);
        return sum + ChatStateManager.countTokens(content);
      }, 0);

      if (
        totalInputTokens + reservedOutputTokens + preFlightSafetyMargin >
        modelContextLength
      ) {
        const effectiveLimit = modelContextLength - preFlightSafetyMargin;
        const overBy = totalInputTokens + reservedOutputTokens - effectiveLimit;
        const errMsg =
          `Context too large: ~${totalInputTokens.toLocaleString()} input tokens + ` +
          `${reservedOutputTokens.toLocaleString()} output reserve = ` +
          `${(totalInputTokens + reservedOutputTokens).toLocaleString()} total, ` +
          `exceeding model limit of ${modelContextLength.toLocaleString()} by ~${overBy.toLocaleString()} tokens. ` +
          `Try: remove some context items, set a larger Context Window in model settings, ` +
          `or enable RAG with a lower token threshold.`;
        Zotero.debug(`[seerai] Pre-flight budget check failed: ${errMsg}`);

        // Don't throw — show the error gracefully in the chat UI
        if (contentDiv) {
          contentDiv.querySelector(".typing-indicator")?.remove();
          contentDiv.innerHTML = `<span style="color: #c62828;">${errMsg}</span>`;
        }
        this.isStreaming = false;
        return;
      }

      Zotero.debug(
        `[seerai] Pre-flight budget OK: ${totalInputTokens} input + ${reservedOutputTokens} output ` +
          `= ${totalInputTokens + reservedOutputTokens} / ${modelContextLength} (${(((totalInputTokens + reservedOutputTokens) / modelContextLength) * 100).toFixed(1)}%)`,
      );

      const configOverride = activeModel
        ? {
            apiURL: activeModel.apiURL,
            apiKey: activeModel.apiKey,
            model: activeModel.model,
            temperature: chatOptions.temperature,
            max_tokens: chatOptions.maxTokens,
          }
        : undefined;

      // Check if agentic mode is enabled
      Zotero.debug(`[seerai] Agentic mode: ${isAgentic}, Search-only mode: ${isSearchOnly}`);

      if (willUseTools) {
        const continuation = await getMessageStore().getContinuation();
        await handleAgenticChat(
          text,
          systemPrompt,
          conversationMessages.slice(0, -1), // Exclude current user message (already in text)
          {
            enableTools: true,
            allowedTools: isSearchOnly ? ["web"] : undefined,
            includeImages:
              options.includeImages || manuallyPastedParts.length > 0,
            pastedImages: pastedImages,
            permissionHandler: Assistant.handleInlinePermissionRequest,
            temperature: chatOptions.temperature,
            maxTokens: chatOptions.maxTokens,
            libraryScope:
              scopePref === "selection"
                ? this.resolveSelectionScope()
                : undefined,
            continuation,
          },
          observer,
        );
      } else {
        // Standard chat without tools
        await openAIService.chatCompletionStream(
          messages,
          {
            onToken: (token) => {
              if (!this.isStreaming) return; // Guard against updates after stop
              fullResponse += token;
              if (contentDiv) {
                // Process streaming content to handle think tags
                const { displayContent, isThinking: stillThinking } =
                  processStreamingContent(fullResponse);

                // If still in think block with no content, keep loading indicator
                if (stillThinking && !displayContent) {
                  return;
                }

                if (isFirstToken && displayContent) {
                  // Surgical removal
                  contentDiv.querySelector(".typing-indicator")?.remove();
                  isFirstToken = false;
                }

                if (displayContent) {
                  // Ensure markdown container exists
                  let mdContainer = contentDiv.querySelector(
                    ".markdown-content",
                  ) as HTMLElement;
                  if (!mdContainer) {
                    mdContainer =
                      contentDiv.ownerDocument!.createElement("div");
                    mdContainer.className = "markdown-content";
                    contentDiv.appendChild(mdContainer);
                  }
                  this.smartRender(mdContainer, displayContent);
                  messagesArea.scrollTop = messagesArea.scrollHeight;
                }
              }
            },
            onComplete: async (content) => {
              // Strip think tags from the final content
              const cleanedContent = stripThinkTags(content);
              const assistantMsg: ChatMessage = {
                id: Date.now().toString(),
                role: "assistant",
                content: cleanedContent,
                timestamp: new Date(),
              };
              conversationMessages.push(assistantMsg);

              // Persist assistant message
              try {
                await getMessageStore().appendMessage(assistantMsg);
              } catch (e) {
                Zotero.debug(`[seerai] Error saving assistant message: ${e}`);
              }

              // Final render with markdown — update only the .markdown-content
              // child so that the RAG progress UI (<details>) is preserved.
              if (contentDiv) {
                contentDiv.setAttribute("data-raw", cleanedContent);
                try {
                  let mdContainer = contentDiv.querySelector(
                    ".markdown-content",
                  ) as HTMLElement;
                  if (!mdContainer) {
                    mdContainer =
                      contentDiv.ownerDocument!.createElement("div");
                    mdContainer.className = "markdown-content";
                    contentDiv.appendChild(mdContainer);
                  }
                  mdContainer.innerHTML = parseMarkdown(cleanedContent);
                } catch (e) {
                  Zotero.debug(`[seerai] onComplete render failed: ${e}`);
                  // Fallback: only set text on markdown container if it exists
                  const mdFallback =
                    contentDiv.querySelector(".markdown-content");
                  if (mdFallback) {
                    mdFallback.textContent = cleanedContent;
                  } else {
                    contentDiv.textContent = cleanedContent;
                  }
                }
              }

              // Clear pasted images after successful send
              clearImages();

              // Auto-play TTS if enabled
              if (
                getChatStateManager().getOptions().autoPlayTts &&
                isTtsConfigured() &&
                cleanedContent
              ) {
                autoPlayTtsResponse(cleanedContent).catch((e) =>
                  Zotero.debug(`[seerai] Auto-play TTS failed: ${e}`),
                );
              }
            },
            onError: (error) => {
              if (contentDiv) {
                contentDiv.innerHTML = `<span style="color: #c62828;">Error: ${error.message}</span>`;
              }
            },
          },
          configOverride,
        );
      }
    } catch (error) {
      // ── Strategy C: Error recovery — retry with RAG on context length errors ──
      const errStr = String(error);
      const isContextLengthError =
        /context.?length|maximum.?context|token.?limit|too.?many.?tokens|max_tokens|context_length_exceeded/i.test(
          errStr,
        );

      if (
        isContextLengthError &&
        !ragActive &&
        embeddingService.isConfigured()
      ) {
        Zotero.debug(
          `[seerai] Context length error detected, retrying with RAG: ${errStr}`,
        );
        try {
          const retryContextItems = ChatContextManager.getInstance().getItems();
          // Expand context items for RAG (resolve tags/collections/authors, collect passthrough)
          const {
            expandedItems: retryExpanded,
            passthroughContext: retryPassthrough,
          } = await Assistant.expandContextItemsForRAG(retryContextItems);
          // Fresh model config lookup (activeModelForRAG is scoped to the try block above)
          const cActiveModel = getActiveModelConfig();
          const ragResult = await retrieveContext(
            text,
            retryExpanded,
            Assistant.extractContentForRAG,
            {
              topK: cActiveModel?.ragTopK || ragConfig.topK,
              maxTokens: ragConfig.tokenThreshold || 8000,
              minScore:
                cActiveModel?.ragMinScore !== undefined
                  ? cActiveModel.ragMinScore / 100
                  : ragConfig.minScore,
              passthroughContext: retryPassthrough,
            },
          );

          if (
            ragResult.context &&
            (ragResult.chunks.length > 0 || retryPassthrough.length > 0)
          ) {
            // Rebuild system prompt with RAG context
            const scopePref = this.getScopePref();
            const scopeLabel = this.getScopeLabel(scopePref);
            
            const retryAgenticEnabled = isAgenticModeEnabled();
            const retryChatOptions = getChatStateManager().getOptions();
            const retrySearchOnly = !retryAgenticEnabled && retryChatOptions.webSearchEnabled;
            const retryWillUseTools = retryAgenticEnabled || retrySearchOnly;
            
            let retryAgentInstructions = "";
            if (retryWillUseTools) {
              retryAgentInstructions = `\n\nYou are a research agent with direct access to ${retryAgenticEnabled ? "the user's Zotero library, web search, and a file workspace" : "web search"}. To help the user, call the tools at your disposal. Do not describe what you could do; use the tools to actually do it. When the task is complete, simply provide a text-only response with your final answer.\n\n${retryAgenticEnabled ? buildToolPromptSections() : "Web Search (use the 'web' tool with the 'action' parameter):\n- web({ action: \"search\", query: \"...\" }) — search the web\n- web({ action: \"read\", url: \"...\" }) — read a specific webpage"}`;
            }
            
            const ragSystemPrompt = `You are a helpful research assistant for Zotero. You help users understand and analyze their academic papers, notes, and research data tables.

Current Library/Folder Scope: ${scopeLabel} ${retryAgenticEnabled ? "(Tools will only find items within this scope)." : ""}

${ragResult.context}

Be concise, accurate, and helpful. When referencing papers, cite them by title or author.
Note: Context was automatically reduced using semantic search due to size constraints.${retryAgentInstructions}`;

            const retryMessages: (OpenAIMessage | VisionMessage)[] = [
              { role: "system", content: ragSystemPrompt },
              ...conversationMessages
                .filter((m) => m.role !== "system" && m.role !== "error")
                .map((m) => ({
                  role: m.role as "user" | "assistant",
                  content: m.content,
                })),
            ];

            const retryActiveModel = getActiveModelConfig();
            const retryConfigOverride = retryActiveModel
              ? {
                  apiURL: retryActiveModel.apiURL,
                  apiKey: retryActiveModel.apiKey,
                  model: retryActiveModel.model,
                  temperature: retryChatOptions.temperature,
                  max_tokens: retryChatOptions.maxTokens,
                }
              : undefined;

            Zotero.debug(
              `[seerai] RAG retry: ${ragResult.stats.chunksRetrieved} chunks, ${ragResult.stats.tokensUsed} tokens`,
            );

            if (retryWillUseTools) {
              const continuation = await getMessageStore().getContinuation();
              await handleAgenticChat(
                text,
                ragSystemPrompt,
                conversationMessages.slice(0, -1),
                {
                  enableTools: true,
                  allowedTools: retrySearchOnly ? ["web"] : undefined,
                  includeImages: false,
                  pastedImages: [],
                  permissionHandler: Assistant.handleInlinePermissionRequest,
                  temperature: retryChatOptions.temperature,
                  maxTokens: retryChatOptions.maxTokens,
                  continuation,
                },
                observer,
              );
            } else {
              let retryFullResponse = "";
              let retryIsFirstToken = true;
              await openAIService.chatCompletionStream(
                retryMessages,
                {
                  onToken: (token) => {
                    if (!this.isStreaming) return;
                    retryFullResponse += token;
                    if (contentDiv) {
                      const { displayContent } =
                        processStreamingContent(retryFullResponse);
                      if (retryIsFirstToken && displayContent) {
                        contentDiv.querySelector(".typing-indicator")?.remove();
                        retryIsFirstToken = false;
                      }
                      if (displayContent) {
                        let mdContainer = contentDiv.querySelector(
                          ".markdown-content",
                        ) as HTMLElement;
                        if (!mdContainer) {
                          mdContainer =
                            contentDiv.ownerDocument!.createElement("div");
                          mdContainer.className = "markdown-content";
                          contentDiv.appendChild(mdContainer);
                        }
                        this.smartRender(mdContainer, displayContent);
                        messagesArea.scrollTop = messagesArea.scrollHeight;
                      }
                    }
                  },
                  onComplete: async (content) => {
                    const cleanedContent = stripThinkTags(content);
                    const assistantMsg: ChatMessage = {
                      id: Date.now().toString(),
                      role: "assistant",
                      content: cleanedContent,
                      timestamp: new Date(),
                    };
                    conversationMessages.push(assistantMsg);
                    try {
                      await getMessageStore().appendMessage(assistantMsg);
                    } catch (e) {
                      Zotero.debug(
                        `[seerai] Error saving retry assistant message: ${e}`,
                      );
                    }
                    if (contentDiv) {
                      contentDiv.setAttribute("data-raw", cleanedContent);
                      try {
                        let mdC = contentDiv.querySelector(
                          ".markdown-content",
                        ) as HTMLElement;
                        if (!mdC) {
                          mdC = contentDiv.ownerDocument!.createElement("div");
                          mdC.className = "markdown-content";
                          contentDiv.appendChild(mdC);
                        }
                        mdC.innerHTML = parseMarkdown(cleanedContent);
                      } catch (e) {
                        const mdF =
                          contentDiv.querySelector(".markdown-content");
                        if (mdF) {
                          mdF.textContent = cleanedContent;
                        } else {
                          contentDiv.textContent = cleanedContent;
                        }
                      }
                    }
                    clearImages();
                  },
                  onError: (retryError) => {
                    if (contentDiv) {
                      contentDiv.innerHTML = `<span style="color: #c62828;">Error (after RAG retry): ${retryError.message}</span>`;
                    }
                  },
                },
                retryConfigOverride,
              );
            }
            // RAG retry succeeded
            return;
          }
        } catch (retryErr) {
          Zotero.debug(`[seerai] RAG retry also failed: ${retryErr}`);
        }
      }

      // ── Strategy D: When RAG was already active but still exceeded limits ──
      // Retry with stricter parameters: halve topK, increase score threshold,
      // and force passthrough through RAG (Tier 3).
      if (
        isContextLengthError &&
        ragActive &&
        embeddingService.isConfigured()
      ) {
        Zotero.debug(
          `[seerai] Strategy D: RAG was active but still hit context limit. ` +
            `Retrying with stricter parameters...`,
        );
        try {
          const retryContextItems = ChatContextManager.getInstance().getItems();
          const {
            expandedItems: retryExpanded,
            passthroughContext: retryPassthrough,
          } = await Assistant.expandContextItemsForRAG(retryContextItems);

          // Compute a tighter token budget
          const dActiveModel = getActiveModelConfig();
          const dModelCtx = dActiveModel?.contextLength || 128000;
          const dSystemOverhead = 2000;
          const dHistoryTokens = conversationMessages
            .filter(
              (m: ChatMessage) => m.role !== "system" && m.role !== "error",
            )
            .reduce(
              (sum: number, m: ChatMessage) =>
                sum + ChatStateManager.countTokens(m.content),
              0,
            );
          // Use at most 10% of context for history in retry to free more room
          const dHistoryBudget = Math.min(dHistoryTokens, dModelCtx * 0.1);
          const dChatOptions = getChatStateManager().getOptions();
          const dReservedOutput = dChatOptions?.maxTokens || 4096;
          const dTokenBudget = computeTokenBudget(
            dModelCtx,
            dSystemOverhead,
            dHistoryBudget,
            0,
            dReservedOutput,
          );

          const ragResult = await retrieveContext(
            text,
            retryExpanded,
            Assistant.extractContentForRAG,
            {
              topK: Math.max(
                Math.floor((dActiveModel?.ragTopK || ragConfig.topK) / 2),
                5,
              ),
              maxTokens: ragConfig.tokenThreshold || 8000,
              minScore: Math.min(
                ((dActiveModel?.ragMinScore !== undefined
                  ? dActiveModel.ragMinScore / 100
                  : ragConfig.minScore) || 0.3) + 0.1,
                0.8,
              ),
              passthroughContext: retryPassthrough,
              tokenBudget: dTokenBudget,
              adaptiveRetrieval: true,
            },
          );

          if (
            ragResult.context &&
            (ragResult.chunks.length > 0 || retryPassthrough.length > 0)
          ) {
            const scopePref = this.getScopePref();
            const scopeLabel = this.getScopeLabel(scopePref);

            const retryAgenticEnabled = isAgenticModeEnabled();
            const retryChatOptions = getChatStateManager().getOptions();
            const retrySearchOnly = !retryAgenticEnabled && retryChatOptions.webSearchEnabled;
            const retryWillUseTools = retryAgenticEnabled || retrySearchOnly;
            
            let retryAgentInstructions = "";
            if (retryWillUseTools) {
              retryAgentInstructions = `\n\nYou are a research agent with direct access to ${retryAgenticEnabled ? "the user's Zotero library, web search, and a file workspace" : "web search"}. To help the user, call the tools at your disposal. Do not describe what you could do; use the tools to actually do it. When the task is complete, simply provide a text-only response with your final answer.\n\n${retryAgenticEnabled ? buildToolPromptSections() : "Web Search (use the 'web' tool with the 'action' parameter):\n- web({ action: \"search\", query: \"...\" }) — search the web\n- web({ action: \"read\", url: \"...\" }) — read a specific webpage"}`;
            }

            const ragSystemPrompt = `You are a helpful research assistant for Zotero. You help users understand and analyze their academic papers, notes, and research data tables.

Current Library/Folder Scope: ${scopeLabel} ${retryAgenticEnabled ? "(Tools will only find items within this scope)." : ""}

${ragResult.context}

Be concise, accurate, and helpful. When referencing papers, cite them by title or author.
Note: Context was automatically reduced using stricter semantic search due to size constraints.${retryAgentInstructions}`;

            // Budget history for retry
            const dHistoryMsgs = conversationMessages.filter(
              (m: ChatMessage) => m.role !== "system" && m.role !== "error",
            );
            let dHistUsed = 0;
            let dHistStart = dHistoryMsgs.length;
            for (let i = dHistoryMsgs.length - 1; i >= 0; i--) {
              const mTok = ChatStateManager.countTokens(
                dHistoryMsgs[i].content,
              );
              if (dHistUsed + mTok > dHistoryBudget) break;
              dHistUsed += mTok;
              dHistStart = i;
            }
            const dBudgetedHist = dHistoryMsgs.slice(dHistStart);

            const retryMessages: (OpenAIMessage | VisionMessage)[] = [
              { role: "system", content: ragSystemPrompt },
              ...dBudgetedHist.map((m: ChatMessage) => ({
                role: m.role as "user" | "assistant",
                content: m.content,
              })),
            ];

            // Pre-flight check for Strategy D
            const dTotalTokens = retryMessages.reduce((sum, m) => {
              const c =
                typeof m.content === "string"
                  ? m.content
                  : JSON.stringify(m.content);
              return sum + ChatStateManager.countTokens(c);
            }, 0);

            if (
              dTotalTokens + dReservedOutput + Math.ceil(dModelCtx * 0.1) >
              dModelCtx
            ) {
              Zotero.debug(
                `[seerai] Strategy D pre-flight failed: ${dTotalTokens} + ${dReservedOutput} > ${dModelCtx}. ` +
                  `Giving up — context cannot be reduced enough.`,
              );
              if (contentDiv) {
                contentDiv.querySelector(".typing-indicator")?.remove();
                contentDiv.innerHTML =
                  `<span style="color: #c62828;">` +
                  `Context is too large even after aggressive RAG reduction ` +
                  `(~${dTotalTokens.toLocaleString()} tokens). ` +
                  `Try removing some context items or increasing the Context Window in model settings.` +
                  `</span>`;
              }
              this.isStreaming = false;
              return;
            }

            const retryActiveModel = getActiveModelConfig();
            const retryConfigOverride = retryActiveModel
              ? {
                  apiURL: retryActiveModel.apiURL,
                  apiKey: retryActiveModel.apiKey,
                  model: retryActiveModel.model,
                  temperature: retryChatOptions.temperature,
                  max_tokens: retryChatOptions.maxTokens,
                }
              : undefined;

            Zotero.debug(
              `[seerai] Strategy D retry: ${ragResult.stats.chunksRetrieved} chunks, ` +
                `${ragResult.stats.tokensUsed} tokens, total=${dTotalTokens}`,
            );

            if (retryWillUseTools) {
              const continuation = await getMessageStore().getContinuation();
              await handleAgenticChat(
                text,
                ragSystemPrompt,
                dBudgetedHist.slice(0, -1),
                {
                  enableTools: true,
                  allowedTools: retrySearchOnly ? ["web"] : undefined,
                  includeImages: false,
                  pastedImages: [],
                  permissionHandler: Assistant.handleInlinePermissionRequest,
                  temperature: retryChatOptions.temperature,
                  maxTokens: retryChatOptions.maxTokens,
                  continuation,
                },
                observer,
              );
            } else {
              let retryFullResponse = "";
              let retryIsFirstToken = true;
              await openAIService.chatCompletionStream(
                retryMessages,
                {
                  onToken: (token) => {
                    if (!this.isStreaming) return;
                    retryFullResponse += token;
                    if (contentDiv) {
                      const { displayContent } =
                        processStreamingContent(retryFullResponse);
                      if (retryIsFirstToken && displayContent) {
                        contentDiv.querySelector(".typing-indicator")?.remove();
                        retryIsFirstToken = false;
                      }
                      if (displayContent) {
                        let mdContainer = contentDiv.querySelector(
                          ".markdown-content",
                        ) as HTMLElement;
                        if (!mdContainer) {
                          mdContainer =
                            contentDiv.ownerDocument!.createElement("div");
                          mdContainer.className = "markdown-content";
                          contentDiv.appendChild(mdContainer);
                        }
                        this.smartRender(mdContainer, displayContent);
                        messagesArea.scrollTop = messagesArea.scrollHeight;
                      }
                    }
                  },
                  onComplete: async (content) => {
                    const cleanedContent = stripThinkTags(content);
                    const assistantMsg: ChatMessage = {
                      id: Date.now().toString(),
                      role: "assistant",
                      content: cleanedContent,
                      timestamp: new Date(),
                    };
                    conversationMessages.push(assistantMsg);
                    try {
                      await getMessageStore().appendMessage(assistantMsg);
                    } catch (e) {
                      Zotero.debug(
                        `[seerai] Error saving Strategy D assistant message: ${e}`,
                      );
                    }
                    if (contentDiv) {
                      contentDiv.setAttribute("data-raw", cleanedContent);
                      try {
                        let mdC = contentDiv.querySelector(
                          ".markdown-content",
                        ) as HTMLElement;
                        if (!mdC) {
                          mdC = contentDiv.ownerDocument!.createElement("div");
                          mdC.className = "markdown-content";
                          contentDiv.appendChild(mdC);
                        }
                        mdC.innerHTML = parseMarkdown(cleanedContent);
                      } catch (e) {
                        const mdF =
                          contentDiv.querySelector(".markdown-content");
                        if (mdF) {
                          mdF.textContent = cleanedContent;
                        } else {
                          contentDiv.textContent = cleanedContent;
                        }
                      }
                    }
                    clearImages();
                  },
                  onError: (retryError) => {
                    if (contentDiv) {
                      contentDiv.innerHTML = `<span style="color: #c62828;">Error (after Strategy D retry): ${retryError.message}</span>`;
                    }
                  },
                },
                retryConfigOverride,
              );
            }
            // Strategy D succeeded
            return;
          }
        } catch (strategyDErr) {
          Zotero.debug(`[seerai] Strategy D also failed: ${strategyDErr}`);
        }
      }

      const errMsg =
        error instanceof Error && error.message === "Request was cancelled"
          ? "Generation stopped"
          : String(error);
      if (contentDiv) {
        const isError =
          error instanceof Error && error.message !== "Request was cancelled";
        const indicator = contentDiv.ownerDocument!.createElement("div");
        indicator.style.cssText =
          "margin-top:8px;padding:4px 8px;border-radius:4px;font-size:12px;" +
          (isError
            ? "color:#c62828;background:rgba(198,40,40,0.08);"
            : "color:var(--text-secondary);background:var(--background-secondary);");
        indicator.textContent = errMsg;
        contentDiv.appendChild(indicator);
      }
    } finally {
      input.disabled = false;
      input.focus();
      this.isStreaming = false;
      activeAgentSession = null; // Ensure session is cleared
      if (stopBtn) stopBtn.style.display = "none";
    }
  }

  /**
   * Append a message bubble to the chat area with action buttons
   * @param msgId The message ID for reference
   * @param isLastUserMsg Whether this is the last user message (for edit button)
   */
  private static appendMessage(
    container: HTMLElement,
    sender: string,
    text: string,
    msgId?: string,
    isLastUserMsg?: boolean,
    toolResults?: { toolCall: ToolCall; result?: ToolResult }[],
    iterationCount?: number,
  ): HTMLElement {
    const doc = container.ownerDocument!;
    const isUser = sender === "You";
    const isAssistant = sender === "Assistant";

    const msgDiv = ztoolkit.UI.createElement(doc, "div", {
      properties: {
        className: `message-bubble ${isUser ? "message-user" : "message-assistant"}`,
      },
      attributes: { "data-msg-id": msgId || "" },
      styles: {
        padding: "12px 16px",
        borderRadius: isUser ? "12px 12px 4px 12px" : "12px 12px 12px 4px",
        fontSize: "13px",
        maxWidth: isUser ? "90%" : "100%",
        minWidth: "0",
        flexShrink: "0",
        alignSelf: isUser ? "flex-end" : "flex-start",
        boxShadow: "0 1px 3px rgba(0,0,0,0.12)",
        position: "relative",
        boxSizing: "border-box",
        backgroundColor: isUser
          ? "var(--accent-blue, #1976d2)"
          : "var(--background-secondary, #f5f5f5)",
        color: isUser ? "#ffffff" : "var(--text-primary, #212121)",
        border: isUser ? "none" : "1px solid var(--border-primary, #e0e0e0)",
        userSelect: "text",
        cursor: "text",
      },
    });

    // Header with sender and actions
    const headerDiv = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        marginBottom: "4px",
      },
    });

    const senderDiv = ztoolkit.UI.createElement(doc, "span", {
      styles: { fontWeight: "600", fontSize: "11px", opacity: "0.8" },
      properties: { innerText: sender },
    });

    // Action buttons container
    const actionsDiv = ztoolkit.UI.createElement(doc, "div", {
      styles: {
        display: "flex",
        gap: "4px",
        opacity: "0.6",
      },
    });

    // Copy button (for all messages) with Smart Copy logic
    let clickCount = 0;
    let clickTimer: ReturnType<typeof setTimeout> | null = null;

    const copyBtn = this.createActionButton(
      doc,
      "📋",
      "Click: Copy Text\nDouble-Click: Copy + Logs",
      () => {
        // Get the current text from data-raw attribute (for streaming messages)
        // or from the markdown container's data-raw, or fallback to initial text
        const msgBubble = (copyBtn as HTMLElement).closest(".message-bubble");
        const contentEl = msgBubble?.querySelector("[data-content]");
        const mdContainer = contentEl?.querySelector(".markdown-content");
        const currentText =
          mdContainer?.getAttribute("data-raw") ||
          contentEl?.getAttribute("data-raw") ||
          text;

        clickCount++;
        if (clickTimer) clearTimeout(clickTimer);

        clickTimer = setTimeout(() => {
          const isMultiClick = clickCount >= 2;
          clickCount = 0;
          clickTimer = null;

          let copyText = currentText;
          if (isMultiClick && toolResults && toolResults.length > 0) {
            const toolLogs = toolResults
              .map((tr) => {
                const args = tr.toolCall.function.arguments;
                const result = tr.result
                  ? JSON.stringify(tr.result, null, 2)
                  : "No result";
                return `[Tool: ${tr.toolCall.function.name}]\nInput: ${args}\nOutput: ${result}`;
              })
              .join("\n\n");
            copyText = `${currentText}\n\n--- Tool Executions ---\n${toolLogs}`;
          }
          this.copyToClipboard(copyText, copyBtn as HTMLElement);
        }, 300);
      },
    );

    actionsDiv.appendChild(copyBtn);

    // TTS play button (when TTS is configured)
    if (isTtsConfigured()) {
      const ttsBtn = this.createActionButton(
        doc,
        "\u{1F50A}",
        "Play TTS",
        () => {
          const msgBubble = (ttsBtn as HTMLElement).closest(".message-bubble");
          const contentEl = msgBubble?.querySelector("[data-content]");
          const mdContainer = contentEl?.querySelector(".markdown-content");
          const currentText =
            mdContainer?.getAttribute("data-raw") ||
            contentEl?.getAttribute("data-raw") ||
            text;
          playTts(currentText, ttsBtn as HTMLElement);
        },
      );
      actionsDiv.appendChild(ttsBtn);
    }

    // Edit button (only for last user message)
    if (isUser && isLastUserMsg) {
      const editBtn = this.createActionButton(doc, "✏️", "Edit", () => {
        this.handleEditMessage(container, msgDiv as HTMLElement, msgId || "");
      });
      actionsDiv.appendChild(editBtn);
    }

    // Retry button (only for assistant messages)
    if (isAssistant) {
      const retryBtn = this.createActionButton(doc, "🔄", "Retry", () => {
        const outerPanel = (msgDiv as HTMLElement).closest(
          "#assistant-messages-area",
        )?.parentElement as HTMLElement | undefined;
        if (!outerPanel) return;

        // Remove this assistant from history so regeneration starts clean
        if (msgId) {
          const idx = conversationMessages.findIndex((m) => m.id === msgId);
          if (idx !== -1) {
            conversationMessages = conversationMessages.slice(0, idx);
          }
        }

        // Remove the DOM bubble and regenerate
        (msgDiv as HTMLElement).remove();
        this.regenerateLastResponse(outerPanel);
      });
      actionsDiv.appendChild(retryBtn);
    }

    headerDiv.appendChild(senderDiv);
    headerDiv.appendChild(actionsDiv);

    const contentDiv = ztoolkit.UI.createElement(doc, "div", {
      attributes: { "data-content": "true", "data-raw": text },
      styles: {
        lineHeight: "1.6",
        overflowX: "auto",
        overflowWrap: "break-word",
        wordBreak: "break-word",
        maxWidth: "100%",
        minWidth: "0",
      },
    });

    // Standard markdown container structure (used by streaming logic)
    const mdContainer = doc.createElement("div");
    mdContainer.className = "markdown-content";
    mdContainer.setAttribute("data-raw", text);
    mdContainer.innerHTML = parseMarkdown(text);
    contentDiv.appendChild(mdContainer);
    wireCodePreviewButtons(contentDiv);

    // Bind copy button events for code blocks
    const copyBtns = mdContainer.querySelectorAll(".code-copy-btn");
    copyBtns.forEach((btn: Element) => {
      btn.addEventListener("click", () => {
        const codeId = btn.getAttribute("data-code-id");
        const codeEl = contentDiv.querySelector(`#${codeId}`);
        if (codeEl) {
          const codeText = codeEl.textContent || "";
          this.copyToClipboard(codeText, btn as HTMLElement);
        }
      });
    });

    msgDiv.appendChild(headerDiv);
    msgDiv.appendChild(contentDiv);

    // Render persisted tool executions
    if (toolResults && toolResults.length > 0) {
      const { container, setCompleted } = createToolProcessUI(doc);
      container.style.marginTop = "8px";

      const listContainer = container.querySelector(".tool-list-container");
      const targetContainer = listContainer || container;

      let hasQuestions = false;
      const questionPanels: HTMLElement[] = [];
      toolResults.forEach((tr) => {
        const toolUI = createToolExecutionUI(doc, tr.toolCall, tr.result);
        targetContainer.appendChild(toolUI);

        if (
          tr.toolCall.function.name === "workspace_question" &&
          tr.result?.success &&
          (tr.result.data as any).questions &&
          Array.isArray((tr.result.data as any).questions)
        ) {
          hasQuestions = true;
          const questions = (tr.result.data as any).questions as any[];
          const panel = createQuestionPanel(doc, questions);
          questionPanels.push(panel);
        }
      });

      // Set state to completed
      const finalCount = iterationCount !== undefined ? iterationCount : 0;
      const toolCount = toolResults ? toolResults.length : 0;
      setCompleted(finalCount, toolCount);

      if (hasQuestions) {
        questionPanels.forEach((panel) => {
          msgDiv.appendChild(panel);
        });
      }

      msgDiv.appendChild(container);
    }

    container.appendChild(msgDiv);
    container.scrollTop = container.scrollHeight;

    return msgDiv;
  }

  /**
   * Create an action button with tooltip
   */
  private static createActionButton(
    doc: Document,
    icon: string,
    tooltip: string,
    onClick: () => void,
  ): HTMLElement {
    const btn = ztoolkit.UI.createElement(doc, "span", {
      properties: { innerText: icon, title: tooltip },
      styles: {
        cursor: "pointer",
        fontSize: "12px",
        padding: "2px 4px",
        borderRadius: "4px",
        transition: "background-color 0.1s",
      },
      listeners: [
        {
          type: "click",
          listener: (e: Event) => {
            e.stopPropagation();
            onClick();
          },
        },
        {
          type: "mouseenter",
          listener: () => {
            (btn as HTMLElement).style.backgroundColor = "rgba(0,0,0,0.1)";
            (btn as HTMLElement).style.opacity = "1";
          },
        },
        {
          type: "mouseleave",
          listener: () => {
            (btn as HTMLElement).style.backgroundColor = "transparent";
            (btn as HTMLElement).style.opacity = "0.6";
          },
        },
      ],
    });
    return btn;
  }

  /**
   * Copy text to clipboard and show feedback
   * Uses ztoolkit.Clipboard for Zotero compatibility
   */
  private static copyToClipboard(text: string, buttonElement: HTMLElement) {
    const originalText = buttonElement.innerText;

    try {
      // Use ztoolkit.Clipboard which works in Zotero's environment
      new ztoolkit.Clipboard().addText(text, "text/unicode").copy();

      // Visual feedback - success
      buttonElement.innerText = "✓";
      setTimeout(() => {
        buttonElement.innerText = originalText;
      }, 1500);

      Zotero.debug("[seerai] Copied message to clipboard");
    } catch (e) {
      Zotero.debug(`[seerai] ztoolkit.Clipboard failed: ${e}`);
      // Visual feedback - failure
      buttonElement.innerText = "❌";
      setTimeout(() => {
        buttonElement.innerText = originalText;
      }, 1500);
    }
  }

  /**
   * Handle edit message action
   */
  private static handleEditMessage(
    container: HTMLElement,
    msgDiv: HTMLElement,
    msgId: string,
  ) {
    const contentDiv = msgDiv.querySelector("[data-content]") as HTMLElement;
    if (!contentDiv) return;

    const originalText = contentDiv.innerText;
    const doc = container.ownerDocument!;

    // Replace content with input
    const inputContainer = ztoolkit.UI.createElement(doc, "div", {
      styles: { display: "flex", gap: "4px", marginTop: "4px" },
    });

    const input = ztoolkit.UI.createElement(doc, "input", {
      attributes: { type: "text", value: originalText },
      styles: {
        flex: "1",
        padding: "6px 10px",
        border: "1px solid #1976d2",
        borderRadius: "4px",
        fontSize: "13px",
      },
    }) as HTMLInputElement;

    const saveBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Send" },
      styles: {
        padding: "6px 12px",
        backgroundColor: "#1976d2",
        color: "#fff",
        border: "none",
        borderRadius: "4px",
        cursor: "pointer",
        fontSize: "12px",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            const newText = input.value.trim();
            if (newText && newText !== originalText) {
              this.submitEditedMessage(container, msgId, newText);
            } else {
              // Restore original
              contentDiv.innerText = originalText;
              inputContainer.remove();
              contentDiv.style.display = "block";
            }
          },
        },
      ],
    });

    const cancelBtn = ztoolkit.UI.createElement(doc, "button", {
      namespace: "html",
      properties: { innerText: "Cancel" },
      styles: {
        padding: "6px 12px",
        backgroundColor: "#f5f5f5",
        color: "#666",
        border: "1px solid #ddd",
        borderRadius: "4px",
        cursor: "pointer",
        fontSize: "12px",
      },
      listeners: [
        {
          type: "click",
          listener: () => {
            contentDiv.style.display = "block";
            inputContainer.remove();
          },
        },
      ],
    });

    input.addEventListener("keypress", (e: KeyboardEvent) => {
      if (e.key === "Enter") {
        saveBtn.click();
      } else if (e.key === "Escape") {
        cancelBtn.click();
      }
    });

    inputContainer.appendChild(input);
    inputContainer.appendChild(saveBtn);
    inputContainer.appendChild(cancelBtn);

    contentDiv.style.display = "none";
    msgDiv.appendChild(inputContainer);
    input.focus();
    input.select();
  }

  /**
   * Submit edited message and regenerate response
   */
  private static submitEditedMessage(
    container: HTMLElement,
    msgId: string,
    newText: string,
  ) {
    // Find the message index
    const msgIndex = conversationMessages.findIndex((m) => m.id === msgId);
    if (msgIndex === -1) return;

    // Update the message
    conversationMessages[msgIndex].content = newText;

    // Remove all messages after this one
    conversationMessages = conversationMessages.slice(0, msgIndex + 1);

    // Re-render chat and regenerate
    this.rerenderChat(container);
    this.regenerateLastResponse(container);
  }

  /**
   * Re-render the chat area with current messages
   */
  private static rerenderChat(container: HTMLElement) {
    const messagesArea = container.querySelector(
      "#assistant-messages-area",
    ) as HTMLElement;
    if (!messagesArea) return;

    messagesArea.replaceChildren();

    const lastUserMsgIndex = conversationMessages
      .map((m) => m.role)
      .lastIndexOf("user");

    conversationMessages.forEach((msg, idx) => {
      const isUser = msg.role === "user";
      const sender = isUser
        ? "You"
        : msg.role === "error"
          ? "Error"
          : "Assistant";
      const isLastUserMsg = isUser && idx === lastUserMsgIndex;

      this.appendMessage(
        messagesArea,
        sender,
        msg.content,
        msg.id,
        isLastUserMsg,
        msg.toolResults, // Pass toolResults so they're re-rendered on tab switch
        msg.iterationCount,
      );
    });
  }

  /**
   * Regenerate the last response based on the last user message
   */
  private static async regenerateLastResponse(container: HTMLElement) {
    const messagesArea = container.querySelector(
      "#assistant-messages-area",
    ) as HTMLElement;
    if (!messagesArea) return;

    const stateManager = getChatStateManager();
    const input = container.querySelector(
      "input[type='text']",
    ) as HTMLInputElement;

    if (input) input.disabled = true;
    this.isStreaming = true;

    const stopBtn = container.ownerDocument?.getElementById(
      "stop-btn",
    ) as HTMLElement;
    if (stopBtn) stopBtn.style.display = "inline-block";

    // Create streaming message placeholder
    const streamingDiv = this.appendMessage(
      messagesArea,
      "Assistant",
      "",
      undefined,
      false,
      undefined,
      1,
    );
    const contentDiv = streamingDiv.querySelector(
      "[data-content]",
    ) as HTMLElement;

    try {
      const states = stateManager.getStates();
      let context = "=== Selected Papers ===\n";

      for (const item of states.items) {
        context += `\n--- ${item.title} ---`;
        if (item.year) context += ` (${item.year})`;
        if (item.creators && item.creators.length > 0) {
          context += `\nAuthors: ${item.creators.join(", ")}`;
        }
        if (item.abstract) {
          context += `\nAbstract: ${item.abstract}`;
        }

        // Fetch PDF/note content for this item
        try {
          const zoteroItem = Zotero.Items.get(item.id);
          if (zoteroItem && zoteroItem.isRegularItem()) {
            const itemContent = await Assistant.getPdfTextForItem(
              zoteroItem,
              0,
              true,
              true,
            );
            if (itemContent) {
              context += `\n\nContent:\n${itemContent}`;
            }
          }
        } catch (e) {
          Zotero.debug(
            `[seerai] Error fetching content for item ${item.id}: ${e}`,
          );
        }
      }

      if (states.notes.length > 0) {
        context += "\n\n=== Notes ===";
        for (const note of states.notes) {
          context += `\n\n--- ${note.title} ---\n${note.content}`;
        }
      }

      // Include table context
      if (states.tables.length > 0) {
        context += "\n\n=== Table Data ===";
        for (const table of states.tables) {
          context += `\n\n--- Table: ${table.title} (${table.rowCount} rows) ---`;
          context += `\nColumns: ${table.columnNames.join(", ")}`;
          context += `\n${table.content}`;
        }
      }

      const systemPrompt = `You are a helpful research assistant for Zotero. You help users understand and analyze their academic papers, notes, and research data tables.

${context}

Be concise, accurate, and helpful. When referencing papers, cite them by title or author. When referencing table data, cite the table name and relevant columns.`;

      const messages: OpenAIMessage[] = [
        { role: "system", content: systemPrompt },
        ...conversationMessages
          .filter((m) => m.role !== "system" && m.role !== "error")
          .map((m) => ({
            role: m.role as "user" | "assistant",
            content: m.content,
          })),
      ];

      let fullResponse = "";

      // Get active model config for API call
      const activeModel = getActiveModelConfig();
      const configOverride = activeModel
        ? {
            apiURL: activeModel.apiURL,
            apiKey: activeModel.apiKey,
            model: activeModel.model,
          }
        : undefined;

      await openAIService.chatCompletionStream(
        messages,
        {
          onToken: (token) => {
            fullResponse += token;
            if (contentDiv) {
              // Process streaming content to handle think tags
              const { displayContent } = processStreamingContent(fullResponse);

              if (displayContent) {
                contentDiv.setAttribute("data-raw", displayContent);
                contentDiv.innerHTML = parseMarkdown(displayContent);
                messagesArea.scrollTop = messagesArea.scrollHeight;
              }
            }
          },
          onComplete: (content) => {
            // Strip think tags from the final content
            const cleanedContent = stripThinkTags(content);
            const assistantMsg: ChatMessage = {
              id: Date.now().toString(),
              role: "assistant",
              content: cleanedContent,
              timestamp: new Date(),
            };
            conversationMessages.push(assistantMsg);
            // Final render with markdown
            if (contentDiv) {
              contentDiv.setAttribute("data-raw", cleanedContent);
              contentDiv.innerHTML = parseMarkdown(cleanedContent);
            }
          },
          onError: (error) => {
            if (contentDiv) {
              contentDiv.innerHTML = `<span style="color: #c62828;">Error: ${error.message}</span>`;
            }
          },
        },
        configOverride,
      );
    } catch (error) {
      const errMsg =
        error instanceof Error && error.message === "Request was cancelled"
          ? "Generation stopped"
          : String(error);
      if (contentDiv) {
        const isError =
          error instanceof Error && error.message !== "Request was cancelled";
        const indicator = contentDiv.ownerDocument!.createElement("div");
        indicator.style.cssText =
          "margin-top:8px;padding:4px 8px;border-radius:4px;font-size:12px;" +
          (isError
            ? "color:#c62828;background:rgba(198,40,40,0.08);"
            : "color:var(--text-secondary);background:var(--background-secondary);");
        indicator.textContent = errMsg;
        contentDiv.appendChild(indicator);
      }
    } finally {
      if (input) input.disabled = false;
      if (input) input.focus();
      this.isStreaming = false;
      if (stopBtn) stopBtn.style.display = "none";
    }
  }

  /**
   * Render a stored message (for restoring conversation)
   */
  private static renderStoredMessage(
    container: HTMLElement,
    msg: ChatMessage,
    isLastUserMsg: boolean = false,
  ) {
    const isUser = msg.role === "user";
    const sender = isUser
      ? "You"
      : msg.role === "error"
        ? "Error"
        : "Assistant";
    this.appendMessage(
      container,
      sender,
      msg.content,
      msg.id,
      isLastUserMsg,
      msg.toolResults,
      msg.iterationCount,
    );
  }

  /**
   * Save current conversation as a Zotero note
   */
  private static async saveConversationAsNote() {
    if (conversationMessages.length === 0) {
      Zotero.debug("[seerai] No messages to save");
      return;
    }

    const stateManager = getChatStateManager();
    const states = stateManager.getStates();

    const parentItem = states.items[0];
    if (!parentItem) {
      Zotero.debug("[seerai] No items to attach note to");
      return;
    }

    let noteContent = `<h2>AI Chat Conversation</h2>`;
    noteContent += `<p><em>Saved: ${new Date().toLocaleString()}</em></p>`;
    noteContent += `<p><strong>Context:</strong> ${stateManager.getSummary()}</p><hr/>`;

    for (const msg of conversationMessages) {
      const role = msg.role === "user" ? "🧑 You" : "🤖 Assistant";
      noteContent += `<p><strong>${role}:</strong></p>`;
      noteContent += `<p>${msg.content.replace(/\n/g, "<br/>")}</p>`;
      noteContent += `<hr/>`;
    }

    try {
      const zoteroItem = Zotero.Items.get(parentItem.id);
      const note = new Zotero.Item("note");
      note.setNote(noteContent);
      note.parentID = zoteroItem.id;
      await note.saveTx();
      Zotero.debug("[seerai] Conversation saved as note");
    } catch (error) {
      Zotero.debug(`[seerai] Failed to save note: ${error}`);
    }
  }

  /**
   * Check if an item is in the current table
   */
  static isItemInCurrentTable(itemId: number): boolean {
    return currentTableConfig?.addedPaperIds.includes(itemId) || false;
  }

  /**
   * Add items to the currently active table (if any)
   */
  static async addItemsToCurrentTable(items: Zotero.Item[]): Promise<void> {
    const tableStore = getTableStore();
    currentTableConfig = await tableStore.loadConfig();

    if (!currentTableConfig) {
      new ztoolkit.ProgressWindow("SeerAI")
        .createLine({
          text: "No active table found. Please open the Assistant Table tab first.",
          progress: 100,
          icon: "warning",
        })
        .show();
      return;
    }

    const newIds = items
      .filter((item) => item.isRegularItem())
      .map((item) => item.id)
      .filter((id) => !currentTableConfig!.addedPaperIds.includes(id));

    if (newIds.length === 0) {
      const totalItems = currentTableConfig!.addedPaperIds.length;
      const pageSize = currentTableConfig!.pageSize || 25;
      const totalPages = Math.ceil(totalItems / pageSize);
      Zotero.debug(
        `[seerai] addItemsToCurrentTable: All ${items.length} selected already in table (${totalItems} total, ${totalPages} page(s))`,
      );
      new ztoolkit.ProgressWindow("SeerAI")
        .createLine({
          text:
            totalItems > pageSize
              ? `Selected items are already in the table. Navigate to page ${totalPages} or check filtering.`
              : "Selected items are already in the table.",
          progress: 100,
        })
        .show();
      return;
    }

    currentTableConfig.addedPaperIds.push(...newIds);
    await tableStore.saveConfig(currentTableConfig);

    // Force immediate refresh if table tab is visible
    if (activeTab === "table") {
      await this.forceRefreshTable();
    }

    new ztoolkit.ProgressWindow("SeerAI")
      .createLine({
        text: `Added ${newIds.length} items to table`,
        progress: 100,
      })
      .show();
  }

  /**
   * Remove items from the currently active table (if present)
   */
  static async removeItemsFromCurrentTable(
    items: Zotero.Item[],
  ): Promise<void> {
    const tableStore = getTableStore();
    currentTableConfig = await tableStore.loadConfig();

    if (!currentTableConfig) return;

    const idsToRemove = items.map((item) => item.id);
    const initialLength = currentTableConfig.addedPaperIds.length;

    currentTableConfig.addedPaperIds = currentTableConfig.addedPaperIds.filter(
      (id) => !idsToRemove.includes(id),
    );

    if (currentTableConfig.addedPaperIds.length === initialLength) {
      new ztoolkit.ProgressWindow("SeerAI")
        .createLine({
          text: "Selected items are not in the table.",
          progress: 100,
        })
        .show();
      return;
    }

    // Cleanup generated data
    idsToRemove.forEach((id) => {
      if (currentTableConfig!.generatedData?.[id]) {
        delete currentTableConfig!.generatedData![id];
      }
    });

    await tableStore.saveConfig(currentTableConfig);

    // Force immediate refresh if table tab is visible
    if (activeTab === "table") {
      await this.forceRefreshTable();
    }

    new ztoolkit.ProgressWindow("SeerAI")
      .createLine({
        text: `Removed items from table`,
        progress: 100,
      })
      .show();
  }
}
