import {
  BookOpen,
  Command as CommandIcon,
  ArrowUp,
  Key,
  Microphone as Mic,
  Stop,
  X,
} from "@phosphor-icons/react";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { shouldIgnoreFile } from "@/features/quick-open/utils/file-filtering";
import { classifySessionConfigOption } from "@/features/ai/lib/session-config-option-classifier";
import { AI_CHAT_INSERT_SKILL_EVENT } from "@/features/ai/lib/skill-events";
import { useAIChatStore } from "@/features/ai/store/store";
import type { InlineDropdownPosition } from "@/features/ai/store/types";
import type { AIChatSkill } from "@/features/ai/types/skills";
import type { SlashCommand } from "@/features/ai/types/acp";
import type { AIChatInputBarProps } from "@/features/ai/types/ai-chat";
import type { FileEntry } from "@/features/file-system/types/app";
import { getProviderById } from "@/features/ai/types/providers";
import { openSidebarResourceBuffer } from "@/features/sidebar-drag/open-sidebar-resource";
import {
  hasSidebarResourceDragData,
  readSidebarResourceDragData,
  SIDEBAR_RESOURCE_DROP_ON_AI_EVENT,
  type SidebarDragResource,
} from "@/features/sidebar-drag/sidebar-resource-drag";
import { useSettingsStore } from "@/features/settings/store";
import Badge from "@/ui/badge";
import { Button } from "@/ui/button";
import { SidebarComposerBody, SidebarFooter } from "@/ui/sidebar";
import { toast } from "@/ui/toast";
import { cn } from "@/utils/cn";
import { IS_LINUX, isMac } from "@/utils/platform";
import { FileMentionDropdown } from "../mentions/file-mention-dropdown";
import { SlashCommandDropdown } from "../mentions/slash-command-dropdown";
import { AcpConfigSelector } from "../selectors/acp-config-selector";
import { ModelSelector } from "../selectors/model-selector";
import { ProviderSelector } from "../selectors/provider-selector";
import { ModeSelector } from "../selectors/mode-selector";
import { ContextSelector } from "../selectors/context-selector";
import { ProviderApiKeyCommand } from "../provider-api-key-command";
import { SkillsCommand } from "../skills/skills-command";
import { ChatLoadingIndicator } from "../chat/chat-loading-indicator";
import { chatComposerIconButtonClassName } from "./chat-composer-control-styles";

function isComposerTokenElement(node: Node | null): node is Element {
  return (
    node?.nodeType === Node.ELEMENT_NODE &&
    ((node as Element).hasAttribute("data-mention") ||
      (node as Element).hasAttribute("data-slash-command"))
  );
}

function getMicrophoneAccessErrorMessage(error?: string): string {
  if (IS_LINUX) {
    if (error === "service-not-allowed") {
      return "Voice input is unavailable in this Linux webview. Chromium speech recognition may be blocked even when the microphone works.";
    }

    return "Microphone access failed. Check your PipeWire/PulseAudio input device and unmute the default microphone.";
  }

  return "Microphone access failed. Check System Settings -> Privacy & Security -> Microphone.";
}

