/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import type React from 'react';
import {
  useCallback,
  useEffect,
  useState,
  useRef,
  useMemo,
  Fragment,
} from 'react';
import clipboardy from 'clipboardy';
import { Box, Text, useStdout, type DOMElement } from 'ink';
import { SuggestionsDisplay, MAX_WIDTH } from './SuggestionsDisplay.js';
import { theme } from '../semantic-colors.js';
import { useInputHistory } from '../hooks/useInputHistory.js';
import { escapeAtSymbols } from '../hooks/atCommandProcessor.js';
import {
  ScrollableList,
  type ScrollableListRef,
} from './shared/ScrollableList.js';
import { ListeningIndicator } from './ListeningIndicator.js';
import { HalfLinePaddedBox } from './shared/HalfLinePaddedBox.js';
import {
  type TextBuffer,
  logicalPosToOffset,
  expandPastePlaceholders,
  getTransformUnderCursor,
  LARGE_PASTE_LINE_THRESHOLD,
  LARGE_PASTE_CHAR_THRESHOLD,
} from './shared/text-buffer.js';
import {
  cpSlice,
  cpLen,
  toCodePoints,
  cpIndexToOffset,
} from '../utils/textUtils.js';
import chalk from 'chalk';
import stringWidth from 'string-width';
import { useShellHistory } from '../hooks/useShellHistory.js';
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
import {
  useCommandCompletion,
  CompletionMode,
} from '../hooks/useCommandCompletion.js';
import { useKeypress, type Key } from '../hooks/useKeypress.js';
import { Command } from '../key/keyMatchers.js';
import { formatCommand } from '../key/keybindingUtils.js';
import type { CommandContext, SlashCommand } from '../commands/types.js';
import {
  ApprovalMode,
  coreEvents,
  debugLogger,
  type Config,
} from '@google/gemini-cli-core';
import { useVoiceMode } from '../hooks/useVoiceMode.js';
import {
  parseInputForHighlighting,
  parseSegmentsFromTokens,
} from '../utils/highlight.js';
import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js';
import {
  clipboardHasImage,
  saveClipboardImage,
  cleanupOldClipboardImages,
} from '../utils/clipboardUtils.js';
import {
  isAutoExecutableCommand,
  isSlashCommand,
} from '../utils/commandUtils.js';
import { parseSlashCommand } from '../../utils/commands.js';
import * as path from 'node:path';
import { SCREEN_READER_USER_PREFIX } from '../textConstants.js';
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useInputState } from '../contexts/InputContext.js';
import {
  appEvents,
  AppEvent,
  TransientMessageType,
} from '../../utils/events.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { StreamingState } from '../types.js';
import { useMouseClick } from '../hooks/useMouseClick.js';
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { useIsHelpDismissKey } from '../utils/shortcutsHelp.js';
import { useRepeatedKeyPress } from '../hooks/useRepeatedKeyPress.js';
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';

const SCROLLBAR_GUTTER_WIDTH = 1;

/**
 * Returns if the terminal can be trusted to handle paste events atomically
 * rather than potentially sending multiple paste events separated by line
 * breaks which could trigger unintended command execution.
 */
export function isTerminalPasteTrusted(
  kittyProtocolSupported: boolean,
): boolean {
  // Ideally we could trust all VSCode family terminals as well but it appears
  // we cannot as Cursor users on windows reported being impacted by this
  // issue (https://github.com/google-gemini/gemini-cli/issues/3763).
  return kittyProtocolSupported;
}

export type ScrollableItem =
  | { type: 'visualLine'; lineText: string; absoluteVisualIdx: number }
  | { type: 'ghostLine'; ghostLine: string; index: number };

export interface InputPromptProps {
  onSubmit: (value: string) => void;
  onClearScreen: () => void;
  config: Config;
  slashCommands: readonly SlashCommand[];
  commandContext: CommandContext;
  placeholder?: string;
  focus?: boolean;
  setShellModeActive: (value: boolean) => void;
  approvalMode: ApprovalMode;
  onEscapePromptChange?: (showPrompt: boolean) => void;
  onSuggestionsVisibilityChange?: (visible: boolean) => void;
  vimHandleInput?: (key: Key) => boolean;
  isEmbeddedShellFocused?: boolean;
  setQueueErrorMessage: (message: string | null) => void;
  streamingState: StreamingState;
  popAllMessages?: () => string | undefined;
  onQueueMessage?: (message: string) => void;
  suggestionsPosition?: 'above' | 'below';
  setBannerVisible: (visible: boolean) => void;
}

// The input content, input container, and input suggestions list may have different widths
export const calculatePromptWidths = (mainContentWidth: number) => {
  const FRAME_PADDING_AND_BORDER = 4; // Border (2) + padding (2)
  const PROMPT_PREFIX_WIDTH = 2; // '> ' or '! '

  const FRAME_OVERHEAD = FRAME_PADDING_AND_BORDER + PROMPT_PREFIX_WIDTH;
  const suggestionsWidth = Math.max(20, mainContentWidth);

  return {
    inputWidth: Math.max(mainContentWidth - FRAME_OVERHEAD, 1),
    containerWidth: mainContentWidth,
    suggestionsWidth,
    frameOverhead: FRAME_OVERHEAD,
  } as const;
};

/**
 * Returns true if the given text exceeds the thresholds for being considered a "large paste".
 */
export function isLargePaste(text: string): boolean {
  const pasteLineCount = text.split('\n').length;
  return (
    pasteLineCount > LARGE_PASTE_LINE_THRESHOLD ||
    text.length > LARGE_PASTE_CHAR_THRESHOLD
  );
}

const DOUBLE_TAB_CLEAN_UI_TOGGLE_WINDOW_MS = 350;
/**
 * Attempt to toggle expansion of a paste placeholder in the buffer.
 * Returns true if a toggle action was performed or hint was shown, false otherwise.
 */
export function tryTogglePasteExpansion(buffer: TextBuffer): boolean {
  if (!buffer.pastedContent || Object.keys(buffer.pastedContent).length === 0) {
    return false;
  }

  const [row, col] = buffer.cursor;

  // 1. Check if cursor is on or immediately after a collapsed placeholder
  const transform = getTransformUnderCursor(
    row,
    col,
    buffer.transformationsByLine,
    { includeEdge: true },
  );
  if (transform?.type === 'paste' && transform.id) {
    buffer.togglePasteExpansion(transform.id, row, col);
    return true;
  }

  // 2. Check if cursor is inside an expanded paste region — collapse it
  const expandedId = buffer.getExpandedPasteAtLine(row);
  if (expandedId) {
    buffer.togglePasteExpansion(expandedId, row, col);
    return true;
  }

  // 3. Placeholders exist but cursor isn't on one — show hint
  appEvents.emit(AppEvent.TransientMessage, {
    message: 'Move cursor within placeholder to expand',
    type: TransientMessageType.Hint,
  });
  return true;
}