const AIChatInputBar = memo(function AIChatInputBar({
  buffers,
  allProjectFiles,
  isActiveSurface = true,
  onSendMessage,
  onStopStreaming,
}: AIChatInputBarProps) {
  const inputRef = useRef<HTMLDivElement>(null);
  const contextDropdownRef = useRef<HTMLDivElement>(null);
  const aiChatContainerRef = useRef<HTMLDivElement>(null);
  const isUpdatingContentRef = useRef(false);
  const visibleMentionFilesRef = useRef<FileEntry[]>([]);
  const performanceTimer = useRef<number | null>(null);
  const speechRecognitionRef = useRef<SpeechRecognition | null>(null);
  const shouldKeepListeningRef = useRef(false);

  // Local state for input emptiness check (to avoid subscribing to full input text)
  const [hasInputText, setHasInputText] = useState(false);
  const [isListening, setIsListening] = useState(false);
  const [interimTranscript, setInterimTranscript] = useState("");
  const [isContextDragOver, setIsContextDragOver] = useState(false);
  const [activeInlineControl, setActiveInlineControl] = useState<string | null>(null);
  const [isSkillsOpen, setIsSkillsOpen] = useState(false);
  const [isApiKeyManagerOpen, setIsApiKeyManagerOpen] = useState(false);
  const [isComposerFocused, setIsComposerFocused] = useState(false);
  const slashCommandRangeRef = useRef({ startIndex: 0, endIndex: 0 });

  // Get state from store - DO NOT subscribe to 'input' to avoid re-renders on every keystroke
  const isTyping = useAIChatStore((state) => state.isTyping);
  const streamingMessageId = useAIChatStore((state) => state.streamingMessageId);
  const selectedBufferIds = useAIChatStore((state) => state.selectedBufferIds);
  const selectedFilesPaths = useAIChatStore((state) => state.selectedFilesPaths);
  const isContextDropdownOpen = useAIChatStore((state) => state.isContextDropdownOpen);
  const queueCount = useAIChatStore((state) => state.messageQueue.length);
  const hasApiKey = useAIChatStore((state) => state.hasApiKey);
  const mentionState = useAIChatStore((state) => state.mentionState);
  const getCurrentAgentId = useAIChatStore((state) => state.getCurrentAgentId);
  const sessionConfigOptions = useAIChatStore((state) => state.sessionConfigOptions);
  const sessionModeState = useAIChatStore((state) => state.sessionModeState);
  const acpStatus = useAIChatStore((state) => state.acpStatus);
  const aiProviderId = useSettingsStore((state) => state.settings.aiProviderId);
  const aiModelId = useSettingsStore((state) => state.settings.aiModelId);
  const aiCustomModelId = useSettingsStore((state) => state.settings.aiCustomModelId);
  const aiAutocompleteCustomModelId = useSettingsStore(
    (state) => state.settings.aiAutocompleteCustomModelId,
  );
  const updateSetting = useSettingsStore((state) => state.updateSetting);

  // Check if current agent is "custom" (only show model selector for custom agent)
  const currentAgentId = getCurrentAgentId();
  const isCustomAgent = currentAgentId === "custom";

  // ACP agents don't need API key (they handle their own auth)
  const isInputEnabled = isCustomAgent ? hasApiKey : true;
  const isStreaming = isTyping && !!streamingMessageId;
  const acpInlineConfigOptions = useMemo(
    () =>
      isCustomAgent
        ? []
        : sessionConfigOptions
            .map((option) => ({
              option,
              category: classifySessionConfigOption(option),
            }))
            .filter(
              ({ option, category }) =>
                option.kind.type === "select" &&
                option.kind.options.length > 0 &&
                (category === "model" || category === "mode" || category === "thought_level"),
            )
            .slice(0, 3),
    [isCustomAgent, sessionConfigOptions],
  );
  const hasAcpLegacyModeOptions = !isCustomAgent && sessionModeState.availableModes.length > 0;
  const hasAcpConfigModeOption = acpInlineConfigOptions.some(({ category }) => category === "mode");
  const hasAcpConfigOptions = acpInlineConfigOptions.length > 0;
  const isAcpMetadataLoading =
    !isCustomAgent &&
    isTyping &&
    (!acpStatus?.initialized || (!hasAcpLegacyModeOptions && !hasAcpConfigOptions));

  // Memoize action selectors
  const setInput = useAIChatStore((state) => state.setInput);
  const setIsContextDropdownOpen = useAIChatStore((state) => state.setIsContextDropdownOpen);
  const toggleBufferSelection = useAIChatStore((state) => state.toggleBufferSelection);
  const toggleFileSelection = useAIChatStore((state) => state.toggleFileSelection);
  const setSelectedBufferIds = useAIChatStore((state) => state.setSelectedBufferIds);
  const setSelectedFilesPaths = useAIChatStore((state) => state.setSelectedFilesPaths);
  const showMention = useAIChatStore((state) => state.showMention);
  const hideMention = useAIChatStore((state) => state.hideMention);
  const updatePosition = useAIChatStore((state) => state.updatePosition);
  const setSelectedIndex = useAIChatStore((state) => state.setSelectedIndex);

  // Slash command state and actions
  const slashCommandState = useAIChatStore((state) => state.slashCommandState);
  const showSlashCommands = useAIChatStore((state) => state.showSlashCommands);
  const hideSlashCommands = useAIChatStore((state) => state.hideSlashCommands);
  const selectNextSlashCommand = useAIChatStore((state) => state.selectNextSlashCommand);
  const selectPreviousSlashCommand = useAIChatStore((state) => state.selectPreviousSlashCommand);
  const getFilteredSlashCommands = useAIChatStore((state) => state.getFilteredSlashCommands);
  const changeSessionConfigOption = useAIChatStore((state) => state.changeSessionConfigOption);

  const handleAthasProviderChange = useCallback(
    (nextProviderId: string) => {
      const provider = getProviderById(nextProviderId);
      void updateSetting("aiProviderId", nextProviderId);
      if (nextProviderId === "custom") {
        void updateSetting("aiModelId", aiCustomModelId || aiAutocompleteCustomModelId);
        return;
      }
      if (provider && provider.models.length > 0) {
        void updateSetting("aiModelId", provider.models[0].id);
      }
    },
    [aiAutocompleteCustomModelId, aiCustomModelId, updateSetting],
  );

  const handleAthasModelChange = useCallback(
    (nextModelId: string) => {
      if (aiProviderId === "custom") {
        void updateSetting("aiCustomModelId", nextModelId);
      }
      void updateSetting("aiModelId", nextModelId);
    },
    [aiProviderId, updateSetting],
  );

  // Pasted images state and actions
  const pastedImages = useAIChatStore((state) => state.pastedImages);
  const addPastedImage = useAIChatStore((state) => state.addPastedImage);
  const removePastedImage = useAIChatStore((state) => state.removePastedImage);
  const clearPastedImages = useAIChatStore((state) => state.clearPastedImages);

  const closeComposerPopovers = useCallback(() => {
    if (slashCommandState.active) {
      hideSlashCommands();
    }
    if (isContextDropdownOpen) {
      setIsContextDropdownOpen(false);
    }
    if (mentionState.active) {
      hideMention();
    }
  }, [
    slashCommandState.active,
    hideSlashCommands,
    isContextDropdownOpen,
    setIsContextDropdownOpen,
    mentionState.active,
    hideMention,
  ]);

  const closeInlineMenus = useCallback(() => {
    setActiveInlineControl(null);
    closeComposerPopovers();
  }, [closeComposerPopovers]);

  const addBufferToContext = useCallback(
    (bufferId: string) => {
      const currentSelectedBufferIds = useAIChatStore.getState().selectedBufferIds;
      if (currentSelectedBufferIds.has(bufferId)) return;
      const nextSelectedBufferIds = new Set(currentSelectedBufferIds);
      nextSelectedBufferIds.add(bufferId);
      setSelectedBufferIds(nextSelectedBufferIds);
    },
    [setSelectedBufferIds],
  );

  const addPathToContext = useCallback(
    (filePath: string) => {
      const currentSelectedFilesPaths = useAIChatStore.getState().selectedFilesPaths;
      if (currentSelectedFilesPaths.has(filePath)) return;
      const nextSelectedFilesPaths = new Set(currentSelectedFilesPaths);
      nextSelectedFilesPaths.add(filePath);
      setSelectedFilesPaths(nextSelectedFilesPaths);
    },
    [setSelectedFilesPaths],
  );

  const addSidebarResourceToContext = useCallback(
    async (resource: SidebarDragResource) => {
      if (resource.type === "file") {
        const matchingBuffer = !resource.isDir
          ? buffers.find((buffer) => buffer.path === resource.path)
          : null;
        if (matchingBuffer) {
          addBufferToContext(matchingBuffer.id);
        } else {
          addPathToContext(resource.path);
        }
        return;
      }

      if (resource.type === "git-worktree") {
        addPathToContext(resource.path);
        return;
      }

      const bufferId = await openSidebarResourceBuffer(resource);
      if (bufferId) {
        addBufferToContext(bufferId);
      }
    },
    [addBufferToContext, addPathToContext, buffers],
  );

  useEffect(() => {
    const handleSidebarResourceDropOnAI = (event: Event) => {
      const resource = (event as CustomEvent<{ resource?: SidebarDragResource }>).detail?.resource;
      if (!resource) return;
      void addSidebarResourceToContext(resource);
    };

    window.addEventListener(SIDEBAR_RESOURCE_DROP_ON_AI_EVENT, handleSidebarResourceDropOnAI);
    return () =>
      window.removeEventListener(SIDEBAR_RESOURCE_DROP_ON_AI_EVENT, handleSidebarResourceDropOnAI);
  }, [addSidebarResourceToContext]);

  const handleContextDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
    if (!hasSidebarResourceDragData(event.dataTransfer)) return;
    event.preventDefault();
    event.stopPropagation();
    event.dataTransfer.dropEffect = "copy";
    setIsContextDragOver(true);
  }, []);

  const handleContextDragLeave = useCallback((event: React.DragEvent<HTMLDivElement>) => {
    const relatedTarget = event.relatedTarget as HTMLElement | null;
    if (!relatedTarget || !event.currentTarget.contains(relatedTarget)) {
      setIsContextDragOver(false);
    }
  }, []);

  const handleContextDrop = useCallback(
    async (event: React.DragEvent<HTMLDivElement>) => {
      const resource = readSidebarResourceDragData(event.dataTransfer);
      if (!resource) return;

      event.preventDefault();
      event.stopPropagation();
      setIsContextDragOver(false);
      await addSidebarResourceToContext(resource);
    },
    [addSidebarResourceToContext],
  );

  // Computed state for send button
  const hasImages = pastedImages.length > 0;
  const isSendDisabled = isStreaming ? false : (!hasInputText && !hasImages) || !isInputEnabled;
  const SpeechRecognitionCtor = window.SpeechRecognition || window.webkitSpeechRecognition;
  const isMacDevSpeechRecognitionBlocked = import.meta.env.DEV && isMac();
  const isSpeechRecognitionSupported =
    !isMacDevSpeechRecognitionBlocked && typeof SpeechRecognitionCtor !== "undefined";

  // Highly optimized function to get plain text from contentEditable div
  const getPlainTextFromDiv = useCallback(() => {
    if (!inputRef.current) return "";

    const element = inputRef.current;
    const children = element.childNodes;

    // Fast path: check if there are any composer tokens (without Array.from for performance)
    let hasComposerTokens = false;
    for (let i = 0; i < children.length; i++) {
      if (isComposerTokenElement(children[i])) {
        hasComposerTokens = true;
        break;
      }
    }

    // If no tokens, just return textContent (fastest path)
    if (!hasComposerTokens) {
      return element.textContent || "";
    }

    // Handle composer tokens
    let text = "";
    for (let i = 0; i < children.length; i++) {
      const node = children[i];
      if (node.nodeType === Node.TEXT_NODE) {
        text += node.textContent || "";
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        const el = node as Element;
        if (el.hasAttribute("data-mention")) {
          const fileName = el.getAttribute("data-mention-name") || el.textContent?.trim();
          if (fileName) text += `@[${fileName}]`;
        } else if (el.hasAttribute("data-slash-command")) {
          const commandName = el.getAttribute("data-slash-command-name") || el.textContent?.trim();
          if (commandName) text += commandName.startsWith("/") ? commandName : `/${commandName}`;
        } else {
          text += node.textContent || "";
        }
      }
    }

    return text;
  }, []);

  const getTextBeforeCaret = useCallback(() => {
    if (!inputRef.current) return getPlainTextFromDiv();

    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) return getPlainTextFromDiv();

    const range = selection.getRangeAt(0);
    if (!inputRef.current.contains(range.startContainer)) return getPlainTextFromDiv();

    const preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(inputRef.current);
    preCaretRange.setEnd(range.startContainer, range.startOffset);
    return preCaretRange.toString();
  }, [getPlainTextFromDiv]);

  const getCaretDropdownPosition = useCallback(() => {
    if (!inputRef.current) {
      return { top: 0, bottom: 0, left: 0, width: 0 };
    }

    const inputRect = inputRef.current.getBoundingClientRect();
    if (inputRect.width <= 0 || inputRect.height <= 0 || inputRect.bottom <= 0) {
      return { top: 0, bottom: 0, left: 0, width: 0 };
    }

    const fallbackPosition: InlineDropdownPosition = {
      top: Math.max(inputRect.top, inputRect.bottom - 24),
      bottom: inputRect.bottom,
      left: inputRect.left + 12,
      width: 320,
    };

    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) {
      return fallbackPosition;
    }

    const range = selection.getRangeAt(0).cloneRange();
    if (!inputRef.current.contains(range.startContainer)) {
      return fallbackPosition;
    }

    range.collapse(true);
    let rect = range.getClientRects()[0] ?? range.getBoundingClientRect();

    if ((rect.width === 0 && rect.height === 0) || !Number.isFinite(rect.left)) {
      const marker = document.createElement("span");
      marker.textContent = "\u200B";
      range.insertNode(marker);
      rect = marker.getBoundingClientRect();
      const parent = marker.parentNode;
      const nextSibling = marker.nextSibling;
      marker.remove();
      if (parent) {
        const restoreRange = document.createRange();
        if (nextSibling) {
          restoreRange.setStartBefore(nextSibling);
        } else {
          restoreRange.selectNodeContents(parent);
          restoreRange.collapse(false);
        }
        restoreRange.collapse(true);
        selection.removeAllRanges();
        selection.addRange(restoreRange);
      }
    }

    if (!Number.isFinite(rect.left) || rect.height === 0) {
      return fallbackPosition;
    }

    const horizontalPadding = 12;
    const left = Math.min(
      Math.max(rect.left, inputRect.left + horizontalPadding),
      inputRect.right - horizontalPadding,
    );

    const position: InlineDropdownPosition = {
      top: rect.top,
      bottom: rect.bottom,
      left,
      width: 320,
    };

    return position;
  }, []);

  const getMentionDropdownPosition = useCallback(() => {
    const position = getCaretDropdownPosition();
    if (!inputRef.current) return position;

    const inputRect = inputRef.current.getBoundingClientRect();
    return {
      ...position,
      width: Math.min(360, Math.max(220, inputRect.width - 24)),
    };
  }, [getCaretDropdownPosition]);
  const getSlashDropdownPosition = useCallback(() => {
    const position = getCaretDropdownPosition();
    if (!inputRef.current) return position;

    const inputRect = inputRef.current.getBoundingClientRect();
    return {
      ...position,
      width: Math.min(320, Math.max(180, inputRect.width - 24)),
    };
  }, [getCaretDropdownPosition]);

  const syncInputFromEditable = useCallback(() => {
    const newPlainText = getPlainTextFromDiv();
    setInput(newPlainText);
    setHasInputText(newPlainText.trim().length > 0);
    return newPlainText;
  }, [getPlainTextFromDiv, setInput]);

  const removeComposerToken = useCallback(
    (token: Element) => {
      const parent = token.parentNode;
      const nextSibling = token.nextSibling;
      token.remove();
      if (
        nextSibling?.nodeType === Node.TEXT_NODE &&
        (nextSibling.textContent === "\u200B" || nextSibling.textContent === " ")
      ) {
        nextSibling.remove();
      }

      syncInputFromEditable();

      if (!parent) return;

      const selection = window.getSelection();
      if (!selection) return;

      const range = document.createRange();
      if (nextSibling?.parentNode === parent) {
        range.setStartBefore(nextSibling);
      } else {
        range.selectNodeContents(parent);
        range.collapse(false);
      }
      range.collapse(true);
      selection.removeAllRanges();
      selection.addRange(range);
    },
    [syncInputFromEditable],
  );

  // Function to recalculate mention dropdown position
  const recalculateMentionPosition = useCallback(() => {
    if (!mentionState.active) return;
    updatePosition(getMentionDropdownPosition());
  }, [mentionState.active, updatePosition, getMentionDropdownPosition]);

  const mentionableFiles = useMemo(
    () => allProjectFiles.filter((file) => !file.isDir && !shouldIgnoreFile(file.path)),
    [allProjectFiles],
  );

  // ResizeObserver to track container size changes
  useEffect(() => {
    if (!aiChatContainerRef.current) return;

    const resizeObserver = new ResizeObserver(() => {
      recalculateMentionPosition();
      if (slashCommandState.active) {
        showSlashCommands(getSlashDropdownPosition(), slashCommandState.search);
      }
    });

    resizeObserver.observe(aiChatContainerRef.current);

    // Also observe the window resize
    const handleWindowResize = () => {
      recalculateMentionPosition();
      if (slashCommandState.active) {
        showSlashCommands(getSlashDropdownPosition(), slashCommandState.search);
      }
    };

    window.addEventListener("resize", handleWindowResize);

    return () => {
      resizeObserver.disconnect();
      window.removeEventListener("resize", handleWindowResize);
      // Cleanup timers
      if (performanceTimer.current) {
        clearTimeout(performanceTimer.current);
      }
    };
  }, [
    recalculateMentionPosition,
    slashCommandState.active,
    slashCommandState.search,
    showSlashCommands,
    getSlashDropdownPosition,
  ]);

  // Sync contentEditable div with input state when it changes externally (e.g., when switching chats)
  // We use a ref to track the last synced input to avoid subscribing to every keystroke
  const lastSyncedInputRef = useRef("");

  useEffect(() => {
    // Only sync when input changes externally (not from user typing)
    const checkAndSync = () => {
      if (!inputRef.current || isUpdatingContentRef.current) return;

      const storeInput = useAIChatStore.getState().input;
      const currentContent = getPlainTextFromDiv();

      // Only sync if store input differs from what's in the DOM and it's an external change
      if (storeInput !== currentContent && storeInput !== lastSyncedInputRef.current) {
        isUpdatingContentRef.current = true;
        lastSyncedInputRef.current = storeInput;

        // Update contentEditable content
        if (storeInput === "") {
          inputRef.current.innerHTML = "";
        } else {
          inputRef.current.textContent = storeInput;
        }

        // Position cursor at the end
        setTimeout(() => {
          if (inputRef.current) {
            const selection = window.getSelection();
            if (selection) {
              const range = document.createRange();
              range.selectNodeContents(inputRef.current);
              range.collapse(false);
              selection.removeAllRanges();
              selection.addRange(range);
            }
            inputRef.current.focus();
          }
          isUpdatingContentRef.current = false;
        }, 0);
      }
    };

    // Check on mount and when component updates
    checkAndSync();

    // Note: We don't subscribe to continuous changes to avoid re-renders
    // The checkAndSync on mount handles initial sync and chat switching
  }, [getPlainTextFromDiv]);

  useEffect(() => {
    return () => {
      shouldKeepListeningRef.current = false;
      speechRecognitionRef.current?.abort();
      speechRecognitionRef.current = null;
    };
  }, []);

  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
    // Handle slash command navigation
    if (slashCommandState.active) {
      if (e.key === "ArrowDown") {
        e.preventDefault();
        selectNextSlashCommand();
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        selectPreviousSlashCommand();
      } else if (e.key === "Enter" || e.key === "Tab") {
        e.preventDefault();
        const filteredCommands = getFilteredSlashCommands();
        if (filteredCommands[slashCommandState.selectedIndex]) {
          handleSlashCommandSelect(filteredCommands[slashCommandState.selectedIndex]);
        }
      } else if (e.key === "Escape") {
        e.preventDefault();
        hideSlashCommands();
      }
    } else if (mentionState.active) {
      if (e.key === "ArrowDown") {
        e.preventDefault();
        const lastIndex = visibleMentionFilesRef.current.length - 1;
        setSelectedIndex(lastIndex < 0 ? 0 : Math.min(mentionState.selectedIndex + 1, lastIndex));
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        setSelectedIndex(Math.max(mentionState.selectedIndex - 1, 0));
      } else if (e.key === "Enter" || e.key === "Tab") {
        e.preventDefault();
        const visibleFiles = visibleMentionFilesRef.current;
        if (visibleFiles[mentionState.selectedIndex]) {
          handleFileMentionSelect(visibleFiles[mentionState.selectedIndex]);
        }
      } else if (e.key === "Escape") {
        e.preventDefault();
        hideMention();
      }
    } else if (e.key === "Backspace" || e.key === "Delete") {
      // Handle composer token deletion
      const selection = window.getSelection();
      if (selection && selection.rangeCount > 0 && inputRef.current) {
        const range = selection.getRangeAt(0);
        if (!range.collapsed) return;

        const container = range.startContainer;
        const offset = range.startOffset;
        let tokenToRemove: Element | null = null;
        const isBackwardDelete = e.key === "Backspace";

        if (container === inputRef.current) {
          const candidateIndex = isBackwardDelete ? offset - 1 : offset;
          const candidateNode = inputRef.current.childNodes[candidateIndex] ?? null;
          if (isComposerTokenElement(candidateNode)) {
            tokenToRemove = candidateNode;
          }
        }

        // Check if cursor is at the beginning of a text node that follows a composer token
        if (!tokenToRemove && container.nodeType === Node.TEXT_NODE) {
          const textContent = container.textContent || "";
          const candidateSibling =
            isBackwardDelete && offset === 0
              ? container.previousSibling
              : !isBackwardDelete && offset === textContent.length
                ? container.nextSibling
                : null;

          if (isComposerTokenElement(candidateSibling)) {
            tokenToRemove = candidateSibling;
          }
        }

        // Check if cursor is right after a composer token (in separator text node)
        if (
          isBackwardDelete &&
          !tokenToRemove &&
          container.nodeType === Node.TEXT_NODE &&
          container.textContent === "\u200B" &&
          offset === 1
        ) {
          const previousSibling = container.previousSibling?.previousSibling ?? null; // Skip the space node

          if (isComposerTokenElement(previousSibling)) {
            tokenToRemove = previousSibling;
          }
        }

        if (tokenToRemove) {
          e.preventDefault();
          removeComposerToken(tokenToRemove);
          return;
        }
      }
    } else if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      handleSendMessage();
    }
  };

  // Debounced mention detection - increased delay for better performance
  const debouncedMentionDetection = useCallback(() => {
    if (performanceTimer.current) {
      clearTimeout(performanceTimer.current);
    }

    performanceTimer.current = window.setTimeout(() => {
      if (!inputRef.current) return;

      const textBeforeCaret = getTextBeforeCaret();
      const lastAtIndex = textBeforeCaret.lastIndexOf("@");

      if (lastAtIndex !== -1) {
        const afterAt = textBeforeCaret.slice(lastAtIndex + 1);
        // Check if there's no space between @ and end, and it's not part of a mention badge
        if (!afterAt.includes(" ") && !afterAt.includes("]") && afterAt.length < 50) {
          const position = getMentionDropdownPosition();
          showMention(position, afterAt, lastAtIndex);
        } else {
          hideMention();
        }
      } else {
        hideMention();
      }
    }, 150); // Increased to 150ms for better performance
  }, [showMention, hideMention, getMentionDropdownPosition, getTextBeforeCaret]);

  // Optimized input change handler - no throttle for immediate response
  const handleInputChange = useCallback(() => {
    if (!inputRef.current || isUpdatingContentRef.current) return;

    const plainTextFromDiv = getPlainTextFromDiv();

    // Use store's getState to avoid dependency on input prop
    const currentInput = useAIChatStore.getState().input;

    // Only update if content actually changed
    if (plainTextFromDiv !== currentInput) {
      setInput(plainTextFromDiv);

      // Update local state for button enabled/disabled
      setHasInputText(plainTextFromDiv.trim().length > 0);

      const textBeforeCaret = getTextBeforeCaret();
      const slashMatch = textBeforeCaret.match(/(?:^|\s)\/([^\s/]*)$/);
      if (slashMatch && slashMatch[1].length < 50) {
        const search = slashMatch[1];
        const startIndex = textBeforeCaret.length - search.length - 1;
        slashCommandRangeRef.current = {
          startIndex,
          endIndex: textBeforeCaret.length,
        };
        setActiveInlineControl("commands");
        if (isContextDropdownOpen) {
          setIsContextDropdownOpen(false);
        }
        showSlashCommands(getSlashDropdownPosition(), search);
      } else if (slashCommandState.active) {
        setActiveInlineControl(null);
        hideSlashCommands();
      }

      // Only do mention detection if text contains @ and is reasonably short
      if (plainTextFromDiv.includes("@") && plainTextFromDiv.length < 500) {
        debouncedMentionDetection();
      } else if (mentionState.active) {
        hideMention();
      }
    }
  }, [
    setInput,
    getPlainTextFromDiv,
    getTextBeforeCaret,
    debouncedMentionDetection,
    hideMention,
    mentionState.active,
    showSlashCommands,
    hideSlashCommands,
    slashCommandState.active,
    getSlashDropdownPosition,
    isContextDropdownOpen,
    setIsContextDropdownOpen,
  ]);

  const handleEditableMouseDown = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
    if (!inputRef.current) return;

    const target = event.target as HTMLElement | null;
    const token = target?.closest("[data-mention],[data-slash-command]");
    if (!token || !inputRef.current.contains(token)) return;

    event.preventDefault();
    inputRef.current.focus();

    const selection = window.getSelection();
    if (!selection) return;

    const range = document.createRange();
    range.setStartAfter(token);
    range.collapse(true);
    selection.removeAllRanges();
    selection.addRange(range);
  }, []);

  const insertTextAtCursor = useCallback(
    (text: string) => {
      if (!inputRef.current || !text) return;

      const normalizedText = text.replace(/\s+/g, " ").trim();
      if (!normalizedText) return;

      const selection = window.getSelection();
      const range = document.createRange();
      const currentText = getPlainTextFromDiv();
      const prefix = currentText.trim().length > 0 && !/\s$/.test(currentText) ? " " : "";
      const textNode = document.createTextNode(`${prefix}${normalizedText} `);

      inputRef.current.focus();

      const selectionInsideInput =
        !!selection && selection.rangeCount > 0 && inputRef.current.contains(selection.anchorNode);

      if (selectionInsideInput && selection) {
        const selectedRange = selection.getRangeAt(0);
        selectedRange.deleteContents();
        selectedRange.insertNode(textNode);
        range.setStartAfter(textNode);
      } else {
        range.selectNodeContents(inputRef.current);
        range.collapse(false);
        range.insertNode(textNode);
        range.setStartAfter(textNode);
      }

      range.collapse(true);
      selection?.removeAllRanges();
      selection?.addRange(range);
      handleInputChange();
    },
    [getPlainTextFromDiv, handleInputChange],
  );

  const insertSkillAtCursor = useCallback(
    (skill: AIChatSkill) => {
      if (!inputRef.current || !skill.content.trim()) return;

      const selection = window.getSelection();
      const range = document.createRange();
      const currentText = getPlainTextFromDiv();
      const prefix = currentText.trim().length > 0 && !/\s$/.test(currentText) ? "\n\n" : "";
      const textNode = document.createTextNode(`${prefix}${skill.content.trim()} `);

      inputRef.current.focus();

      const selectionInsideInput =
        !!selection && selection.rangeCount > 0 && inputRef.current.contains(selection.anchorNode);

      if (selectionInsideInput && selection) {
        const selectedRange = selection.getRangeAt(0);
        selectedRange.deleteContents();
        selectedRange.insertNode(textNode);
        range.setStartAfter(textNode);
      } else {
        range.selectNodeContents(inputRef.current);
        range.collapse(false);
        range.insertNode(textNode);
        range.setStartAfter(textNode);
      }

      range.collapse(true);
      selection?.removeAllRanges();
      selection?.addRange(range);
      handleInputChange();
      setHasInputText(true);
    },
    [getPlainTextFromDiv, handleInputChange],
  );

  useEffect(() => {
    const handleInsertSkill = (event: Event) => {
      const skill = (event as CustomEvent<AIChatSkill>).detail;
      if (!skill) return;
      insertSkillAtCursor(skill);
    };

    window.addEventListener(AI_CHAT_INSERT_SKILL_EVENT, handleInsertSkill);
    return () => window.removeEventListener(AI_CHAT_INSERT_SKILL_EVENT, handleInsertSkill);
  }, [insertSkillAtCursor]);

  // Handle paste - strip HTML formatting, keep only plain text. Images are added to preview.
  const handlePaste = useCallback(
    (e: React.ClipboardEvent<HTMLDivElement>) => {
      const clipboardData = e.clipboardData;
      if (!clipboardData) return;

      // Check for images first
      const items = clipboardData.items;
      let hasImage = false;

      for (let i = 0; i < items.length; i++) {
        if (items[i].type.startsWith("image/")) {
          hasImage = true;
          e.preventDefault();

          const file = items[i].getAsFile();
          if (file) {
            const reader = new FileReader();
            reader.onload = (event) => {
              const dataUrl = event.target?.result as string;
              if (dataUrl) {
                addPastedImage({
                  id: `img-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
                  dataUrl,
                  name: file.name || `image-${Date.now()}.png`,
                  size: file.size,
                });
              }
            };
            reader.readAsDataURL(file);
          }
        }
      }

      // If there was an image, don't process text
      if (hasImage) return;

      // For text content, prevent default and insert plain text only
      e.preventDefault();

      // Get plain text from clipboard
      const plainText = clipboardData.getData("text/plain");
      if (!plainText) return;

      // Insert plain text at cursor position
      const selection = window.getSelection();
      if (!selection || selection.rangeCount === 0) return;

      const range = selection.getRangeAt(0);
      range.deleteContents();

      const textNode = document.createTextNode(plainText);
      range.insertNode(textNode);

      // Move cursor to end of inserted text
      range.setStartAfter(textNode);
      range.setEndAfter(textNode);
      selection.removeAllRanges();
      selection.addRange(range);

      // Trigger input change handler to update state
      handleInputChange();
    },
    [handleInputChange, addPastedImage],
  );

  // Handle file mention selection
  const handleFileMentionSelect = useCallback(
    (file: FileEntry) => {
      if (!inputRef.current) return;

      isUpdatingContentRef.current = true;

      const currentInput = useAIChatStore.getState().input;
      const beforeMention = currentInput.slice(0, mentionState.startIndex);
      const afterMention = currentInput.slice(
        mentionState.startIndex + mentionState.search.length + 1,
      );
      const newInput = `${beforeMention}@[${file.name}] ${afterMention}`;

      // Update input state and hide mention dropdown
      setInput(newInput);
      hideMention();

      // Completely rebuild the DOM content to ensure clean structure
      setTimeout(() => {
        if (!inputRef.current) return;

        // Clear all content
        inputRef.current.innerHTML = "";

        // Build new content piece by piece
        // Add text before mention if any
        if (beforeMention) {
          const beforeTextNode = document.createTextNode(beforeMention);
          inputRef.current.appendChild(beforeTextNode);
        }

        // Add the mention badge
        const mentionSpan = document.createElement("span");
        mentionSpan.setAttribute("data-mention", "true");
        mentionSpan.setAttribute("data-mention-name", file.name);
        mentionSpan.setAttribute("data-mention-path", file.path);
        mentionSpan.setAttribute("contenteditable", "false");
        mentionSpan.title = file.path;
        mentionSpan.className =
          "ui-font ui-text-xs inline-flex min-h-6 max-w-[180px] items-center gap-1 truncate rounded-md border border-accent/30 bg-accent/10 px-1.5 py-0.5 leading-[1.35] text-accent align-baseline select-none";
        mentionSpan.textContent = file.name;
        inputRef.current.appendChild(mentionSpan);

        // Always add a space and remaining text as separate text node
        // Ensure there's always substantial content to prevent cursor from jumping to span
        const remainingText = ` ${afterMention}${afterMention ? "" : " "}`;
        const afterTextNode = document.createTextNode(remainingText);
        inputRef.current.appendChild(afterTextNode);

        // Add an invisible zero-width space to ensure cursor stays in text node
        const separatorNode = document.createTextNode("\u200B"); // Zero-width space
        inputRef.current.appendChild(separatorNode);

        // Position cursor in the separator node to ensure it doesn't jump to span
        const selection = window.getSelection();
        if (selection) {
          const range = document.createRange();
          try {
            // Position at the end of the separator node
            range.setStart(separatorNode, 1);
            range.collapse(true);
            selection.removeAllRanges();
            selection.addRange(range);
          } catch {
            // Fallback - position at end of input
            range.selectNodeContents(inputRef.current);
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
          }
        }

        inputRef.current.focus();
        isUpdatingContentRef.current = false;
        setHasInputText(true);
      }, 0);
    },
    [mentionState.startIndex, mentionState.search.length, setInput, hideMention],
  );

  // Handle slash command selection
  const handleSlashCommandSelect = useCallback(
    (command: SlashCommand) => {
      if (!inputRef.current) return;

      isUpdatingContentRef.current = true;

      const currentInput = getPlainTextFromDiv();
      const { startIndex, endIndex } = slashCommandRangeRef.current;
      const beforeCommand = currentInput.slice(0, startIndex);
      const afterCommand = currentInput.slice(endIndex);
      const normalizedAfterCommand = afterCommand.startsWith(" ")
        ? afterCommand.slice(1)
        : afterCommand;
      const newInput = `${beforeCommand}/${command.name} ${normalizedAfterCommand}`;
      setInput(newInput);
      hideSlashCommands();

      // Rebuild the DOM so the command behaves like an atomic composer token.
      setTimeout(() => {
        if (!inputRef.current) return;

        inputRef.current.innerHTML = "";

        if (beforeCommand) {
          inputRef.current.appendChild(document.createTextNode(beforeCommand));
        }

        const commandSpan = document.createElement("span");
        commandSpan.setAttribute("data-slash-command", "true");
        commandSpan.setAttribute("data-slash-command-name", command.name);
        commandSpan.setAttribute("contenteditable", "false");
        commandSpan.title = command.description || `/${command.name}`;
        commandSpan.className =
          "ui-font ui-text-xs inline-flex min-h-6 max-w-[180px] items-center gap-1 truncate rounded-md border border-border/70 bg-hover/70 px-1.5 py-0.5 leading-[1.35] text-text align-baseline select-none";
        commandSpan.textContent = `/${command.name}`;
        inputRef.current.appendChild(commandSpan);

        const afterTextNode = document.createTextNode(
          ` ${normalizedAfterCommand}${normalizedAfterCommand ? "" : " "}`,
        );
        inputRef.current.appendChild(afterTextNode);

        const separatorNode = document.createTextNode("\u200B");
        inputRef.current.appendChild(separatorNode);

        const selection = window.getSelection();
        if (selection) {
          const range = document.createRange();
          try {
            range.setStart(separatorNode, 1);
            range.collapse(true);
            selection.removeAllRanges();
            selection.addRange(range);
          } catch {
            range.selectNodeContents(inputRef.current);
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
          }
        }

        inputRef.current.focus();
        isUpdatingContentRef.current = false;
        setHasInputText(true);
      }, 0);
    },
    [getPlainTextFromDiv, setInput, hideSlashCommands],
  );

  const handleSendMessage = async () => {
    const currentInput = useAIChatStore.getState().input;
    const currentImages = useAIChatStore.getState().pastedImages;
    const hasContent = currentInput.trim() || currentImages.length > 0;
    if (!hasContent || !isInputEnabled) return;

    // Clear input and images immediately after send is triggered
    setInput("");
    setHasInputText(false);
    clearPastedImages();
    if (inputRef.current) {
      inputRef.current.innerHTML = "";
    }

    // Send the captured message (TODO: include images in message)
    await onSendMessage(currentInput);
  };

  const stopVoiceInput = useCallback(() => {
    shouldKeepListeningRef.current = false;
    speechRecognitionRef.current?.stop();
    setIsListening(false);
    setInterimTranscript("");
  }, []);

  const startVoiceInput = useCallback(async () => {
    if (!isInputEnabled) return;

    if (isMacDevSpeechRecognitionBlocked) {
      toast.warning("Voice input is disabled in macOS dev mode. Test it in a packaged app build.");
      return;
    }

    if (!SpeechRecognitionCtor) {
      toast.warning("Voice input is not supported in this webview.");
      return;
    }

    if (navigator.mediaDevices?.getUserMedia) {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        stream.getTracks().forEach((track) => track.stop());
      } catch {
        toast.error(getMicrophoneAccessErrorMessage());
        return;
      }
    }

    const recognition = new SpeechRecognitionCtor();
    speechRecognitionRef.current = recognition;
    shouldKeepListeningRef.current = true;

    recognition.continuous = true;
    recognition.interimResults = true;
    recognition.lang = navigator.language || "en-US";

    recognition.onresult = (event) => {
      let committedTranscript = "";
      let nextInterimTranscript = "";

      for (let i = event.resultIndex; i < event.results.length; i++) {
        const transcript = event.results[i][0]?.transcript?.trim();
        if (!transcript) continue;

        if (event.results[i].isFinal) {
          committedTranscript += `${transcript} `;
        } else {
          nextInterimTranscript = transcript;
        }
      }

      if (committedTranscript.trim()) {
        insertTextAtCursor(committedTranscript);
      }

      setInterimTranscript(nextInterimTranscript);
    };

    recognition.onerror = (event) => {
      const isExpectedAbort = event.error === "aborted" || event.error === "no-speech";
      setIsListening(false);
      setInterimTranscript("");

      if (event.error === "not-allowed" || event.error === "service-not-allowed") {
        shouldKeepListeningRef.current = false;
        toast.error(getMicrophoneAccessErrorMessage(event.error));
        return;
      }

      if (!isExpectedAbort) {
        shouldKeepListeningRef.current = false;
        toast.error("Voice input stopped unexpectedly.");
      }
    };

    recognition.onend = () => {
      if (shouldKeepListeningRef.current) {
        try {
          recognition.start();
          return;
        } catch {
          shouldKeepListeningRef.current = false;
        }
      }

      setIsListening(false);
      setInterimTranscript("");
      speechRecognitionRef.current = null;
    };

    try {
      recognition.start();
      setIsListening(true);
      setInterimTranscript("");
      inputRef.current?.focus();
    } catch {
      shouldKeepListeningRef.current = false;
      speechRecognitionRef.current = null;
      toast.error("Voice input could not be started.");
    }
  }, [SpeechRecognitionCtor, insertTextAtCursor, isInputEnabled, isMacDevSpeechRecognitionBlocked]);

  const toggleVoiceInput = useCallback(() => {
    if (isListening) {
      stopVoiceInput();
      return;
    }

    void startVoiceInput();
  }, [isListening, startVoiceInput, stopVoiceInput]);

  // Get available slash commands
  const availableSlashCommands = useAIChatStore((state) => state.availableSlashCommands);
  const hasSlashCommands = availableSlashCommands.length > 0;
  const hasAttachedComposerDropdown = mentionState.active || slashCommandState.active;

  return (
    <SidebarFooter
      ref={aiChatContainerRef}
      surface
      attached={hasAttachedComposerDropdown}
      data-ai-context-drop-target
      onDragOver={handleContextDragOver}
      onDragLeave={handleContextDragLeave}
      onDrop={handleContextDrop}
      className={cn(
        "ai-chat-container relative z-20",
        isContextDragOver && "border-accent bg-accent/5 shadow-[0_0_0_1px_var(--color-accent)]",
      )}
    >
      <SidebarComposerBody>
        {pastedImages.length > 0 && (
          <div className="flex flex-wrap gap-2 px-3 pt-3">
            {pastedImages.map((image) => (
              <div
                key={image.id}
                className="group relative overflow-hidden rounded border border-border bg-secondary-bg"
              >
                <img
                  src={image.dataUrl}
                  alt={image.name}
                  className="h-16 w-auto max-w-[120px] object-cover"
                />
                <Button
                  onClick={() => removePastedImage(image.id)}
                  variant="ghost"
                  compact
                  className="absolute top-0.5 right-0.5 rounded-full bg-black/60 text-white opacity-0 hover:bg-black/80 group-hover:opacity-100"
                  aria-label="Remove image"
                >
                  <X />
                </Button>
              </div>
            ))}
          </div>
        )}

        <div
          ref={inputRef}
          contentEditable={isInputEnabled}
          onInput={handleInputChange}
          onKeyDown={handleKeyDown}
          onMouseDown={handleEditableMouseDown}
          onFocus={() => setIsComposerFocused(true)}
          onBlur={() => setIsComposerFocused(false)}
          onPaste={handlePaste}
          data-placeholder={
            isInputEnabled
              ? hasSlashCommands
                ? "Ask anything... (@ files, / commands)"
                : "Ask anything... (@ to mention files)"
              : "Configure API key to enable AI chat..."
          }
          className={cn(
            "max-h-[140px] min-h-[64px] w-full resize-none overflow-x-hidden overflow-y-auto bg-transparent",
            "ui-font ui-text-sm px-3 pt-3 pb-2 text-text placeholder:text-text-lighter",
            "whitespace-pre-wrap focus:outline-none",
            hasAttachedComposerDropdown && "border-none",
            !isInputEnabled ? "cursor-not-allowed opacity-50" : "cursor-text",
            "empty:before:pointer-events-none empty:before:text-text-lighter empty:before:content-[attr(data-placeholder)]",
          )}
          style={
            {
              lineHeight: "1.4",
              wordWrap: "break-word",
              overflowWrap: "break-word",
            } as React.CSSProperties
          }
          role="textbox"
          aria-multiline="true"
          aria-label="Message input"
          tabIndex={isInputEnabled ? 0 : -1}
        />

        <div className="flex items-end gap-2 px-2 pb-2 pt-1">
          <div ref={contextDropdownRef} className="min-w-0 flex-1">
            <ContextSelector
              buffers={buffers}
              selectedBufferIds={selectedBufferIds}
              selectedFilesPaths={selectedFilesPaths}
              onToggleBuffer={toggleBufferSelection}
              onToggleFile={toggleFileSelection}
              isOpen={isContextDropdownOpen}
              onToggleOpen={() => {
                if (!isContextDropdownOpen) {
                  closeInlineMenus();
                }
                setIsContextDropdownOpen(!isContextDropdownOpen);
              }}
              selectedItemsClassName="max-h-14 pr-1"
            />
          </div>

          {queueCount > 0 && (
            <Badge
              size="sm"
              className="shrink-0 gap-1 border border-accent/30 bg-accent/10 px-2.5 text-accent"
            >
              <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-accent" />
              <span>{queueCount}</span>
            </Badge>
          )}

          <div className="flex shrink-0 items-center gap-1">
            <Button
              type="button"
              disabled={!isInputEnabled || !isSpeechRecognitionSupported}
              onClick={toggleVoiceInput}
              variant="ghost"
              className={cn(
                chatComposerIconButtonClassName(),
                isListening && "bg-accent/10 text-accent hover:bg-accent/14 hover:text-accent",
              )}
              tooltip={
                isMacDevSpeechRecognitionBlocked
                  ? "Voice input is disabled in macOS dev mode"
                  : !isSpeechRecognitionSupported
                    ? "Voice input is not supported"
                    : isListening
                      ? interimTranscript || "Stop voice input"
                      : "Start voice input"
              }
              aria-label={isListening ? "Stop voice input" : "Start voice input"}
              aria-pressed={isListening}
              compact
            >
              <Mic size={12} className={cn(isListening && "animate-pulse")} />
            </Button>

            <Button
              type="button"
              disabled={isSendDisabled}
              onClick={isStreaming ? onStopStreaming : handleSendMessage}
              variant="ghost"
              className={cn(
                chatComposerIconButtonClassName(),
                isSendDisabled
                  ? "cursor-not-allowed bg-hover/40 text-text-lighter opacity-50"
                  : isStreaming
                    ? "bg-error/10 text-error hover:bg-error/15 hover:text-error"
                    : (hasInputText || hasImages) && isInputEnabled
                      ? "bg-accent text-white hover:bg-accent/85 hover:text-white"
                      : "bg-hover/70 text-text-lighter hover:bg-hover hover:text-text",
              )}
              tooltip={
                isStreaming ? "Stop generation" : queueCount > 0 ? "Add to queue" : "Send message"
              }
              shortcut={isStreaming ? "escape" : "enter"}
              aria-label={isStreaming ? "Stop generation" : "Send message"}
            >
              {isStreaming ? <Stop /> : <ArrowUp />}
            </Button>
          </div>
        </div>
      </SidebarComposerBody>

      <div className="flex flex-wrap items-center gap-x-2 gap-y-1 px-1 pt-1.5">
        {isAcpMetadataLoading ? (
          <ChatLoadingIndicator label="loading session" compact />
        ) : (
          <>
            <div className="flex min-w-0 flex-1 flex-wrap items-center gap-1">
              {acpInlineConfigOptions.map(({ option, category }) => {
                const controlId = `config:${option.id}`;
                return (
                  <AcpConfigSelector
                    key={option.id}
                    option={option}
                    onChange={(value) => void changeSessionConfigOption(option.id, value)}
                    open={activeInlineControl === controlId}
                    onOpenChange={(open) => {
                      if (open) {
                        closeComposerPopovers();
                        setActiveInlineControl(controlId);
                        return;
                      }
                      setActiveInlineControl((current) => (current === controlId ? null : current));
                    }}
                    className={category === "model" ? "max-w-[220px]" : "max-w-[168px]"}
                    menuClassName="max-w-[260px]"
                    menuMinWidth={category === "model" ? 220 : 180}
                  />
                );
              })}

              {isCustomAgent && (
                <>
                  <ProviderSelector
                    providerId={aiProviderId}
                    onChange={handleAthasProviderChange}
                    appearance="composer"
                    open={activeInlineControl === "provider"}
                    onOpenChange={(open) => {
                      if (open) {
                        closeComposerPopovers();
                        setActiveInlineControl("provider");
                        return;
                      }
                      setActiveInlineControl((current) =>
                        current === "provider" ? null : current,
                      );
                    }}
                    triggerClassName="max-w-[128px]"
                    tooltip="Select provider"
                  />
                  <ModelSelector
                    providerId={aiProviderId}
                    modelId={aiModelId}
                    onChange={handleAthasModelChange}
                    appearance="composer"
                    open={activeInlineControl === "model"}
                    onOpenChange={(open) => {
                      if (open) {
                        closeComposerPopovers();
                        setActiveInlineControl("model");
                        return;
                      }
                      setActiveInlineControl((current) => (current === "model" ? null : current));
                    }}
                    triggerClassName="max-w-[176px]"
                    tooltip="Select model"
                  />
                  <Button
                    type="button"
                    variant="ghost"
                    className={chatComposerIconButtonClassName()}
                    tooltip="API keys"
                    aria-label="Manage API keys"
                    onClick={() => {
                      closeInlineMenus();
                      setIsApiKeyManagerOpen(true);
                    }}
                  >
                    <Key />
                  </Button>
                </>
              )}

              {(isCustomAgent || !hasAcpConfigModeOption) && (
                <ModeSelector
                  open={activeInlineControl === "mode"}
                  onOpenChange={(open) => {
                    if (open) {
                      closeComposerPopovers();
                      setActiveInlineControl("mode");
                      return;
                    }
                    setActiveInlineControl((current) => (current === "mode" ? null : current));
                  }}
                  iconOnly
                />
              )}

              {hasSlashCommands && (
                <Button
                  onClick={() => {
                    if (inputRef.current && isInputEnabled) {
                      if (slashCommandState.active) {
                        setActiveInlineControl(null);
                        hideSlashCommands();
                        return;
                      }
                      closeInlineMenus();
                      inputRef.current.textContent = "/";
                      setInput("/");
                      setHasInputText(true);
                      inputRef.current.focus();
                      const selection = window.getSelection();
                      if (selection) {
                        const range = document.createRange();
                        range.selectNodeContents(inputRef.current);
                        range.collapse(false);
                        selection.removeAllRanges();
                        selection.addRange(range);
                      }
                      slashCommandRangeRef.current = { startIndex: 0, endIndex: 1 };
                      setActiveInlineControl("commands");
                      showSlashCommands(getSlashDropdownPosition(), "");
                    }
                  }}
                  variant="ghost"
                  compact
                  active={slashCommandState.active}
                  className={chatComposerIconButtonClassName()}
                  tooltip="Show slash commands"
                  aria-label="Show slash commands"
                >
                  <CommandIcon size={12} />
                </Button>
              )}
            </div>

            <Button
              type="button"
              onClick={() => {
                closeInlineMenus();
                setIsSkillsOpen(true);
              }}
              variant="ghost"
              compact
              className={chatComposerIconButtonClassName("ml-auto shrink-0")}
              tooltip="Skills"
              aria-label="Skills"
            >
              <BookOpen />
            </Button>
          </>
        )}
      </div>

      {(isActiveSurface || isComposerFocused) && mentionState.active && (
        <FileMentionDropdown
          files={mentionableFiles}
          onSelect={handleFileMentionSelect}
          onVisibleFilesChange={(files) => {
            visibleMentionFilesRef.current = files;
          }}
        />
      )}

      {(isActiveSurface || isComposerFocused) && slashCommandState.active && (
        <SlashCommandDropdown onSelect={handleSlashCommandSelect} />
      )}

      <SkillsCommand
        isOpen={isSkillsOpen}
        onClose={() => setIsSkillsOpen(false)}
        onSelectSkill={insertSkillAtCursor}
      />

      <ProviderApiKeyCommand
        isOpen={isApiKeyManagerOpen}
        onClose={() => setIsApiKeyManagerOpen(false)}
        initialProviderId={aiProviderId}
      />
    </SidebarFooter>
  );
});

export default AIChatInputBar;