export const InputPrompt: React.FC<InputPromptProps> = ({
  onSubmit,
  onClearScreen,
  config,
  slashCommands,
  commandContext,
  placeholder = '  Type your message or @path/to/file',
  focus = true,
  setShellModeActive,
  approvalMode,
  onEscapePromptChange,
  onSuggestionsVisibilityChange,
  vimHandleInput,
  isEmbeddedShellFocused,
  setQueueErrorMessage,
  streamingState,
  popAllMessages,
  onQueueMessage,
  suggestionsPosition = 'below',
  setBannerVisible,
}) => {
  const inputState = useInputState();
  const {
    buffer,
    userMessages,
    shellModeActive,
    copyModeEnabled,
    inputWidth,
    suggestionsWidth,
  } = inputState;
  const isHelpDismissKey = useIsHelpDismissKey();
  const keyMatchers = useKeyMatchers();
  const { stdout } = useStdout();
  const { merged: settings } = useSettings();
  const kittyProtocol = useKittyKeyboardProtocol();
  const isShellFocused = useShellFocusState();
  const {
    setEmbeddedShellFocused,
    setShortcutsHelpVisible,
    toggleCleanUiDetailsVisible,
    setVoiceModeEnabled,
  } = useUIActions();
  const {
    terminalWidth,
    activePtyId,
    history,
    backgroundTasks,
    backgroundTaskHeight,
    shortcutsHelpVisible,
    isVoiceModeEnabled,
  } = useUIState();
  const [suppressCompletion, setSuppressCompletion] = useState(false);
  const { handlePress: registerPlainTabPress, resetCount: resetPlainTabPress } =
    useRepeatedKeyPress({
      windowMs: DOUBLE_TAB_CLEAN_UI_TOGGLE_WINDOW_MS,
    });
  const [showEscapePrompt, setShowEscapePrompt] = useState(false);
  const { handlePress: handleEscPress, resetCount: resetEscapeState } =
    useRepeatedKeyPress({
      windowMs: 500,
      onRepeat: (count) => {
        if (count === 1) {
          setShowEscapePrompt(true);
        } else if (count === 2) {
          resetEscapeState();
          if (buffer.text.length > 0) {
            buffer.setText('');
            resetTurnBaseline();
            resetCompletionState();
          } else if (history.length > 0) {
            onSubmit('/rewind');
          } else {
            coreEvents.emitFeedback('info', 'Nothing to rewind to');
          }
        }
      },
      onReset: () => setShowEscapePrompt(false),
    });
  const [recentUnsafePasteTime, setRecentUnsafePasteTime] = useState<
    number | null
  >(null);
  const pasteTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const innerBoxRef = useRef<DOMElement>(null);
  const hasUserNavigatedSuggestions = useRef(false);
  const listRef = useRef<ScrollableListRef<ScrollableItem>>(null);

  const { isRecording, handleVoiceInput, resetTurnBaseline } = useVoiceMode({
    buffer,
    config,
    settings,
    setQueueErrorMessage,
    isVoiceModeEnabled,
    setVoiceModeEnabled,
    keyMatchers,
  });

  const [reverseSearchActive, setReverseSearchActive] = useState(false);
  const [commandSearchActive, setCommandSearchActive] = useState(false);
  const [textBeforeReverseSearch, setTextBeforeReverseSearch] = useState('');
  const [cursorPosition, setCursorPosition] = useState<[number, number]>([
    0, 0,
  ]);
  const [expandedSuggestionIndex, setExpandedSuggestionIndex] =
    useState<number>(-1);
  const shellHistory = useShellHistory(config.getProjectRoot(), config.storage);
  const shellHistoryData = shellHistory.history;

  const completion = useCommandCompletion({
    buffer,
    cwd: config.getTargetDir(),
    slashCommands,
    commandContext,
    reverseSearchActive,
    shellModeActive,
    config,
    active: !suppressCompletion,
  });

  const reverseSearchCompletion = useReverseSearchCompletion(
    buffer,
    shellHistoryData,
    reverseSearchActive,
  );

  const reversedUserMessages = useMemo(
    () => [...userMessages].reverse(),
    [userMessages],
  );

  const commandSearchCompletion = useReverseSearchCompletion(
    buffer,
    reversedUserMessages,
    commandSearchActive,
  );

  const resetCompletionState = completion.resetCompletionState;
  const resetReverseSearchCompletionState =
    reverseSearchCompletion.resetCompletionState;
  const resetCommandSearchCompletionState =
    commandSearchCompletion.resetCompletionState;

  const getActiveCompletion = useCallback(() => {
    if (commandSearchActive) return commandSearchCompletion;
    if (reverseSearchActive) return reverseSearchCompletion;
    return completion;
  }, [
    commandSearchActive,
    commandSearchCompletion,
    reverseSearchActive,
    reverseSearchCompletion,
    completion,
  ]);

  const activeCompletion = getActiveCompletion();
  const shouldShowSuggestions = activeCompletion.showSuggestions;

  const {
    forceShowShellSuggestions,
    setForceShowShellSuggestions,
    isShellSuggestionsVisible,
  } = completion;

  const effectivePlaceholder = useMemo(() => {
    if (!isVoiceModeEnabled) return placeholder;
    const voiceAction =
      (settings.experimental.voice?.activationMode ?? 'push-to-talk') ===
      'push-to-talk'
        ? 'hold space to talk'
        : 'space to talk';
    return `  Type your message or ${voiceAction} (Esc to exit)`;
  }, [
    isVoiceModeEnabled,
    placeholder,
    settings.experimental.voice?.activationMode,
  ]);

  const showCursor =
    focus && isShellFocused && !isEmbeddedShellFocused && !copyModeEnabled;

  useEffect(() => {
    appEvents.emit(AppEvent.ScrollToBottom);
  }, [buffer.text, buffer.cursor]);

  // Notify parent component about escape prompt state changes
  useEffect(() => {
    if (onEscapePromptChange) {
      onEscapePromptChange(showEscapePrompt);
    }
  }, [showEscapePrompt, onEscapePromptChange]);

  // Clear paste timeout on unmount
  useEffect(
    () => () => {
      if (pasteTimeoutRef.current) {
        clearTimeout(pasteTimeoutRef.current);
      }
    },
    [],
  );

  const handleSubmitAndClear = useCallback(
    (submittedValue: string) => {
      let processedValue = submittedValue;
      if (buffer.pastedContent) {
        processedValue = expandPastePlaceholders(
          processedValue,
          buffer.pastedContent,
        );
      }

      if (shellModeActive) {
        shellHistory.addCommandToHistory(processedValue);
      }
      // Clear the buffer *before* calling onSubmit to prevent potential re-submission
      // if onSubmit triggers a re-render while the buffer still holds the old value.
      buffer.setText('');
      resetTurnBaseline();
      onSubmit(processedValue);
      resetCompletionState();
      resetReverseSearchCompletionState();
    },
    [
      buffer,
      onSubmit,
      resetCompletionState,
      shellModeActive,
      shellHistory,
      resetReverseSearchCompletionState,
      resetTurnBaseline,
    ],
  );

  const customSetTextAndResetCompletionSignal = useCallback(
    (newText: string, cursorPosition?: 'start' | 'end' | number) => {
      buffer.setText(newText, cursorPosition);
      setSuppressCompletion(true);
    },
    [buffer, setSuppressCompletion],
  );

  const inputHistory = useInputHistory({
    userMessages,
    onSubmit: handleSubmitAndClear,
    isActive:
      (!(completion.showSuggestions && isShellSuggestionsVisible) ||
        completion.suggestions.length === 1) &&
      !shellModeActive,
    currentQuery: buffer.text,
    currentCursorOffset: buffer.getOffset(),
    onChange: customSetTextAndResetCompletionSignal,
  });

  const handleSubmit = useCallback(
    (submittedValue: string) => {
      const trimmedMessage = submittedValue.trim();
      const isSlash = isSlashCommand(trimmedMessage);

      const isShell = shellModeActive;
      if (
        (isSlash || isShell) &&
        streamingState === StreamingState.Responding
      ) {
        if (isSlash) {
          const { commandToExecute } = parseSlashCommand(
            trimmedMessage,
            slashCommands,
          );
          if (commandToExecute?.isSafeConcurrent) {
            handleSubmitAndClear(trimmedMessage);
            return;
          }
        }

        setQueueErrorMessage(
          `${isShell ? 'Shell' : 'Slash'} commands cannot be queued`,
        );
        return;
      }
      inputHistory.handleSubmit(trimmedMessage);
    },
    [
      inputHistory,
      shellModeActive,
      streamingState,
      setQueueErrorMessage,
      slashCommands,
      handleSubmitAndClear,
    ],
  );

  // Effect to reset completion if history navigation just occurred and set the text
  useEffect(() => {
    if (suppressCompletion) {
      resetCompletionState();
      resetReverseSearchCompletionState();
      resetCommandSearchCompletionState();
      setExpandedSuggestionIndex(-1);
    }
  }, [
    suppressCompletion,
    buffer.text,
    resetCompletionState,
    setSuppressCompletion,
    resetReverseSearchCompletionState,
    resetCommandSearchCompletionState,
    setExpandedSuggestionIndex,
  ]);

  // Helper function to handle loading queued messages into input
  // Returns true if we should continue with input history navigation
  const tryLoadQueuedMessages = useCallback(() => {
    if (buffer.text.trim() === '' && popAllMessages) {
      const allMessages = popAllMessages();
      if (allMessages) {
        buffer.setText(allMessages);
        return true;
      } else {
        // No queued messages, proceed with input history
        inputHistory.navigateUp();
      }
      return true; // We handled the up arrow key
    }
    return false;
  }, [buffer, popAllMessages, inputHistory]);

  // Handle clipboard image pasting with Ctrl+V
  const handleClipboardPaste = useCallback(async () => {
    if (shortcutsHelpVisible) {
      setShortcutsHelpVisible(false);
    }
    try {
      if (await clipboardHasImage()) {
        const imagePath = await saveClipboardImage(config.getTargetDir());
        if (imagePath) {
          // Clean up old images
          cleanupOldClipboardImages(config.getTargetDir()).catch(() => {
            // Ignore cleanup errors
          });

          // Get relative path from current directory
          const relativePath = path.relative(config.getTargetDir(), imagePath);

          // Insert @path reference at cursor position
          const insertText = `@${relativePath}`;
          const currentText = buffer.text;
          const offset = buffer.getOffset();

          // Add spaces around the path if needed
          let textToInsert = insertText;
          const charBefore = offset > 0 ? currentText[offset - 1] : '';
          const charAfter =
            offset < currentText.length ? currentText[offset] : '';

          if (charBefore && charBefore !== ' ' && charBefore !== '\n') {
            textToInsert = ' ' + textToInsert;
          }
          if (!charAfter || (charAfter !== ' ' && charAfter !== '\n')) {
            textToInsert = textToInsert + ' ';
          }

          // Insert at cursor position
          buffer.replaceRangeByOffset(offset, offset, textToInsert);
        }
      }

      if (settings.experimental?.useOSC52Paste) {
        stdout.write('\x1b]52;c;?\x07');
      } else {
        const textToInsert = await clipboardy.read();
        const escapedText = settings.ui?.escapePastedAtSymbols
          ? escapeAtSymbols(textToInsert)
          : textToInsert;
        buffer.insert(escapedText, { paste: true });

        if (isLargePaste(textToInsert)) {
          appEvents.emit(AppEvent.TransientMessage, {
            message: `Press ${formatCommand(Command.EXPAND_PASTE)} to expand pasted text`,
            type: TransientMessageType.Hint,
          });
        }
      }
    } catch (error) {
      debugLogger.error('Error handling paste:', error);
    }
  }, [
    buffer,
    config,
    stdout,
    settings,
    shortcutsHelpVisible,
    setShortcutsHelpVisible,
  ]);

  useMouseClick(
    innerBoxRef,
    (_event, relX, relY) => {
      setSuppressCompletion(true);
      if (isEmbeddedShellFocused) {
        setEmbeddedShellFocused(false);
      }
      const currentScrollTop = Math.round(
        listRef.current?.getScrollState().scrollTop ?? buffer.visualScrollRow,
      );
      const visualRow = currentScrollTop + relY;
      buffer.moveToVisualPosition(visualRow, relX);
    },
    { isActive: focus },
  );

  const isAlternateBuffer = useAlternateBuffer();

  // Double-click to expand/collapse paste placeholders
  useMouseClick(
    innerBoxRef,
    (_event, relX, relY) => {
      if (!isAlternateBuffer) return;

      const currentScrollTop = Math.round(
        listRef.current?.getScrollState().scrollTop ?? buffer.visualScrollRow,
      );
      const visualLine = buffer.allVisualLines[currentScrollTop + relY];
      if (!visualLine) return;

      // Even if we click past the end of the line, we might want to collapse an expanded paste
      const isPastEndOfLine = relX >= stringWidth(visualLine);

      const logicalPos = isPastEndOfLine
        ? null
        : buffer.getLogicalPositionFromVisual(currentScrollTop + relY, relX);

      // Check for paste placeholder (collapsed state)
      if (logicalPos) {
        const transform = getTransformUnderCursor(
          logicalPos.row,
          logicalPos.col,
          buffer.transformationsByLine,
          { includeEdge: true },
        );
        if (transform?.type === 'paste' && transform.id) {
          buffer.togglePasteExpansion(
            transform.id,
            logicalPos.row,
            logicalPos.col,
          );
          return;
        }
      }

      // If we didn't click a placeholder to expand, check if we are inside or after
      // an expanded paste region and collapse it.
      const visualRow = currentScrollTop + relY;
      const mapEntry = buffer.visualToLogicalMap[visualRow];
      const row = mapEntry ? mapEntry[0] : visualRow;
      const expandedId = buffer.getExpandedPasteAtLine(row);
      if (expandedId) {
        buffer.togglePasteExpansion(
          expandedId,
          row,
          logicalPos?.col ?? relX, // Fallback to relX if past end of line
        );
      }
    },
    { isActive: focus, name: 'double-click' },
  );

  useMouse(
    (event: MouseEvent) => {
      if (event.name === 'right-release') {
        setSuppressCompletion(false);
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        handleClipboardPaste();
      }
    },
    { isActive: focus },
  );

  const handleInput = useCallback(
    (key: Key) => {
      if (handleVoiceInput(key)) return true;

      // Determine if this keypress is a history navigation command
      const isHistoryUp =
        !shellModeActive &&
        (keyMatchers[Command.HISTORY_UP](key) ||
          (keyMatchers[Command.NAVIGATION_UP](key) &&
            (buffer.allVisualLines.length === 1 ||
              (buffer.visualCursor[0] === 0 && buffer.visualScrollRow === 0))));
      const isHistoryDown =
        !shellModeActive &&
        (keyMatchers[Command.HISTORY_DOWN](key) ||
          (keyMatchers[Command.NAVIGATION_DOWN](key) &&
            (buffer.allVisualLines.length === 1 ||
              buffer.visualCursor[0] === buffer.allVisualLines.length - 1)));

      const isHistoryNav = isHistoryUp || isHistoryDown;
      const isCursorMovement =
        keyMatchers[Command.MOVE_LEFT](key) ||
        keyMatchers[Command.MOVE_RIGHT](key) ||
        keyMatchers[Command.MOVE_UP](key) ||
        keyMatchers[Command.MOVE_DOWN](key) ||
        keyMatchers[Command.MOVE_WORD_LEFT](key) ||
        keyMatchers[Command.MOVE_WORD_RIGHT](key) ||
        keyMatchers[Command.HOME](key) ||
        keyMatchers[Command.END](key);

      const isSuggestionsNav =
        shouldShowSuggestions &&
        (keyMatchers[Command.COMPLETION_UP](key) ||
          keyMatchers[Command.COMPLETION_DOWN](key) ||
          keyMatchers[Command.EXPAND_SUGGESTION](key) ||
          keyMatchers[Command.COLLAPSE_SUGGESTION](key) ||
          keyMatchers[Command.ACCEPT_SUGGESTION](key));

      // Reset completion suppression if the user performs any action other than
      // history navigation or cursor movement.
      // We explicitly skip this if we are currently navigating suggestions.
      if (!isSuggestionsNav) {
        setSuppressCompletion(
          isHistoryNav || isCursorMovement || keyMatchers[Command.ESCAPE](key),
        );
        hasUserNavigatedSuggestions.current = false;

        if (key.name !== 'tab') {
          setForceShowShellSuggestions(false);
        }
      }

      // TODO(jacobr): this special case is likely not needed anymore.
      // We should probably stop supporting paste if the InputPrompt is not
      // focused.
      /// We want to handle paste even when not focused to support drag and drop.
      if (!focus && key.name !== 'paste') {
        return false;
      }

      // Handle escape to close shortcuts panel first, before letting it bubble
      // up for cancellation. This ensures pressing Escape once closes the panel,
      // and pressing again cancels the operation.
      if (shortcutsHelpVisible && key.name === 'escape') {
        setShortcutsHelpVisible(false);
        return true;
      }

      const isGenerating =
        streamingState === StreamingState.Responding ||
        streamingState === StreamingState.WaitingForConfirmation;

      const isQueueMessageKey = keyMatchers[Command.QUEUE_MESSAGE](key);
      const isPlainTab =
        key.name === 'tab' && !key.shift && !key.alt && !key.ctrl && !key.cmd;
      const hasTabCompletionInteraction =
        (completion.showSuggestions && isShellSuggestionsVisible) ||
        Boolean(completion.promptCompletion.text) ||
        reverseSearchActive ||
        commandSearchActive;

      if (
        isGenerating &&
        isQueueMessageKey &&
        !hasTabCompletionInteraction &&
        buffer.text.trim().length > 0
      ) {
        const trimmedMessage = buffer.text.trim();
        const isSlash = isSlashCommand(trimmedMessage);

        if (isSlash || shellModeActive) {
          setQueueErrorMessage(
            `${shellModeActive ? 'Shell' : 'Slash'} commands cannot be queued`,
          );
        } else if (onQueueMessage) {
          onQueueMessage(buffer.text);
          buffer.setText('');
          resetCompletionState();
          resetReverseSearchCompletionState();
        }
        resetPlainTabPress();
        return true;
      }

      if (isPlainTab && shellModeActive) {
        resetPlainTabPress();
        if (!shouldShowSuggestions) {
          setSuppressCompletion(false);
          if (completion.promptCompletion.text) {
            completion.promptCompletion.accept();
            return true;
          } else if (
            completion.suggestions.length > 0 &&
            !forceShowShellSuggestions
          ) {
            setForceShowShellSuggestions(true);
            return true;
          }
        }
      } else if (isPlainTab) {
        if (!hasTabCompletionInteraction) {
          if (registerPlainTabPress() === 2) {
            toggleCleanUiDetailsVisible();
            resetPlainTabPress();
            return true;
          }
        } else {
          resetPlainTabPress();
        }
      } else {
        resetPlainTabPress();
      }

      if (key.name === 'paste') {
        if (shortcutsHelpVisible) {
          setShortcutsHelpVisible(false);
        }
        // Record paste time to prevent accidental auto-submission
        if (!isTerminalPasteTrusted(kittyProtocol.enabled)) {
          setRecentUnsafePasteTime(Date.now());

          // Clear any existing paste timeout
          if (pasteTimeoutRef.current) {
            clearTimeout(pasteTimeoutRef.current);
          }

          // Clear the paste protection after a very short delay to prevent
          // false positives.
          // Due to how we use a reducer for text buffer state updates, it is
          // reasonable to expect that key events that are really part of the
          // same paste will be processed in the same event loop tick. 40ms
          // is chosen arbitrarily as it is faster than a typical human
          // could go from pressing paste to pressing enter. The fastest typists
          // can type at 200 words per minute which roughly translates to 50ms
          // per letter.
          pasteTimeoutRef.current = setTimeout(() => {
            setRecentUnsafePasteTime(null);
            pasteTimeoutRef.current = null;
          }, 40);
        }
        if (settings.ui?.escapePastedAtSymbols) {
          buffer.handleInput({
            ...key,
            sequence: escapeAtSymbols(key.sequence || ''),
          });
        } else {
          buffer.handleInput(key);
        }

        if (key.sequence && isLargePaste(key.sequence)) {
          appEvents.emit(AppEvent.TransientMessage, {
            message: `Press ${formatCommand(Command.EXPAND_PASTE)} to expand pasted text`,
            type: TransientMessageType.Hint,
          });
        }
        return true;
      }

      if (shortcutsHelpVisible && isHelpDismissKey(key)) {
        setShortcutsHelpVisible(false);
      }

      if (shortcutsHelpVisible) {
        if (key.sequence === '?' && key.insertable) {
          setShortcutsHelpVisible(false);
          buffer.handleInput(key);
          return true;
        }
        // Escape is handled earlier to ensure it closes the panel before
        // potentially cancelling an operation
        if (key.name === 'backspace' || key.sequence === '\b') {
          setShortcutsHelpVisible(false);
          return true;
        }
        if (key.insertable) {
          setShortcutsHelpVisible(false);
        }
      }

      if (
        key.sequence === '?' &&
        key.insertable &&
        !shortcutsHelpVisible &&
        buffer.text.length === 0
      ) {
        setShortcutsHelpVisible(true);
        return true;
      }

      if (vimHandleInput && vimHandleInput(key)) {
        return true;
      }

      // Reset ESC count and hide prompt on any non-ESC key
      if (key.name !== 'escape') {
        resetEscapeState();
      }

      // Ctrl+O to expand/collapse paste placeholders
      if (keyMatchers[Command.EXPAND_PASTE](key)) {
        const handled = tryTogglePasteExpansion(buffer);
        if (handled) return true;
      }

      if (
        key.sequence === '!' &&
        buffer.text === '' &&
        !(completion.showSuggestions && isShellSuggestionsVisible)
      ) {
        setShellModeActive(!shellModeActive);
        buffer.setText(''); // Clear the '!' from input
        resetTurnBaseline();
        return true;
      }
      if (keyMatchers[Command.ESCAPE](key)) {
        const cancelSearch = (
          setActive: (active: boolean) => void,
          resetCompletion: () => void,
        ) => {
          setActive(false);
          resetCompletion();
          buffer.setText(textBeforeReverseSearch);
          const offset = logicalPosToOffset(
            buffer.lines,
            cursorPosition[0],
            cursorPosition[1],
          );
          buffer.moveToOffset(offset);
          setExpandedSuggestionIndex(-1);
        };

        if (reverseSearchActive) {
          cancelSearch(
            setReverseSearchActive,
            reverseSearchCompletion.resetCompletionState,
          );
          return true;
        }
        if (commandSearchActive) {
          cancelSearch(
            setCommandSearchActive,
            commandSearchCompletion.resetCompletionState,
          );
          return true;
        }

        if (completion.showSuggestions && isShellSuggestionsVisible) {
          completion.resetCompletionState();
          setExpandedSuggestionIndex(-1);
          resetEscapeState();
          return true;
        }

        if (shellModeActive) {
          setShellModeActive(false);
          resetEscapeState();
          return true;
        }

        // If we're generating and no local overlay consumed Escape, let it
        // propagate to the global cancellation handler.
        if (isGenerating) {
          return false;
        }

        handleEscPress();
        return true;
      }

      if (keyMatchers[Command.CLEAR_SCREEN](key)) {
        setBannerVisible(false);
        onClearScreen();
        return true;
      }

      if (shellModeActive && keyMatchers[Command.REVERSE_SEARCH](key)) {
        setReverseSearchActive(true);
        setTextBeforeReverseSearch(buffer.text);
        setCursorPosition(buffer.cursor);
        return true;
      }

      if (reverseSearchActive || commandSearchActive) {
        const isCommandSearch = commandSearchActive;

        const sc = isCommandSearch
          ? commandSearchCompletion
          : reverseSearchCompletion;

        const {
          activeSuggestionIndex,
          navigateUp,
          navigateDown,
          showSuggestions,
          suggestions,
        } = sc;
        const setActive = isCommandSearch
          ? setCommandSearchActive
          : setReverseSearchActive;
        const resetState = sc.resetCompletionState;

        if (showSuggestions) {
          if (keyMatchers[Command.NAVIGATION_UP](key)) {
            navigateUp();
            return true;
          }
          if (keyMatchers[Command.NAVIGATION_DOWN](key)) {
            navigateDown();
            return true;
          }
          if (keyMatchers[Command.COLLAPSE_SUGGESTION](key)) {
            if (suggestions[activeSuggestionIndex].value.length >= MAX_WIDTH) {
              setExpandedSuggestionIndex(-1);
              return true;
            }
          }
          if (keyMatchers[Command.EXPAND_SUGGESTION](key)) {
            if (suggestions[activeSuggestionIndex].value.length >= MAX_WIDTH) {
              setExpandedSuggestionIndex(activeSuggestionIndex);
              return true;
            }
          }
          if (keyMatchers[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH](key)) {
            sc.handleAutocomplete(activeSuggestionIndex);
            resetState();
            setActive(false);
            return true;
          }
        }

        if (keyMatchers[Command.SUBMIT_REVERSE_SEARCH](key)) {
          const textToSubmit =
            showSuggestions && activeSuggestionIndex > -1
              ? suggestions[activeSuggestionIndex].value
              : buffer.text;
          handleSubmit(textToSubmit);
          resetState();
          setActive(false);
          return true;
        }

        // Prevent up/down from falling through to regular history navigation
        if (
          keyMatchers[Command.NAVIGATION_UP](key) ||
          keyMatchers[Command.NAVIGATION_DOWN](key)
        ) {
          return true;
        }
      }

      // If the command is a perfect match, pressing enter should execute it.
      // We prioritize execution unless the user is explicitly selecting a different suggestion.
      if (
        completion.isPerfectMatch &&
        keyMatchers[Command.SUBMIT](key) &&
        recentUnsafePasteTime === null &&
        (!(completion.showSuggestions && isShellSuggestionsVisible) ||
          (completion.activeSuggestionIndex <= 0 &&
            !hasUserNavigatedSuggestions.current))
      ) {
        handleSubmit(buffer.text);
        return true;
      }

      // Newline insertion
      if (keyMatchers[Command.NEWLINE](key)) {
        buffer.newline();
        return true;
      }

      if (completion.showSuggestions && isShellSuggestionsVisible) {
        if (completion.suggestions.length > 1) {
          if (keyMatchers[Command.COMPLETION_UP](key)) {
            completion.navigateUp();
            hasUserNavigatedSuggestions.current = true;
            setExpandedSuggestionIndex(-1); // Reset expansion when navigating
            return true;
          }
          if (keyMatchers[Command.COMPLETION_DOWN](key)) {
            completion.navigateDown();
            hasUserNavigatedSuggestions.current = true;
            setExpandedSuggestionIndex(-1); // Reset expansion when navigating
            return true;
          }
        }

        if (keyMatchers[Command.ACCEPT_SUGGESTION](key)) {
          if (completion.suggestions.length > 0) {
            const targetIndex =
              completion.activeSuggestionIndex === -1
                ? 0 // Default to the first if none is active
                : completion.activeSuggestionIndex;

            if (targetIndex < completion.suggestions.length) {
              const suggestion = completion.suggestions[targetIndex];

              const isEnterKey = key.name === 'enter' && !key.ctrl;

              if (isEnterKey && shellModeActive) {
                if (hasUserNavigatedSuggestions.current) {
                  completion.handleAutocomplete(
                    completion.activeSuggestionIndex,
                  );
                  setExpandedSuggestionIndex(-1);
                  hasUserNavigatedSuggestions.current = false;
                  return true;
                }
                completion.resetCompletionState();
                setExpandedSuggestionIndex(-1);
                hasUserNavigatedSuggestions.current = false;
                if (buffer.text.trim()) {
                  handleSubmit(buffer.text);
                }
                return true;
              }

              if (isEnterKey && buffer.text.startsWith('/')) {
                if (suggestion.submitValue) {
                  setExpandedSuggestionIndex(-1);
                  handleSubmit(suggestion.submitValue.trim());
                  return true;
                }

                const { isArgumentCompletion, leafCommand } =
                  completion.slashCompletionRange;

                if (
                  isArgumentCompletion &&
                  isAutoExecutableCommand(leafCommand)
                ) {
                  // isArgumentCompletion guarantees leafCommand exists
                  const completedText = completion.getCompletedText(suggestion);
                  if (completedText) {
                    setExpandedSuggestionIndex(-1);
                    handleSubmit(completedText.trim());
                    return true;
                  }
                } else if (!isArgumentCompletion) {
                  // Existing logic for command name completion
                  const command =
                    completion.getCommandFromSuggestion(suggestion);

                  // Only auto-execute if the command has no completion function
                  // (i.e., it doesn't require an argument to be selected)
                  if (
                    command &&
                    isAutoExecutableCommand(command) &&
                    !command.completion
                  ) {
                    const completedText =
                      completion.getCompletedText(suggestion);

                    if (completedText) {
                      setExpandedSuggestionIndex(-1);
                      handleSubmit(completedText.trim());
                      return true;
                    }
                  }
                }
              }

              // Default behavior: auto-complete to prompt box
              completion.handleAutocomplete(targetIndex);
              setExpandedSuggestionIndex(-1); // Reset expansion after selection
            }
          }
          return true;
        }
      }

      // Handle Tab key for ghost text acceptance
      if (
        key.name === 'tab' &&
        !key.shift &&
        !(completion.showSuggestions && isShellSuggestionsVisible) &&
        completion.promptCompletion.text
      ) {
        completion.promptCompletion.accept();
        return true;
      }

      if (!shellModeActive) {
        if (keyMatchers[Command.REVERSE_SEARCH](key)) {
          setCommandSearchActive(true);
          setTextBeforeReverseSearch(buffer.text);
          setCursorPosition(buffer.cursor);
          return true;
        }

        if (isHistoryUp) {
          if (
            keyMatchers[Command.NAVIGATION_UP](key) &&
            buffer.visualCursor[1] > 0
          ) {
            buffer.move('home');
            return true;
          }
          // Check for queued messages first when input is empty
          // If no queued messages, inputHistory.navigateUp() is called inside tryLoadQueuedMessages
          if (tryLoadQueuedMessages()) {
            return true;
          }
          // Only navigate history if popAllMessages doesn't exist
          inputHistory.navigateUp();
          return true;
        }
        if (isHistoryDown) {
          if (
            keyMatchers[Command.NAVIGATION_DOWN](key) &&
            buffer.visualCursor[1] <
              cpLen(buffer.allVisualLines[buffer.visualCursor[0]] || '')
          ) {
            buffer.move('end');
            return true;
          }
          inputHistory.navigateDown();
          return true;
        }
      } else {
        // Shell History Navigation
        if (keyMatchers[Command.NAVIGATION_UP](key)) {
          if (
            (buffer.allVisualLines.length === 1 ||
              (buffer.visualCursor[0] === 0 && buffer.visualScrollRow === 0)) &&
            buffer.visualCursor[1] > 0
          ) {
            buffer.move('home');
            return true;
          }
          const prevCommand = shellHistory.getPreviousCommand();
          if (prevCommand !== null) buffer.setText(prevCommand);
          return true;
        }
        if (keyMatchers[Command.NAVIGATION_DOWN](key)) {
          if (
            (buffer.allVisualLines.length === 1 ||
              buffer.visualCursor[0] === buffer.allVisualLines.length - 1) &&
            buffer.visualCursor[1] <
              cpLen(buffer.allVisualLines[buffer.visualCursor[0]] || '')
          ) {
            buffer.move('end');
            return true;
          }
          const nextCommand = shellHistory.getNextCommand();
          if (nextCommand !== null) buffer.setText(nextCommand);
          return true;
        }
      }

      if (keyMatchers[Command.SUBMIT](key)) {
        if (buffer.text.trim()) {
          // Check if a paste operation occurred recently to prevent accidental auto-submission
          if (recentUnsafePasteTime !== null) {
            // Paste occurred recently in a terminal where we don't trust pastes
            // to be reported correctly so assume this paste was really a
            // newline that was part of the paste.
            // This has the added benefit that in the worst case at least users
            // get some feedback that their keypress was handled rather than
            // wondering why it was completely ignored.
            buffer.newline();
            return true;
          }

          const [row, col] = buffer.cursor;
          const line = buffer.lines[row];
          const charBefore = col > 0 ? cpSlice(line, col - 1, col) : '';
          if (charBefore === '\\') {
            buffer.backspace();
            buffer.newline();
          } else {
            handleSubmit(buffer.text);
          }
        }
        return true;
      }

      // Ctrl+A (Home) / Ctrl+E (End)
      if (keyMatchers[Command.HOME](key)) {
        buffer.move('home');
        return true;
      }
      if (keyMatchers[Command.END](key)) {
        buffer.move('end');
        return true;
      }

      // Kill line commands
      if (keyMatchers[Command.KILL_LINE_RIGHT](key)) {
        buffer.killLineRight();
        return true;
      }
      if (keyMatchers[Command.KILL_LINE_LEFT](key)) {
        buffer.killLineLeft();
        return true;
      }

      if (keyMatchers[Command.DELETE_WORD_BACKWARD](key)) {
        buffer.deleteWordLeft();
        return true;
      }

      // External editor
      if (keyMatchers[Command.OPEN_EXTERNAL_EDITOR](key)) {
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        buffer.openInExternalEditor();
        return true;
      }

      if (keyMatchers[Command.DEPRECATED_OPEN_EXTERNAL_EDITOR](key)) {
        const cmdKey = formatCommand(Command.OPEN_EXTERNAL_EDITOR);
        appEvents.emit(AppEvent.TransientMessage, {
          message: `Use ${cmdKey} to open the external editor.`,
          type: TransientMessageType.Hint,
        });
        return true;
      }

      // Ctrl+V for clipboard paste
      if (keyMatchers[Command.PASTE_CLIPBOARD](key)) {
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        handleClipboardPaste();
        return true;
      }

      if (keyMatchers[Command.TOGGLE_BACKGROUND_SHELL](key)) {
        return false;
      }

      if (keyMatchers[Command.FOCUS_SHELL_INPUT](key)) {
        if (
          activePtyId ||
          (backgroundTasks.size > 0 && backgroundTaskHeight > 0)
        ) {
          setEmbeddedShellFocused(true);
          return true;
        }
        return false;
      }

      // Fall back to the text buffer's default input handling for all other keys
      const handled = buffer.handleInput(key);

      if (handled) {
        if (keyMatchers[Command.CLEAR_INPUT](key)) {
          resetCompletionState();
        }

        // Clear ghost text when user types regular characters (not navigation/control keys)
        if (
          completion.promptCompletion.text &&
          key.sequence &&
          key.sequence.length === 1 &&
          !key.alt &&
          !key.ctrl &&
          !key.cmd
        ) {
          completion.promptCompletion.clear();
          setExpandedSuggestionIndex(-1);
        }
      }
      return handled;
    },
    [
      focus,
      buffer,
      completion,
      setForceShowShellSuggestions,
      shellModeActive,
      setShellModeActive,
      onClearScreen,
      inputHistory,
      handleSubmit,
      shellHistory,
      reverseSearchCompletion,
      handleClipboardPaste,
      resetCompletionState,
      resetEscapeState,
      vimHandleInput,
      reverseSearchActive,
      textBeforeReverseSearch,
      cursorPosition,
      recentUnsafePasteTime,
      commandSearchActive,
      commandSearchCompletion,
      kittyProtocol.enabled,
      shortcutsHelpVisible,
      setShortcutsHelpVisible,
      tryLoadQueuedMessages,
      onQueueMessage,
      setQueueErrorMessage,
      resetReverseSearchCompletionState,
      setBannerVisible,
      activePtyId,
      setEmbeddedShellFocused,
      backgroundTasks.size,
      backgroundTaskHeight,
      streamingState,
      handleEscPress,
      resetTurnBaseline,
      registerPlainTabPress,
      resetPlainTabPress,
      toggleCleanUiDetailsVisible,
      shouldShowSuggestions,
      isShellSuggestionsVisible,
      forceShowShellSuggestions,
      keyMatchers,
      isHelpDismissKey,
      settings,
      handleVoiceInput,
    ],
  );
  useKeypress(handleInput, {
    isActive: !isEmbeddedShellFocused && !copyModeEnabled,
    priority: true,
  });

  const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
    buffer.visualCursor;

  const getGhostTextLines = useCallback(() => {
    if (
      !completion.promptCompletion.text ||
      !buffer.text ||
      !completion.promptCompletion.text.startsWith(buffer.text)
    ) {
      return { inlineGhost: '', additionalLines: [] };
    }

    const ghostSuffix = completion.promptCompletion.text.slice(
      buffer.text.length,
    );
    if (!ghostSuffix) {
      return { inlineGhost: '', additionalLines: [] };
    }

    const currentLogicalLine = buffer.lines[buffer.cursor[0]] || '';
    const cursorCol = buffer.cursor[1];

    const textBeforeCursor = cpSlice(currentLogicalLine, 0, cursorCol);
    const usedWidth = stringWidth(textBeforeCursor);
    const remainingWidth = Math.max(0, inputWidth - usedWidth);

    const ghostTextLinesRaw = ghostSuffix.split('\n');
    const firstLineRaw = ghostTextLinesRaw.shift() || '';

    let inlineGhost = '';
    let remainingFirstLine = '';

    if (stringWidth(firstLineRaw) <= remainingWidth) {
      inlineGhost = firstLineRaw;
    } else {
      const words = firstLineRaw.split(' ');
      let currentLine = '';
      let wordIdx = 0;
      for (const word of words) {
        const prospectiveLine = currentLine ? `${currentLine} ${word}` : word;
        if (stringWidth(prospectiveLine) > remainingWidth) {
          break;
        }
        currentLine = prospectiveLine;
        wordIdx++;
      }
      inlineGhost = currentLine;
      if (words.length > wordIdx) {
        remainingFirstLine = words.slice(wordIdx).join(' ');
      }
    }

    const linesToWrap = [];
    if (remainingFirstLine) {
      linesToWrap.push(remainingFirstLine);
    }
    linesToWrap.push(...ghostTextLinesRaw);
    const remainingGhostText = linesToWrap.join('\n');

    const additionalLines: string[] = [];
    if (remainingGhostText) {
      const textLines = remainingGhostText.split('\n');
      for (const textLine of textLines) {
        const words = textLine.split(' ');
        let currentLine = '';

        for (const word of words) {
          const prospectiveLine = currentLine ? `${currentLine} ${word}` : word;
          const prospectiveWidth = stringWidth(prospectiveLine);

          if (prospectiveWidth > inputWidth) {
            if (currentLine) {
              additionalLines.push(currentLine);
            }

            let wordToProcess = word;
            while (stringWidth(wordToProcess) > inputWidth) {
              let part = '';
              const wordCP = toCodePoints(wordToProcess);
              let partWidth = 0;
              let splitIndex = 0;
              for (let i = 0; i < wordCP.length; i++) {
                const char = wordCP[i];
                const charWidth = stringWidth(char);
                if (partWidth + charWidth > inputWidth) {
                  break;
                }
                part += char;
                partWidth += charWidth;
                splitIndex = i + 1;
              }
              additionalLines.push(part);
              wordToProcess = cpSlice(wordToProcess, splitIndex);
            }
            currentLine = wordToProcess;
          } else {
            currentLine = prospectiveLine;
          }
        }
        if (currentLine) {
          additionalLines.push(currentLine);
        }
      }
    }

    return { inlineGhost, additionalLines };
  }, [
    completion.promptCompletion.text,
    buffer.text,
    buffer.lines,
    buffer.cursor,
    inputWidth,
  ]);

  const { inlineGhost, additionalLines } = getGhostTextLines();

  const scrollableData = useMemo(() => {
    const items: ScrollableItem[] = buffer.allVisualLines.map(
      (lineText, index) => ({
        type: 'visualLine',
        lineText,
        absoluteVisualIdx: index,
      }),
    );

    additionalLines.forEach((ghostLine, index) => {
      items.push({
        type: 'ghostLine',
        ghostLine,
        index,
      });
    });

    return items;
  }, [buffer.allVisualLines, additionalLines]);

  const renderItem = useCallback(
    ({ item }: { item: ScrollableItem; index: number }) => {
      if (item.type === 'ghostLine') {
        const padding = Math.max(0, inputWidth - stringWidth(item.ghostLine));
        return (
          <Box height={1}>
            <Text color={theme.text.secondary}>
              {item.ghostLine}
              {' '.repeat(padding)}
            </Text>
          </Box>
        );
      }

      const { lineText, absoluteVisualIdx } = item;
      // console.log('renderItem called with:', lineText);
      const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx];
      if (!mapEntry) return <Text> </Text>;

      const isOnCursorLine =
        focus && absoluteVisualIdx === cursorVisualRowAbsolute;
      const renderedLine: React.ReactNode[] = [];
      const [logicalLineIdx] = mapEntry;
      const logicalLine = buffer.lines[logicalLineIdx] || '';
      const transformations =
        buffer.transformationsByLine[logicalLineIdx] ?? [];
      const tokens = parseInputForHighlighting(
        logicalLine,
        logicalLineIdx,
        transformations,
        ...(focus && buffer.cursor[0] === logicalLineIdx
          ? [buffer.cursor[1]]
          : []),
      );
      const visualStartCol =
        buffer.visualToTransformedMap[absoluteVisualIdx] ?? 0;
      const visualEndCol = visualStartCol + cpLen(lineText);
      const segments = parseSegmentsFromTokens(
        tokens,
        visualStartCol,
        visualEndCol,
      );
      let charCount = 0;
      segments.forEach((seg, segIdx) => {
        const segLen = cpLen(seg.text);
        let display = seg.text;
        if (isOnCursorLine) {
          const relCol = cursorVisualColAbsolute;
          const segStart = charCount;
          const segEnd = segStart + segLen;
          if (relCol >= segStart && relCol < segEnd) {
            const charToHighlight = cpSlice(
              display,
              relCol - segStart,
              relCol - segStart + 1,
            );
            const highlighted = showCursor
              ? chalk.inverse(charToHighlight)
              : charToHighlight;
            display =
              cpSlice(display, 0, relCol - segStart) +
              highlighted +
              cpSlice(display, relCol - segStart + 1);
          }
          charCount = segEnd;
        } else {
          charCount += segLen;
        }
        const color =
          seg.type === 'command' || seg.type === 'file' || seg.type === 'paste'
            ? theme.text.accent
            : theme.text.primary;
        renderedLine.push(
          <Text key={`token-${segIdx}`} color={color}>
            {display}
          </Text>,
        );
      });

      const currentLineGhost = isOnCursorLine ? inlineGhost : '';
      if (
        isOnCursorLine &&
        cursorVisualColAbsolute === cpLen(lineText) &&
        !currentLineGhost
      ) {
        renderedLine.push(
          <Text key={`cursor-end-${cursorVisualColAbsolute}`}>
            {showCursor ? chalk.inverse(' ') : ' '}
          </Text>,
        );
      }
      const showCursorBeforeGhost =
        focus &&
        isOnCursorLine &&
        cursorVisualColAbsolute === cpLen(lineText) &&
        currentLineGhost;
      return (
        <Box height={1}>
          <Text
            terminalCursorFocus={showCursor && isOnCursorLine}
            terminalCursorPosition={cpIndexToOffset(
              lineText,
              cursorVisualColAbsolute,
            )}
          >
            {renderedLine}
            {showCursorBeforeGhost && (showCursor ? chalk.inverse(' ') : ' ')}
            {currentLineGhost && (
              <Text color={theme.text.secondary}>{currentLineGhost}</Text>
            )}
          </Text>
        </Box>
      );
    },
    [
      buffer.visualToLogicalMap,
      buffer.lines,
      buffer.transformationsByLine,
      buffer.cursor,
      buffer.visualToTransformedMap,
      focus,
      cursorVisualRowAbsolute,
      cursorVisualColAbsolute,
      showCursor,
      inlineGhost,
      inputWidth,
    ],
  );

  const useBackgroundColor = config.getUseBackgroundColor();

  const prevCursorRef = useRef(buffer.visualCursor);
  const prevTextRef = useRef(buffer.text);

  // Effect to ensure cursor remains visible after interactions
  useEffect(() => {
    const cursorChanged = prevCursorRef.current !== buffer.visualCursor;
    const textChanged = prevTextRef.current !== buffer.text;

    prevCursorRef.current = buffer.visualCursor;
    prevTextRef.current = buffer.text;

    if (!cursorChanged && !textChanged) return;

    if (!listRef.current || !focus) return;
    const { scrollTop, innerHeight } = listRef.current.getScrollState();
    if (innerHeight === 0) return;

    const cursorVisualRow = buffer.visualCursor[0];
    const actualScrollTop = Math.round(scrollTop);

    // If cursor is out of the currently visible viewport...
    if (
      cursorVisualRow < actualScrollTop ||
      cursorVisualRow >= actualScrollTop + innerHeight
    ) {
      // Calculate minimal scroll to make it visible
      let newScrollTop = actualScrollTop;
      if (cursorVisualRow < actualScrollTop) {
        newScrollTop = cursorVisualRow;
      } else if (cursorVisualRow >= actualScrollTop + innerHeight) {
        newScrollTop = cursorVisualRow - innerHeight + 1;
      }

      listRef.current.scrollToIndex({ index: newScrollTop });
    }
  }, [buffer.visualCursor, buffer.text, focus]);

  const listBackgroundColor = !useBackgroundColor
    ? undefined
    : theme.background.input;

  const useLineFallback = !!process.env['NO_COLOR'];

  useEffect(() => {
    if (onSuggestionsVisibilityChange) {
      onSuggestionsVisibilityChange(shouldShowSuggestions);
    }
  }, [shouldShowSuggestions, onSuggestionsVisibilityChange]);

  const showAutoAcceptStyling =
    !shellModeActive && approvalMode === ApprovalMode.AUTO_EDIT;
  const showYoloStyling =
    !shellModeActive && approvalMode === ApprovalMode.YOLO;
  const showPlanStyling =
    !shellModeActive && approvalMode === ApprovalMode.PLAN;

  let statusColor: string | undefined;
  let statusText = '';
  if (shellModeActive) {
    statusColor = theme.ui.symbol;
    statusText = 'Shell mode';
  } else if (showYoloStyling) {
    statusColor = theme.status.error;
    statusText = 'YOLO mode';
  } else if (showPlanStyling) {
    statusColor = theme.status.success;
    statusText = 'Plan mode';
  } else if (showAutoAcceptStyling) {
    statusColor = theme.status.warning;
    statusText = 'Accepting edits';
  }

  const suggestionsNode = shouldShowSuggestions ? (
    <Box paddingRight={2}>
      <SuggestionsDisplay
        suggestions={activeCompletion.suggestions}
        activeIndex={activeCompletion.activeSuggestionIndex}
        isLoading={activeCompletion.isLoadingSuggestions}
        width={suggestionsWidth}
        scrollOffset={activeCompletion.visibleStartIndex}
        userInput={buffer.text}
        mode={
          completion.completionMode === CompletionMode.AT ||
          completion.completionMode === CompletionMode.SHELL
            ? 'reverse'
            : buffer.text.startsWith('/') &&
                !reverseSearchActive &&
                !commandSearchActive
              ? 'slash'
              : 'reverse'
        }
        expandedIndex={expandedSuggestionIndex}
      />
    </Box>
  ) : null;

  const borderColor =
    isShellFocused && !isEmbeddedShellFocused
      ? (statusColor ?? theme.ui.focus)
      : theme.border.default;

  return (
    <>
      {suggestionsPosition === 'above' && suggestionsNode}
      {useLineFallback || !useBackgroundColor ? (
        <Box
          borderStyle="round"
          borderTop={true}
          borderBottom={false}
          borderLeft={false}
          borderRight={false}
          borderColor={borderColor}
          width={terminalWidth}
          flexDirection="row"
          alignItems="flex-start"
          height={0}
        />
      ) : null}
      <HalfLinePaddedBox
        backgroundBaseColor={theme.background.input}
        backgroundOpacity={1}
        useBackgroundColor={useBackgroundColor}
      >
        <Box flexGrow={1} flexDirection="row" paddingX={1}>
          {isVoiceModeEnabled &&
            (isRecording ? (
              <ListeningIndicator color={theme.text.accent} />
            ) : (
              <Text color={theme.text.accent}>🎤 </Text>
            ))}
          <Text
            color={statusColor ?? theme.text.accent}
            aria-label={statusText || undefined}
          >
            {shellModeActive ? (
              reverseSearchActive ? (
                <Text
                  color={theme.text.link}
                  aria-label={SCREEN_READER_USER_PREFIX}
                >
                  (r:){' '}
                </Text>
              ) : (
                '!'
              )
            ) : commandSearchActive ? (
              <Text color={theme.text.accent}>(r:) </Text>
            ) : showYoloStyling ? (
              '*'
            ) : (
              '>'
            )}{' '}
          </Text>
          <Box flexGrow={1} flexDirection="column" ref={innerBoxRef}>
            {buffer.text.length === 0 ? (
              effectivePlaceholder ? (
                showCursor ? (
                  <Text
                    terminalCursorFocus={showCursor}
                    terminalCursorPosition={0}
                  >
                    {chalk.inverse(effectivePlaceholder.slice(0, 1))}
                    <Text color={theme.text.secondary}>
                      {effectivePlaceholder.slice(1)}
                    </Text>
                  </Text>
                ) : (
                  <Text color={theme.text.secondary}>
                    {effectivePlaceholder}
                  </Text>
                )
              ) : null
            ) : (
              <Box
                flexDirection="column"
                height={Math.min(buffer.viewportHeight, scrollableData.length)}
                width="100%"
              >
                {config.getUseTerminalBuffer() ? (
                  <ScrollableList
                    ref={listRef}
                    hasFocus={focus}
                    data={scrollableData}
                    renderItem={renderItem}
                    estimatedItemHeight={() => 1}
                    fixedItemHeight={true}
                    keyExtractor={(item) =>
                      item.type === 'visualLine'
                        ? `line-${item.absoluteVisualIdx}`
                        : `ghost-${item.index}`
                    }
                    width={inputWidth + SCROLLBAR_GUTTER_WIDTH}
                    backgroundColor={listBackgroundColor}
                    containerHeight={Math.min(
                      buffer.viewportHeight,
                      scrollableData.length,
                    )}
                  />
                ) : (
                  scrollableData
                    .slice(
                      buffer.visualScrollRow,
                      buffer.visualScrollRow + buffer.viewportHeight,
                    )
                    .map((item, index) => {
                      const actualIndex = buffer.visualScrollRow + index;
                      const key =
                        item.type === 'visualLine'
                          ? `line-${item.absoluteVisualIdx}`
                          : `ghost-${item.index}`;
                      return (
                        <Fragment key={key}>
                          {renderItem({ item, index: actualIndex })}
                        </Fragment>
                      );
                    })
                )}
              </Box>
            )}
          </Box>
        </Box>
      </HalfLinePaddedBox>
      {useLineFallback || !useBackgroundColor ? (
        <Box
          borderStyle="round"
          borderTop={false}
          borderBottom={true}
          borderLeft={false}
          borderRight={false}
          borderColor={borderColor}
          width={terminalWidth}
          flexDirection="row"
          alignItems="flex-start"
          height={0}
        />
      ) : null}
      {suggestionsPosition === 'below' && suggestionsNode}
    </>
  );
};
