// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0

'use client'

import React, { useEffect, useCallback, useMemo, useState, useRef } from 'react'
import { Command, MessageSquareText, ShieldX, X } from 'lucide-react'
import { useSearchParams } from 'next/navigation'
import MessagesArea from '../message/MessagesArea'
import { QuickAccessCards } from './QuickAccessCards'
import { SloganDisplay } from './SloganDisplay'
import { ChatInputCard } from '../input/ChatInputCard'
import PipelineStageIndicator from './PipelineStageIndicator'
import PipelineNextStepDialog from './PipelineNextStepDialog'
import {
  buildPipelineNextStepDraft,
  type PipelineNextStepMessage,
  type PipelineNextStepPayload,
} from './pipelineNextStep'
import { ScrollToBottomIndicator } from './ScrollToBottomIndicator'
import { ScrollbarMarkers } from './ScrollbarMarkers'
import { GuidedQuestions } from '@/features/knowledge/document/components/GuidedQuestions'
import type { PipelineStageInfo } from '@/apis/tasks'
import { useChatAreaState } from './useChatAreaState'
import { useChatStreamHandlers } from './useChatStreamHandlers'
import { allBotsHavePredefinedModel } from '../selector/ModelSelector'
import { QuoteProvider, SelectionTooltip, useQuote } from '../text-selection'
import type { Team, SubtaskContextBrief, TaskType } from '@/types/api'
import type { Model } from '../../hooks/useModelSelection'
import type { ContextItem, QueueMessageContext } from '@/types/context'
import { useTranslation } from '@/hooks/useTranslation'
import { useRouter } from 'next/navigation'
import { useTaskContext } from '../../contexts/taskContext'
import { useTaskStateMachine } from '../../hooks/useTaskStateMachine'
import { useOptionalChatStreamContext } from '../../contexts/chatStreamContext'
import { Button } from '@/components/ui/button'
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { useToast } from '@/hooks/use-toast'
import { useScrollManagement } from '../hooks/useScrollManagement'
import { useFloatingInput } from '../hooks/useFloatingInput'
import { getAttachment } from '@/apis/attachments'
import { useAttachmentUpload } from '../hooks/useAttachmentUpload'
import { useSchemeMessageActions } from '@/lib/scheme'
import { QueryParamAutoSend } from '../params'
import { useSkillSelector } from '../../hooks/useSkillSelector'
import { useModelSelection } from '../../hooks/useModelSelection'
import { QueueMessageHandler } from '@/features/inbox'
import type { ChatAreaExtension } from './types'
import { useProjectContext } from '@/features/projects/contexts/projectContext'

/**
 * Threshold in pixels for determining when to collapse selectors.
 * When the controls container width is less than this value, selectors will collapse.
 */
const COLLAPSE_SELECTORS_THRESHOLD = 420

/** Generation mode type - video or image */
type GenerateMode = 'video' | 'image'

const PIPELINE_NEXT_STEP_CONTEXT_TYPES = new Set<SubtaskContextBrief['context_type']>([
  'attachment',
  'knowledge_base',
  'table',
])

function isPipelineNextStepContext(context: unknown): context is SubtaskContextBrief {
  if (!context || typeof context !== 'object') {
    return false
  }

  const candidate = context as Partial<SubtaskContextBrief>
  return (
    typeof candidate.id === 'number' &&
    typeof candidate.name === 'string' &&
    typeof candidate.status === 'string' &&
    PIPELINE_NEXT_STEP_CONTEXT_TYPES.has(
      candidate.context_type as SubtaskContextBrief['context_type']
    )
  )
}

function getPipelineNextStepContexts(contexts: unknown): SubtaskContextBrief[] | undefined {
  if (!Array.isArray(contexts)) {
    return undefined
  }

  const validContexts = contexts.filter(isPipelineNextStepContext)
  return validContexts.length > 0 ? validContexts : undefined
}

interface ChatAreaProps {
  teams: Team[]
  isTeamsLoading: boolean
  selectedTeamForNewTask?: Team | null
  showRepositorySelector?: boolean
  taskType?: TaskType
  onShareButtonRender?: (button: React.ReactNode) => void
  onRefreshTeams?: () => Promise<Team[]>
  /** Initial knowledge base to pre-select when starting a new chat from knowledge page */
  initialKnowledgeBase?: {
    id: number
    name: string
    namespace: string
    document_count?: number
  } | null
  /** Callback when a new task is created (used for binding knowledge base) */
  onTaskCreated?: (taskId: number) => void
  /** Knowledge base ID for knowledge type tasks */
  knowledgeBaseId?: number
  /** Selected document IDs from DocumentPanel (for notebook mode context injection) */
  selectedDocumentIds?: number[]
  /** Reason why input is disabled (e.g., device offline). If set, input will be disabled and show this message. */
  disabledReason?: string
  /** When true, hide all selectors (team, model, skills, attachments, etc.) - only show text input + send button */
  hideSelectors?: boolean
  /** Callback when user switches between video and image mode (only used in generate page) */
  onGenerateModeChange?: (mode: GenerateMode) => void
  /** Guided questions to display when starting a new conversation (for notebook mode) */
  guidedQuestions?: string[]
  /** When true, input is always positioned at bottom even when there are no messages (used in knowledge notebook mode) */
  inputAlwaysAtBottom?: boolean
  /** Custom content to display when there are no messages (used in knowledge notebook mode for KnowledgeBaseSummaryCard) */
  emptyStateContent?: React.ReactNode
  /** Extension for team editing functionality (injected from parent to avoid module coupling) */
  extension?: ChatAreaExtension
}

/**
 * Inner component that uses the QuoteContext.
 * Must be rendered inside QuoteProvider.
 */
function ChatAreaContent({
  teams,
  isTeamsLoading,
  selectedTeamForNewTask,
  showRepositorySelector = true,
  taskType = 'chat',
  onShareButtonRender,
  onRefreshTeams,
  initialKnowledgeBase,
  onTaskCreated,
  knowledgeBaseId,
  selectedDocumentIds,
  disabledReason,
  hideSelectors,
  onGenerateModeChange,
  guidedQuestions,
  inputAlwaysAtBottom,
  emptyStateContent,
  extension,
}: ChatAreaProps) {
  const { t } = useTranslation()
  const { toast } = useToast()
  const router = useRouter()
  const chatStreamContext = useOptionalChatStreamContext()

  // Pipeline stage info state - shared between PipelineStageIndicator and MessagesArea
  const [pipelineStageInfo, setPipelineStageInfo] = useState<PipelineStageInfo | null>(null)
  const [isPipelineNextStepOpen, setIsPipelineNextStepOpen] = useState(false)
  const [isPipelineNextStepConfirming, setIsPipelineNextStepConfirming] = useState(false)
  const [pendingReplacementMessage, setPendingReplacementMessage] = useState<string | null>(null)
  const [isPendingReplacementOpen, setIsPendingReplacementOpen] = useState(false)
  const [isPendingReplacementConfirming, setIsPendingReplacementConfirming] = useState(false)
  const { quote, clearQuote, formatQuoteForMessage } = useQuote()

  // Task context
  const { selectedTask, selectedTaskDetail, setSelectedTask, accessDenied } = useTaskContext()
  const effectiveTaskId = selectedTask?.id ?? selectedTaskDetail?.id

  // Use useTaskStateMachine hook for reactive state updates (SINGLE SOURCE OF TRUTH per AGENTS.md)
  const { state: taskState } = useTaskStateMachine(effectiveTaskId)

  // Video model selection state - only enabled for video mode
  // Uses unified useModelSelection hook with modelCategoryType='video'
  // NOTE: Must be called before useChatAreaState to provide maxAttachments
  const videoModelSelection = useModelSelection({
    teamId: null,
    taskId: null,
    selectedTeam: null,
    disabled: taskType !== 'video',
    modelCategoryType: 'video',
  })

  // Image model selection state - only enabled for image mode
  // Uses unified useModelSelection hook with modelCategoryType='image'
  // NOTE: Must be called before useChatAreaState to provide maxAttachments
  const imageModelSelection = useModelSelection({
    teamId: null,
    taskId: null,
    selectedTeam: null,
    disabled: taskType !== 'image',
    modelCategoryType: 'image',
  })

  // Compute maxAttachments from selected model's imageConfig
  // This value is passed to useChatAreaState for attachment upload limits
  const maxAttachmentsFromModel = useMemo(() => {
    if (taskType === 'image') {
      const imageConfig = imageModelSelection.selectedModel?.config?.imageConfig as
        | { max_reference_images?: number }
        | undefined
      return imageConfig?.max_reference_images
    }
    // Video mode can also use reference images, use same field if available
    if (taskType === 'video') {
      const videoConfig = videoModelSelection.selectedModel?.config?.videoConfig as
        | { max_reference_images?: number }
        | undefined
      return videoConfig?.max_reference_images
    }
    return undefined
  }, [
    taskType,
    imageModelSelection.selectedModel?.config,
    videoModelSelection.selectedModel?.config,
  ])

  // Chat area state (team, repo, branch, model, input, toggles, etc.)
  const chatState = useChatAreaState({
    teams,
    taskType,
    selectedTeamForNewTask,
    initialKnowledgeBase,
    maxAttachments: maxAttachmentsFromModel,
  })

  // Compute initial selected skills from task detail (for page refresh recovery)
  const initialSelectedSkills = useMemo(() => {
    if (selectedTaskDetail?.requested_skills) {
      return selectedTaskDetail.requested_skills.map(skill => skill.name)
    }
    return []
  }, [selectedTaskDetail?.requested_skills])

  // Skill selector state - fetches available skills and manages selection
  const skillSelector = useSkillSelector({
    team: chatState.selectedTeam,
    enabled: true,
    initialSelectedSkills,
  })

  // Video mode specific state - resolution, aspect ratio, and duration
  // These are kept separate from useModelSelection as they are video-specific parameters
  const [selectedResolution, setSelectedResolution] = useState('1080p')
  const [selectedRatio, setSelectedRatio] = useState('16:9')
  const [selectedDuration, setSelectedDuration] = useState(5)

  // Derive available options and defaults from selected video model's config
  const videoConfig = videoModelSelection.selectedModel?.config?.videoConfig as
    | {
        resolution?: string
        ratio?: string
        duration?: number
        capabilities?: {
          aspect_ratios?: { value: string }[]
          resolutions?: { label: string }[]
          durations_sec?: number[]
        }
      }
    | undefined
  const videoCapabilities = videoConfig?.capabilities

  const availableResolutions = useMemo(() => {
    if (videoCapabilities?.resolutions?.length) {
      return videoCapabilities.resolutions.map(r => r.label)
    }
    return ['480p', '720p', '1080p']
  }, [videoCapabilities?.resolutions])

  const availableRatios = useMemo(() => {
    if (videoCapabilities?.aspect_ratios?.length) {
      return videoCapabilities.aspect_ratios.map(r => r.value)
    }
    return ['16:9', '9:16', '1:1', '4:3', '3:4']
  }, [videoCapabilities?.aspect_ratios])

  const availableDurations = useMemo(() => {
    if (videoCapabilities?.durations_sec?.length) {
      return videoCapabilities.durations_sec
    }
    return [5, 10]
  }, [videoCapabilities?.durations_sec])

  // When video model changes, apply model's recommended defaults
  const videoModelName = videoModelSelection.selectedModel?.name
  useEffect(() => {
    if (!videoConfig) return
    if (videoConfig.resolution && availableResolutions.includes(videoConfig.resolution)) {
      setSelectedResolution(videoConfig.resolution)
    } else if (availableResolutions.length) {
      setSelectedResolution(availableResolutions[0])
    }
    if (videoConfig.ratio && availableRatios.includes(videoConfig.ratio)) {
      setSelectedRatio(videoConfig.ratio)
    } else if (availableRatios.length) {
      setSelectedRatio(availableRatios[0])
    }
    if (videoConfig.duration && availableDurations.includes(videoConfig.duration)) {
      setSelectedDuration(videoConfig.duration)
    } else if (availableDurations.length) {
      setSelectedDuration(availableDurations[0])
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [videoModelName])

  // Image mode specific state - image size
  const [selectedImageSize, setSelectedImageSize] = useState('2048x2048')

  // Compute subtask info for scroll management
  // Note: Now using taskState from state machine instead of selectedTaskDetail.subtasks
  // The state machine messages are the single source of truth
  const lastSubtaskId = useMemo(() => {
    if (!taskState?.messages || taskState.messages.size === 0) return null
    let maxSubtaskId: number | null = null
    for (const msg of taskState.messages.values()) {
      if (msg.subtaskId && (maxSubtaskId === null || msg.subtaskId > maxSubtaskId)) {
        maxSubtaskId = msg.subtaskId
      }
    }
    return maxSubtaskId
  }, [taskState?.messages])
  const lastSubtaskUpdatedAt = null // No longer needed from subtasks, scroll management uses other signals
  // Determine if there are messages to display (computed early for hooks)
  // Uses state machine messages as the single source of truth, not selectedTaskDetail.subtasks
  const hasMessagesForHooks = useMemo(() => {
    const hasSelectedTask = Boolean(effectiveTaskId)
    // Check messages from state machine (single source of truth)
    const hasContextMessages = taskState?.messages && taskState.messages.size > 0
    return Boolean(hasSelectedTask || hasContextMessages)
  }, [effectiveTaskId, taskState?.messages])

  // Get taskId from URL for team sync logic
  const searchParams = useSearchParams()
  const taskIdFromUrl =
    searchParams.get('taskId') || searchParams.get('task_id') || searchParams.get('taskid')
  // Get teamId from URL for auto-selecting a specific team (e.g. after accepting a share invite)
  const teamIdFromUrl = searchParams.get('teamId')
  // Get project info when in project context
  const projectIdFromUrl = searchParams.get('projectId')
  const { projects } = useProjectContext()
  const activeProject = useMemo(() => {
    if (!projectIdFromUrl) return null
    const project = projects.find(p => p.id === Number(projectIdFromUrl))
    if (!project) return null
    const explicitPath = project.config?.workspace?.localPath
    const defaultPath = `~/.wegent-executor/workspace/project${project.id}`
    return {
      name: project.name,
      path: explicitPath || defaultPath,
    }
  }, [projectIdFromUrl, projects])

  // Track initialization and last synced task for team selection
  const hasInitializedTeamRef = useRef(false)
  const lastSyncedTaskIdRef = useRef<number | null>(null)

  // Filter teams by bind_mode based on current mode
  const filteredTeams = useMemo(() => {
    const teamsWithValidBindMode = teams.filter(team => {
      if (Array.isArray(team.bind_mode) && team.bind_mode.length === 0) return false
      return true
    })
    const result = teamsWithValidBindMode.filter(team => {
      if (!team.bind_mode) return true
      const included = team.bind_mode.includes(taskType)
      return included
    })

    return result
  }, [teams, taskType])

  // Extract values for dependency array
  const selectedTeam = chatState.selectedTeam
  const handleTeamChange = chatState.handleTeamChange
  const findDefaultTeamForMode = chatState.findDefaultTeamForMode

  // Team selection logic - using default team from server configuration
  useEffect(() => {
    if (filteredTeams.length === 0) return

    // Extract team ID from task detail
    const detailTeamId = selectedTaskDetail?.team
      ? typeof selectedTaskDetail.team === 'number'
        ? selectedTaskDetail.team
        : (selectedTaskDetail.team as Team).id
      : null

    // Case 1: Sync from task detail (HIGHEST PRIORITY)
    // Only sync when URL taskId matches taskDetail.id to prevent race conditions
    if (taskIdFromUrl && selectedTaskDetail?.id && detailTeamId) {
      if (selectedTaskDetail.id.toString() === taskIdFromUrl) {
        // Only update if we haven't synced this task yet or team is different
        if (
          lastSyncedTaskIdRef.current !== selectedTaskDetail.id ||
          selectedTeam?.id !== detailTeamId
        ) {
          const teamFromDetail = filteredTeams.find(t => t.id === detailTeamId)
          if (teamFromDetail) {
            handleTeamChange(teamFromDetail)
            lastSyncedTaskIdRef.current = selectedTaskDetail.id
            hasInitializedTeamRef.current = true
            return
          } else {
            // Team not in filtered list, try to use the team object from detail
            const teamObject =
              typeof selectedTaskDetail.team === 'object' ? (selectedTaskDetail.team as Team) : null
            if (teamObject) {
              handleTeamChange(teamObject)
              lastSyncedTaskIdRef.current = selectedTaskDetail.id
              hasInitializedTeamRef.current = true
              return
            }
          }
        } else {
          // Already synced this task, skip
          return
        }
      } else {
        // URL and taskDetail don't match - wait for correct taskDetail to load
        return
      }
    }

    // Case 2a: teamId URL param present - select specific team regardless of initialization state
    // This handles navigation after accepting a share invite (/chat?teamId=xxx)
    if (!taskIdFromUrl && teamIdFromUrl) {
      const targetTeamId = Number(teamIdFromUrl)
      const teamFromUrl =
        filteredTeams.find(t => t.id === targetTeamId) || teams.find(t => t.id === targetTeamId)
      if (teamFromUrl) {
        handleTeamChange(teamFromUrl)
        hasInitializedTeamRef.current = true
        lastSyncedTaskIdRef.current = null
        return
      }
    }

    // Case 2: New chat (no taskId in URL) - use default team from server config
    if (!taskIdFromUrl && !hasInitializedTeamRef.current) {
      // Use the default team computed from server config
      const defaultTeamForMode = findDefaultTeamForMode(filteredTeams)
      if (defaultTeamForMode) {
        handleTeamChange(defaultTeamForMode)
        hasInitializedTeamRef.current = true
        lastSyncedTaskIdRef.current = null
        return
      }
      // No default found, select first team
      if (!selectedTeam && filteredTeams.length > 0) {
        handleTeamChange(filteredTeams[0])
      }
      hasInitializedTeamRef.current = true
      lastSyncedTaskIdRef.current = null
      return
    }

    // Case 3: Validate current selection exists in filtered list
    if (selectedTeam) {
      const exists = filteredTeams.some(t => t.id === selectedTeam.id)
      if (!exists) {
        const defaultTeamForMode = findDefaultTeamForMode(filteredTeams)
        handleTeamChange(defaultTeamForMode || filteredTeams[0])
      }
    } else if (!taskIdFromUrl) {
      // No selection and no task - select default team
      const defaultTeamForMode = findDefaultTeamForMode(filteredTeams)
      handleTeamChange(defaultTeamForMode || filteredTeams[0])
    }
  }, [
    filteredTeams,
    teams,
    selectedTaskDetail,
    taskIdFromUrl,
    teamIdFromUrl,
    selectedTeam,
    handleTeamChange,
    findDefaultTeamForMode,
  ])

  // Reset initialization when switching from task to new chat
  useEffect(() => {
    if (!taskIdFromUrl) {
      lastSyncedTaskIdRef.current = null
    }
  }, [taskIdFromUrl])

  // Handle team selection from QuickAccessCards
  const handleTeamSelect = useCallback(
    (team: Team) => {
      handleTeamChange(team)
    },
    [handleTeamChange]
  )

  // Use scroll management hook - consolidates 4 useEffect calls
  const {
    scrollContainerRef,
    isUserNearBottomRef,
    showScrollIndicator,
    scrollToBottom,
    handleMessagesContentChange: _baseHandleMessagesContentChange,
  } = useScrollManagement({
    hasMessages: hasMessagesForHooks,
    isStreaming: false, // Will be updated after streamHandlers is created
    selectedTaskId: selectedTaskDetail?.id,
    lastSubtaskId,
    lastSubtaskUpdatedAt,
  })

  // Use floating input hook - consolidates 3 useEffect calls
  const {
    chatAreaRef,
    floatingInputRef,
    inputControlsRef,
    floatingMetrics,
    inputHeight,
    controlsContainerWidth,
  } = useFloatingInput({
    hasMessages: hasMessagesForHooks,
  })

  // For video/image mode, use respective model selection; otherwise use regular model selection
  // This ensures the correct model is passed to the backend for routing
  const effectiveSelectedModel = useMemo(() => {
    if (taskType === 'video') return videoModelSelection.selectedModel
    if (taskType === 'image') return imageModelSelection.selectedModel
    return chatState.selectedModel
  }, [
    taskType,
    videoModelSelection.selectedModel,
    imageModelSelection.selectedModel,
    chatState.selectedModel,
  ])

  // Build generate params for video/image generation tasks
  // Include model name for display in user message bubble
  const generateParams = useMemo(() => {
    if (taskType === 'video') {
      return {
        resolution: selectedResolution,
        ratio: selectedRatio,
        duration: selectedDuration,
        model: videoModelSelection.selectedModel?.name,
      }
    }
    if (taskType === 'image') {
      return {
        size: selectedImageSize,
        model: imageModelSelection.selectedModel?.name,
      }
    }
    return undefined
  }, [
    taskType,
    selectedResolution,
    selectedRatio,
    selectedDuration,
    selectedImageSize,
    videoModelSelection.selectedModel?.name,
    imageModelSelection.selectedModel?.name,
  ])

  // Stream handlers (send message, retry, cancel, stop)
  const streamHandlers = useChatStreamHandlers({
    selectedTeam: chatState.selectedTeam,
    selectedModel: effectiveSelectedModel,
    forceOverride: chatState.forceOverride,
    setSelectedModel: chatState.setSelectedModel,
    setForceOverride: chatState.setForceOverride,
    selectedRepo: chatState.selectedRepo,
    selectedBranch: chatState.selectedBranch,
    showRepositorySelector,
    effectiveRequiresWorkspace: chatState.effectiveRequiresWorkspace,
    taskInputMessage: chatState.taskInputMessage,
    setTaskInputMessage: chatState.setTaskInputMessage,
    setIsLoading: chatState.setIsLoading,
    enableDeepThinking: chatState.enableDeepThinking,
    enableClarification: chatState.enableClarification,
    externalApiParams: chatState.externalApiParams,
    attachments: chatState.attachmentState.attachments,
    resetAttachment: chatState.resetAttachment,
    isAttachmentReadyToSend: chatState.isAttachmentReadyToSend,
    taskType,
    knowledgeBaseId,
    shouldHideChatInput: chatState.shouldHideChatInput,
    scrollToBottom,
    selectedContexts: chatState.selectedContexts,
    resetContexts: chatState.resetContexts,
    onTaskCreated,
    selectedDocumentIds,
    // Skill selection - pass user-selected skills to backend
    // Uses full skill info (name, namespace, is_public) for backend to determine preload vs download
    additionalSkills: skillSelector.selectedSkills,
    // Generation parameters for video/image generation tasks
    generateParams,
  })

  // Scheme URL action bridge - handles wegent://action/send-message and wegent://action/prefill-message
  useSchemeMessageActions({
    onSendMessage: streamHandlers.handleSendMessage,
    onPrefillMessage: chatState.setTaskInputMessage,
    onTeamChange: teamId => {
      const targetTeam =
        filteredTeams.find(t => t.id === teamId) || teams.find(t => t.id === teamId)
      if (targetTeam) {
        handleTeamChange(targetTeam)
      }
    },
    currentTeamId: chatState.selectedTeam?.id,
    teams: [...filteredTeams, ...teams],
  })

  // Determine if there are messages to display (full computation)
  // Note: Now using taskState.messages from state machine instead of selectedTaskDetail.subtasks
  const hasMessages = useMemo(() => {
    const hasSelectedTask = Boolean(effectiveTaskId)
    const hasNewTaskStream =
      !effectiveTaskId && streamHandlers.pendingTaskId && streamHandlers.isStreaming
    const hasLocalPending = streamHandlers.localPendingMessage !== null
    // Use taskState from state machine (single source of truth)
    const hasUnifiedMessages = taskState?.messages && taskState.messages.size > 0

    // If we have a selected task with messages in state machine, show messages
    if (hasSelectedTask && hasUnifiedMessages) {
      return true
    }

    // In inputAlwaysAtBottom mode (knowledge notebook), only consider actual messages
    // not just the presence of selectedTaskDetail
    if (inputAlwaysAtBottom) {
      return Boolean(
        streamHandlers.hasPendingUserMessage ||
        streamHandlers.isStreaming ||
        hasNewTaskStream ||
        hasLocalPending ||
        hasUnifiedMessages
      )
    }

    return Boolean(
      hasSelectedTask ||
      streamHandlers.hasPendingUserMessage ||
      streamHandlers.isStreaming ||
      hasNewTaskStream ||
      hasLocalPending ||
      hasUnifiedMessages
    )
  }, [
    effectiveTaskId,
    streamHandlers.hasPendingUserMessage,
    streamHandlers.isStreaming,
    streamHandlers.pendingTaskId,
    streamHandlers.localPendingMessage,
    taskState?.messages,
    inputAlwaysAtBottom,
  ])

  const pipelineNextStepMessages = useMemo<PipelineNextStepMessage[]>(() => {
    if (!taskState?.messages) return []

    return Array.from(taskState.messages.values()).map(message => ({
      id: message.id,
      type: message.type,
      status: message.status,
      content: message.content,
      timestamp: message.timestamp,
      messageId: message.messageId,
      contexts: getPipelineNextStepContexts(message.contexts),
    }))
  }, [taskState?.messages])

  const pipelineNextStepDraft = useMemo(
    () => buildPipelineNextStepDraft(pipelineNextStepMessages),
    [pipelineNextStepMessages]
  )

  useEffect(() => {
    if (!pipelineStageInfo?.is_pending_confirmation) {
      setIsPipelineNextStepOpen(false)
    }
  }, [pipelineStageInfo?.is_pending_confirmation])

  // Note: Team selection is now handled by useTeamSelection hook in TeamSelector component
  // Model selection is handled by useModelSelection hook in ModelSelector component

  // Check if model selection is required
  const isModelSelectionRequired = useMemo(() => {
    // OpenClaw devices handle model on device side, no model selection required
    if (hideSelectors) return false
    // Video mode uses video model selection, not regular model selection
    if (taskType === 'video') {
      // In video mode, we need a video model selected
      return !videoModelSelection.selectedModel
    }
    // Image mode uses image model selection
    if (taskType === 'image') {
      // In image mode, we need an image model selected
      return !imageModelSelection.selectedModel
    }
    if (!chatState.selectedTeam || chatState.selectedTeam.agent_type === 'dify') return false
    const hasDefaultOption = allBotsHavePredefinedModel(chatState.selectedTeam)
    if (hasDefaultOption) return false
    return !chatState.selectedModel
  }, [
    chatState.selectedTeam,
    chatState.selectedModel,
    taskType,
    hideSelectors,
    videoModelSelection.selectedModel,
    imageModelSelection.selectedModel,
  ])

  // Unified canSubmit flag
  const canSubmit = useMemo(() => {
    return (
      !disabledReason &&
      !chatState.isLoading &&
      (!streamHandlers.isStreaming || streamHandlers.canQueueMessage) &&
      !isModelSelectionRequired &&
      chatState.isAttachmentReadyToSend
    )
  }, [
    disabledReason,
    chatState.isLoading,
    streamHandlers.isStreaming,
    streamHandlers.canQueueMessage,
    isModelSelectionRequired,
    chatState.isAttachmentReadyToSend,
  ])

  // Collapse selectors when space is limited
  const shouldCollapseSelectors =
    controlsContainerWidth > 0 && controlsContainerWidth < COLLAPSE_SELECTORS_THRESHOLD

  // Keep latest mutable values in refs so callbacks passed to MessagesArea remain stable.
  const taskInputMessageRef = useRef(chatState.taskInputMessage)
  taskInputMessageRef.current = chatState.taskInputMessage

  const stateMessagesRef = useRef(taskState?.messages)
  stateMessagesRef.current = taskState?.messages

  const handleSendMessageRef = useRef(streamHandlers.handleSendMessage)
  handleSendMessageRef.current = streamHandlers.handleSendMessage

  const handleSendMessageWithModelRef = useRef(streamHandlers.handleSendMessageWithModel)
  handleSendMessageWithModelRef.current = streamHandlers.handleSendMessageWithModel

  const handleRetryRef = useRef(streamHandlers.handleRetry)
  handleRetryRef.current = streamHandlers.handleRetry

  const handleRetryWithModelRef = useRef(streamHandlers.handleRetryWithModel)
  handleRetryWithModelRef.current = streamHandlers.handleRetryWithModel

  const setTaskInputMessage = chatState.setTaskInputMessage
  const setSelectedContexts = chatState.setSelectedContexts
  const resetAttachment = chatState.resetAttachment
  const addExistingAttachment = chatState.addExistingAttachment
  const selectedContextsRef = useRef(chatState.selectedContexts)
  selectedContextsRef.current = chatState.selectedContexts

  const shouldConfirmPendingReplacement =
    selectedTaskDetail?.status === 'PENDING' && !selectedTaskDetail?.is_group_chat

  const sendOrConfirmPendingReplacement = useCallback(
    async (message: string) => {
      const trimmedMessage = message.trim()
      if (!trimmedMessage && !chatState.shouldHideChatInput) return

      if (shouldConfirmPendingReplacement) {
        setPendingReplacementMessage(trimmedMessage)
        setIsPendingReplacementOpen(true)
        return
      }

      await streamHandlers.handleSendMessage(trimmedMessage)
    },
    [chatState.shouldHideChatInput, shouldConfirmPendingReplacement, streamHandlers]
  )

  const handleConfirmPendingReplacement = useCallback(async () => {
    if (!pendingReplacementMessage || isPendingReplacementConfirming) return

    setIsPendingReplacementConfirming(true)
    try {
      const cancelled = await streamHandlers.handleCancelTask()
      if (!cancelled) return

      const message = pendingReplacementMessage
      setPendingReplacementMessage(null)
      setIsPendingReplacementOpen(false)
      await streamHandlers.handleSendMessage(message)
    } finally {
      setIsPendingReplacementConfirming(false)
    }
  }, [pendingReplacementMessage, isPendingReplacementConfirming, streamHandlers])

  // Handle queue message loaded from inbox - adds the message(s) as context(s)
  // Supports both single message and batch processing
  const handleQueueMessageLoaded = useCallback(
    (queueMessageContexts: QueueMessageContext[]) => {
      // Add the queue message contexts to selectedContexts
      const currentContexts = selectedContextsRef.current
      // Filter out duplicates
      const newContexts = queueMessageContexts.filter(
        ctx => !currentContexts.some(c => c.type === 'queue_message' && c.id === ctx.id)
      )
      if (newContexts.length > 0) {
        setSelectedContexts([...currentContexts, ...newContexts])
      }
    },
    [setSelectedContexts]
  )

  // Load prompt from sessionStorage - single remaining useEffect
  useEffect(() => {
    if (hasMessages) return

    const pendingPromptData = sessionStorage.getItem('pendingTaskPrompt')
    if (pendingPromptData) {
      try {
        const data = JSON.parse(pendingPromptData)
        const isRecent = Date.now() - data.timestamp < 5 * 60 * 1000

        if (isRecent && data.prompt) {
          setTaskInputMessage(data.prompt)
          sessionStorage.removeItem('pendingTaskPrompt')
        }
      } catch (error) {
        console.error('Failed to parse pending prompt data:', error)
        sessionStorage.removeItem('pendingTaskPrompt')
      }
    }
  }, [hasMessages, setTaskInputMessage])

  // Use attachment upload hook - centralizes all attachment upload logic
  const { handleDragEnter, handleDragLeave, handleDragOver, handleDrop, handlePasteFile } =
    useAttachmentUpload({
      team: chatState.selectedTeam,
      isLoading: chatState.isLoading,
      isStreaming: streamHandlers.isStreaming,
      attachmentState: chatState.attachmentState,
      onFileSelect: chatState.handleFileSelect,
      setIsDragging: chatState.setIsDragging,
    })

  // Callback for MessagesArea content changes - enhanced with streaming check
  const handleMessagesContentChange = useCallback(() => {
    if (streamHandlers.isStreaming || isUserNearBottomRef.current) {
      scrollToBottom()
    }
  }, [streamHandlers.isStreaming, scrollToBottom, isUserNearBottomRef])

  // Callback for child components to send messages
  const handleSendMessageFromChild = useCallback(
    async (content: string) => {
      const existingInput = taskInputMessageRef.current.trim()
      const combinedMessage = existingInput ? `${content}\n\n---\n\n${existingInput}` : content
      setTaskInputMessage('')
      await sendOrConfirmPendingReplacement(combinedMessage)
    },
    [sendOrConfirmPendingReplacement, setTaskInputMessage]
  )

  // Callback for child components to send messages with a specific model (for regeneration)
  // Accepts optional existingContexts to preserve attachments/knowledge bases from the original message
  const handleSendMessageWithModelFromChild = useCallback(
    async (content: string, model: Model, existingContexts?: SubtaskContextBrief[]) => {
      await handleSendMessageWithModelRef.current(content, model, existingContexts)
    },
    []
  )

  // Keep retry callback stable so MessagesArea can skip re-render on input typing.
  const handleRetryFromMessagesArea = useCallback(
    (message: import('../message/MessageBubble').Message) => {
      void handleRetryRef.current(message)
    },
    []
  )

  const handleRetryWithModelFromMessagesArea = useCallback(
    (
      message: import('../message/MessageBubble').Message,
      model: import('@/apis/models').UnifiedModel
    ) => {
      void handleRetryWithModelRef.current(message, model)
    },
    []
  )

  // Callback for re-selecting a context from a message badge
  const handleContextReselect = useCallback(
    (context: SubtaskContextBrief) => {
      // Convert SubtaskContextBrief to ContextItem format
      let contextItem: ContextItem | null = null

      if (context.context_type === 'knowledge_base') {
        if (!context.knowledge_id) return
        contextItem = {
          id: context.knowledge_id,
          name: context.name,
          type: 'knowledge_base',
          document_count: context.document_count ?? undefined,
        }
      } else if (context.context_type === 'table') {
        if (!context.document_id) return
        contextItem = {
          id: `table-${context.document_id}`,
          name: context.name,
          type: 'table',
          document_id: context.document_id,
          source_config: context.source_config ?? undefined,
        }
      }

      if (!contextItem) return

      const currentContexts = selectedContextsRef.current
      const isAlreadySelected = currentContexts.some(
        c => c.type === contextItem!.type && c.id === contextItem!.id
      )
      if (isAlreadySelected) return

      setSelectedContexts([...currentContexts, contextItem])
    },
    [setSelectedContexts]
  )

  const handlePipelineNextStepClick = useCallback(() => {
    setIsPipelineNextStepOpen(true)
  }, [])

  const handlePipelineNextStepConfirm = useCallback(
    async (payload: PipelineNextStepPayload) => {
      const taskId = selectedTaskDetail?.id
      const teamId = chatState.selectedTeam?.id ?? selectedTaskDetail?.team?.id

      if (!taskId || !teamId) {
        toast({
          variant: 'destructive',
          title: t('chat:pipeline.next_step_dialog.missing_task'),
        })
        return
      }

      if (!chatStreamContext) {
        toast({
          variant: 'destructive',
          title: t('chat:pipeline.confirm_failed'),
        })
        return
      }

      setIsPipelineNextStepConfirming(true)
      try {
        await chatStreamContext.sendMessage(
          {
            task_id: taskId,
            team_id: teamId,
            message: payload.message,
            action: 'pipeline:confirm',
            attachment_ids: payload.attachmentIds.length > 0 ? payload.attachmentIds : undefined,
            contexts: payload.contexts.length > 0 ? payload.contexts : undefined,
          },
          {
            pendingUserMessage: payload.message,
            pendingContexts: payload.pendingContexts,
            immediateTaskId: taskId,
            onError: error => {
              throw error
            },
          }
        )

        setIsPipelineNextStepOpen(false)
        toast({
          title: t('chat:pipeline.stage_confirmed'),
        })
      } catch (error) {
        console.error('Failed to confirm pipeline next step:', error)
        toast({
          variant: 'destructive',
          title: t('chat:pipeline.confirm_failed'),
        })
      } finally {
        setIsPipelineNextStepConfirming(false)
      }
    },
    [
      chatState.selectedTeam?.id,
      chatStreamContext,
      selectedTaskDetail?.id,
      selectedTaskDetail?.team?.id,
      t,
      toast,
    ]
  )

  // Callback when user wants to use a previously generated image as reference
  // Fetches the attachment metadata and adds it to the current input attachments
  const handleUseAsReference = useCallback(
    async (item: import('../message/ImageGallery').ImageItem) => {
      if (!item.attachmentId) return
      try {
        const detail = await getAttachment(item.attachmentId)
        addExistingAttachment({
          id: detail.id,
          filename: detail.filename,
          file_size: detail.file_size,
          mime_type: detail.mime_type,
          status: detail.status,
          text_length: detail.text_length ?? null,
          error_message: detail.error_message ?? null,
          error_code: detail.error_code ?? null,
          subtask_id: detail.subtask_id ?? null,
          file_extension: detail.file_extension,
          created_at: detail.created_at,
        })
      } catch (error) {
        // Log error; system will fall back to auto intent analysis
        console.error('Failed to use image as reference:', error)
      }
    },
    [addExistingAttachment]
  )

  // Callback when user clicks re-edit on an AI message
  // Finds the corresponding user message from the state machine messages and restores its prompt + attachments to the input
  const handleReEdit = useCallback(
    async (aiMsg: import('../message/MessageBubble').Message) => {
      if (!aiMsg.subtaskId) return

      // Locate the AI message in the state machine to get its messageId (shared with the user message)
      const stateMessages = stateMessagesRef.current
      if (!stateMessages) return

      const aiStateMsg = stateMessages.get(`ai-${aiMsg.subtaskId}`)
      if (!aiStateMsg) return

      // Find the corresponding user message using the following strategy:
      // 1. Primary: match by shared messageId (works for messages loaded from backend)
      // 2. Fallback: use Map insertion order - find the last user message that appears
      //    before the AI message in the Map (works for live-session messages that have no messageId yet)
      let userStateMsg: import('../../state/TaskStateMachine').UnifiedMessage | undefined

      if (aiStateMsg.messageId != null) {
        // Primary lookup: match by shared messageId
        for (const msg of stateMessages.values()) {
          if (msg.type === 'user' && msg.messageId === aiStateMsg.messageId) {
            userStateMsg = msg
            break
          }
        }
      }

      if (!userStateMsg) {
        // Fallback: iterate the Map in insertion order; track the last user message seen
        // before we reach the target AI message entry
        let lastUserMsg: import('../../state/TaskStateMachine').UnifiedMessage | undefined
        for (const [key, msg] of stateMessages.entries()) {
          if (key === `ai-${aiMsg.subtaskId}`) {
            // Reached the AI message - the previous user message is the one we want
            if (lastUserMsg) {
              userStateMsg = lastUserMsg
            }
            break
          }
          if (msg.type === 'user') {
            lastUserMsg = msg
          }
        }
      }

      if (!userStateMsg) return

      // Restore text prompt to input
      if (userStateMsg.content) {
        setTaskInputMessage(userStateMsg.content)
      }

      // Clear any existing draft attachments and contexts before restoring the original ones
      // so the restored set exactly matches the original user message
      resetAttachment()
      setSelectedContexts([])

      // Restore all contexts (attachments and knowledge bases) from the user message
      const rawContexts = (userStateMsg.contexts || []) as SubtaskContextBrief[]

      // Restore attachment contexts
      const attachmentContexts = rawContexts.filter(c => c.context_type === 'attachment')
      for (const ctx of attachmentContexts) {
        try {
          const detail = await getAttachment(ctx.id)
          addExistingAttachment({
            id: detail.id,
            filename: detail.filename,
            file_size: detail.file_size,
            mime_type: detail.mime_type,
            status: detail.status,
            text_length: detail.text_length ?? null,
            error_message: detail.error_message ?? null,
            error_code: detail.error_code ?? null,
            subtask_id: detail.subtask_id ?? null,
            file_extension: detail.file_extension,
            created_at: detail.created_at,
          })
        } catch (error) {
          console.error('Failed to restore attachment for re-edit:', error)
        }
      }

      // Restore knowledge base and table contexts
      const restoredContextItems: ContextItem[] = []
      for (const ctx of rawContexts) {
        if (ctx.context_type === 'knowledge_base') {
          if (!ctx.knowledge_id) continue
          restoredContextItems.push({
            id: ctx.knowledge_id,
            name: ctx.name,
            type: 'knowledge_base',
            document_count: ctx.document_count ?? undefined,
          })
        } else if (ctx.context_type === 'table') {
          if (!ctx.document_id) continue
          restoredContextItems.push({
            id: `table-${ctx.document_id}`,
            name: ctx.name,
            type: 'table',
            document_id: ctx.document_id,
            source_config: ctx.source_config ?? undefined,
          })
        }
      }
      if (restoredContextItems.length > 0) {
        setSelectedContexts(restoredContextItems)
      }
    },
    [setTaskInputMessage, resetAttachment, setSelectedContexts, addExistingAttachment]
  )

  // Handle access denied state
  if (accessDenied) {
    const handleGoHome = () => {
      setSelectedTask(null)
      router.push('/chat')
    }

    return (
      <div
        ref={chatAreaRef}
        className="flex-1 flex flex-col min-h-0 w-full relative"
        style={{ height: '100%', boxSizing: 'border-box' }}
      >
        <div className="flex-1 flex items-center justify-center p-6">
          <div className="max-w-lg w-full">
            <div className="flex justify-center mb-6">
              <div className="w-20 h-20 rounded-full bg-destructive/10 flex items-center justify-center">
                <ShieldX className="h-10 w-10 text-destructive" />
              </div>
            </div>
            <h1 className="text-2xl font-semibold text-center mb-3 text-text-primary">
              {t('tasks:access_denied_title')}
            </h1>
            <p className="text-center text-text-muted mb-8 leading-relaxed">
              {t('tasks:access_denied_description')}
            </p>
            <div className="flex justify-center">
              <Button
                onClick={handleGoHome}
                variant="default"
                size="default"
                className="min-w-[160px]"
              >
                {t('tasks:access_denied_go_home')}
              </Button>
            </div>
          </div>
        </div>
      </div>
    )
  }

  // Common input card props
  const inputCardProps = {
    taskInputMessage: chatState.taskInputMessage,
    setTaskInputMessage: chatState.setTaskInputMessage,
    selectedTeam: chatState.selectedTeam,
    teams: teams,
    externalApiParams: chatState.externalApiParams,
    onTeamChange: chatState.handleTeamChange,
    onTeamsRefresh: async () => {
      if (onRefreshTeams) {
        await onRefreshTeams()
      }
    },
    onExternalApiParamsChange: chatState.handleExternalApiParamsChange,
    onAppModeChange: chatState.handleAppModeChange,
    // Only enable restore when default team exists
    onRestoreDefaultTeam: chatState.defaultTeam ? chatState.restoreDefaultTeam : undefined,
    isUsingDefaultTeam: chatState.isUsingDefaultTeam,
    taskType,
    tipText: chatState.randomTip,
    isGroupChat: selectedTaskDetail?.is_group_chat || false,
    isDragging: chatState.isDragging,
    onDragEnter: handleDragEnter,
    onDragLeave: handleDragLeave,
    onDragOver: handleDragOver,
    onDrop: handleDrop,
    canSubmit,
    canQueueMessage: streamHandlers.canQueueMessage,
    queuedMessages: streamHandlers.queuedMessages,
    onCancelQueuedMessage: streamHandlers.cancelQueuedMessage,
    onSendQueuedAsGuidance: streamHandlers.sendQueuedAsGuidance,
    canSendGuidance: streamHandlers.canSendGuidance,
    guidanceMessages: streamHandlers.guidanceMessages,
    expiredGuidanceMessages: streamHandlers.expiredGuidanceMessages,
    onCancelGuidance: streamHandlers.cancelGuidance,
    onSendExpiredGuidanceAsMessage: streamHandlers.sendExpiredGuidanceAsMessage,
    handleSendMessage: async (overrideMessage?: string) => {
      // Format message with quote if present, then clear quote
      const baseMessage = overrideMessage?.trim() || chatState.taskInputMessage.trim()
      const message = formatQuoteForMessage(baseMessage)
      if (quote) {
        clearQuote()
      }
      await sendOrConfirmPendingReplacement(message)
    },
    onSendGuidance: async () => {
      const baseMessage = chatState.taskInputMessage.trim()
      const message = formatQuoteForMessage(baseMessage)
      if (quote) {
        clearQuote()
      }
      await streamHandlers.handleSendGuidance(message)
    },
    onPasteFile: handlePasteFile,
    // ChatInputControls props
    selectedModel: chatState.selectedModel,
    setSelectedModel: chatState.setSelectedModel,
    forceOverride: chatState.forceOverride,
    setForceOverride: chatState.setForceOverride,
    teamId: chatState.selectedTeam?.id,
    taskId: selectedTaskDetail?.id,
    showRepositorySelector,
    selectedRepo: chatState.selectedRepo,
    setSelectedRepo: chatState.setSelectedRepo,
    selectedBranch: chatState.selectedBranch,
    setSelectedBranch: chatState.setSelectedBranch,
    selectedTaskDetail,
    effectiveRequiresWorkspace: chatState.effectiveRequiresWorkspace,
    onRequiresWorkspaceChange: (value: boolean) => {
      chatState.setRequiresWorkspaceOverride(value)
    },
    enableDeepThinking: chatState.enableDeepThinking,
    setEnableDeepThinking: chatState.setEnableDeepThinking,
    enableClarification: chatState.enableClarification,
    setEnableClarification: chatState.setEnableClarification,
    enableCorrectionMode: chatState.enableCorrectionMode,
    correctionModelName: chatState.correctionModelName,
    onCorrectionModeToggle: chatState.handleCorrectionModeToggle,
    selectedContexts: chatState.selectedContexts,
    setSelectedContexts: chatState.setSelectedContexts,
    attachmentState: chatState.attachmentState,
    onFileSelect: chatState.handleFileSelect,
    onAttachmentRemove: chatState.handleAttachmentRemove,
    isLoading: chatState.isLoading,
    isStreaming: streamHandlers.isStreaming,
    isAwaitingResponseStart: streamHandlers.isAwaitingResponseStart,
    isStopping: streamHandlers.isStopping,
    hasMessages,
    shouldCollapseSelectors,
    shouldHideQuotaUsage: chatState.shouldHideQuotaUsage,
    shouldHideChatInput: chatState.shouldHideChatInput,
    isModelSelectionRequired,
    isAttachmentReadyToSend: chatState.isAttachmentReadyToSend,
    isSubtaskStreaming: streamHandlers.isSubtaskStreaming,
    onStopStream: streamHandlers.stopStream,
    onCancelTask: streamHandlers.handleCancelTask,
    isCancelling: streamHandlers.isCancelling,
    onSendMessage: () => {
      // Format message with quote if present, then clear quote
      const message = formatQuoteForMessage(chatState.taskInputMessage.trim())
      if (quote) {
        clearQuote()
      }
      void sendOrConfirmPendingReplacement(message)
    },
    // Whether there are no available teams for current mode
    hasNoTeams: filteredTeams.length === 0,
    // Knowledge base ID to exclude from context selector (used in notebook mode)
    knowledgeBaseId,
    // Reason why input is disabled (shown as placeholder)
    disabledReason,
    // Project context
    projectId: projectIdFromUrl ? Number(projectIdFromUrl) : null,
    // Skill selector props
    availableSkills: skillSelector.availableSkills,
    teamSkillNames: skillSelector.teamSkillNames,
    preloadedSkillNames: skillSelector.preloadedSkillNames,
    selectedSkillNames: skillSelector.selectedSkillNames,
    onToggleSkill: skillSelector.toggleSkill,
    // Video mode props - only passed when taskType is 'video'
    // Note: videoModels is no longer passed - ModelSelector fetches models internally via useModelSelection
    selectedVideoModel: videoModelSelection.selectedModel,
    onVideoModelChange: (model: Model) =>
      videoModelSelection.selectModelByKey(`${model.name}:${model.type || ''}`),
    isVideoModelsLoading: videoModelSelection.isLoading,
    selectedResolution,
    onResolutionChange: setSelectedResolution,
    availableResolutions,
    selectedRatio,
    onRatioChange: setSelectedRatio,
    availableRatios,
    selectedDuration,
    onDurationChange: setSelectedDuration,
    availableDurations,
    // Image mode props - only passed when taskType is 'image'
    // Note: imageModels is no longer passed - ModelSelector fetches models internally via useModelSelection
    selectedImageModel: imageModelSelection.selectedModel,
    onImageModelChange: (model: Model) =>
      imageModelSelection.selectModelByKey(`${model.name}:${model.type || ''}`),
    isImageModelsLoading: imageModelSelection.isLoading,
    selectedImageSize,
    onImageSizeChange: setSelectedImageSize,
    // Generate mode switch props - only passed when in generate page
    onGenerateModeChange,
    // Hide all selectors (for OpenClaw devices)
    hideSelectors,
    // Team edit callback - only provided when team is editable
    onEditTeam: extension?.teamEdit?.canEdit ? extension.teamEdit.onEdit : undefined,
  }

  const shouldMountQueueMessageHandler =
    taskType === 'chat' || taskType === 'task' || taskType === 'code'

  return (
    <div
      ref={chatAreaRef}
      className="flex-1 flex flex-col min-h-0 w-full relative"
      style={{ height: '100%', boxSizing: 'border-box' }}
    >
      {/* Queue Message Handler - processes process_message URL parameter from inbox */}
      {shouldMountQueueMessageHandler && (
        <QueueMessageHandler onQueueMessageLoaded={handleQueueMessageLoaded} />
      )}

      {/* Auto-send message from URL query parameter ?q=xxx&teamId=xxx */}
      <QueryParamAutoSend
        teams={teams}
        isTeamsLoading={isTeamsLoading}
        selectedTeam={chatState.selectedTeam}
        onTeamChange={handleTeamChange}
        onSendMessage={streamHandlers.handleSendMessage}
        hasTaskId={!!taskIdFromUrl}
        onPrefillMessage={chatState.setTaskInputMessage}
      />

      {/* Pipeline Stage Indicator - shows current stage progress for pipeline mode */}
      {hasMessages && selectedTaskDetail?.id && (
        <PipelineStageIndicator
          taskId={selectedTaskDetail.id}
          taskStatus={selectedTaskDetail.status || null}
          collaborationModel={
            selectedTaskDetail.team?.workflow?.mode || chatState.selectedTeam?.workflow?.mode
          }
          onStageInfoChange={setPipelineStageInfo}
          canContinueToNextStage={pipelineNextStepDraft.canSubmit}
          onNextStepClick={handlePipelineNextStepClick}
        />
      )}

      {/* Messages Area: always mounted to keep scroll container stable */}
      <div className={hasMessages ? 'relative flex-1 min-h-0' : 'relative'}>
        {/* Top gradient fade effect - limited width to avoid overlapping scrollbar */}
        {hasMessages && (
          <div
            className="absolute top-0 left-0 h-8 z-10 pointer-events-none"
            style={{
              width: 'calc(100% - 12px)',
              background:
                'linear-gradient(to bottom, rgb(var(--color-bg-base)) 0%, rgb(var(--color-bg-base) / 0.6) 50%, rgb(var(--color-bg-base) / 0) 100%)',
            }}
          />
        )}
        {/* Scrollbar markers - shows user message positions on the scrollbar track */}
        <ScrollbarMarkers scrollContainerRef={scrollContainerRef} visible={hasMessages} />
        <div
          ref={scrollContainerRef}
          className={
            (hasMessages ? 'h-full overflow-y-auto custom-scrollbar' : 'overflow-y-hidden') +
            ' transition-opacity duration-200 ' +
            (hasMessages ? 'opacity-100' : 'opacity-0 pointer-events-none h-0')
          }
          aria-hidden={!hasMessages}
          style={{ paddingBottom: hasMessages ? `${inputHeight + 16}px` : '0' }}
        >
          <div className="w-full max-w-5xl mx-auto px-4 sm:px-6 pt-12">
            <MessagesArea
              selectedTeam={chatState.selectedTeam}
              selectedRepo={chatState.selectedRepo}
              selectedBranch={chatState.selectedBranch}
              onContentChange={handleMessagesContentChange}
              onShareButtonRender={onShareButtonRender}
              onSendMessage={handleSendMessageFromChild}
              onSendMessageWithModel={handleSendMessageWithModelFromChild}
              isGroupChat={selectedTaskDetail?.is_group_chat || false}
              onRetry={handleRetryFromMessagesArea}
              onRetryWithModel={handleRetryWithModelFromMessagesArea}
              enableCorrectionMode={chatState.enableCorrectionMode}
              correctionModelId={chatState.correctionModelId}
              enableCorrectionWebSearch={chatState.enableCorrectionWebSearch}
              hasMessages={hasMessages}
              pendingTaskId={streamHandlers.pendingTaskId}
              isPendingConfirmation={pipelineStageInfo?.is_pending_confirmation}
              onContextReselect={handleContextReselect}
              hideGroupChatOptions={taskType === 'knowledge'}
              onUseAsReference={handleUseAsReference}
              onReEdit={handleReEdit}
            />
          </div>
        </div>
      </div>

      {/* Main Content Area */}
      <div
        className={
          hasMessages
            ? 'w-full'
            : inputAlwaysAtBottom
              ? 'w-full flex-1 relative'
              : 'flex-1 flex flex-col w-full'
        }
      >
        {/* Center area for input when no messages (and not in inputAlwaysAtBottom mode) */}
        {!hasMessages && !inputAlwaysAtBottom && (
          <div
            className="flex-1 flex items-center justify-center w-full"
            style={{ marginBottom: '12vh' }}
          >
            <div ref={floatingInputRef} className="w-full max-w-4xl mx-auto px-4 sm:px-6">
              {taskType !== 'knowledge' && (
                <SloganDisplay slogan={chatState.randomSlogan} project={activeProject} />
              )}
              {taskType === 'knowledge' && guidedQuestions && guidedQuestions.length > 0 && (
                <GuidedQuestions
                  questions={guidedQuestions}
                  onQuestionClick={question => chatState.setTaskInputMessage(question)}
                />
              )}
              <ChatInputCard
                {...inputCardProps}
                autoFocus={!hasMessages}
                inputControlsRef={inputControlsRef}
              />
              {taskType !== 'knowledge' && !hideSelectors && !activeProject && (
                <QuickAccessCards
                  teams={teams}
                  selectedTeam={chatState.selectedTeam}
                  onTeamSelect={handleTeamSelect}
                  currentMode={taskType}
                  isLoading={isTeamsLoading}
                  isTeamsLoading={isTeamsLoading}
                  hideSelected={true}
                  onRefreshTeams={onRefreshTeams}
                  showWizardButton={taskType === 'chat'}
                  defaultTeam={chatState.defaultTeam}
                />
              )}
            </div>
          </div>
        )}
        {/* Empty state content for inputAlwaysAtBottom mode (e.g., KnowledgeBaseSummaryCard in notebook mode) */}
        {/* Uses absolute positioning with inset to create a scrollable area that respects the floating input */}
        {!hasMessages && inputAlwaysAtBottom && (
          <div
            className="absolute inset-0 flex flex-col items-center w-full px-4 sm:px-6 overflow-y-auto pt-4"
            style={{
              // Reserve space for: GuidedQuestions (~200px max) + ChatInputCard (~120px) + padding (32px)
              // This prevents overlap between summary card and guided questions on smaller screens
              paddingBottom: guidedQuestions && guidedQuestions.length > 0 ? '352px' : '152px',
            }}
          >
            {emptyStateContent}
          </div>
        )}

        {/* Floating Input Area for messages view or inputAlwaysAtBottom mode */}
        {/* Width is reduced by 12px to avoid overlapping the scrollbar */}
        {(hasMessages || inputAlwaysAtBottom) && (
          <div
            ref={floatingInputRef}
            className="fixed bottom-0 z-50 bg-base"
            style={{
              left: floatingMetrics.left,
              width:
                floatingMetrics.width > 14 ? floatingMetrics.width - 14 : floatingMetrics.width,
            }}
          >
            {/* Top gradient fade effect - creates smooth transition from content to floating input area */}
            {/* For inputAlwaysAtBottom mode without messages, this prevents hard edge with summary card */}
            {/* Width is limited to avoid overlapping the scrollbar (12px reserved for scrollbar) */}
            <div
              className="absolute top-0 h-8 -translate-y-full pointer-events-none"
              style={{
                left: 0,
                width: 'calc(100% - 12px)',
                background:
                  'linear-gradient(to top, rgb(var(--color-bg-base)) 0%, rgb(var(--color-bg-base) / 0.6) 50%, rgb(var(--color-bg-base) / 0) 100%)',
              }}
            />
            {/* Scroll to bottom indicator */}
            {hasMessages && (
              <div className="absolute -top-2 left-1/2 -translate-x-1/2 -translate-y-full pointer-events-auto">
                <ScrollToBottomIndicator
                  visible={showScrollIndicator}
                  onClick={() => scrollToBottom(true)}
                />
              </div>
            )}
            {/* Guided questions for knowledge notebook mode - displayed above input card */}
            {!hasMessages &&
              inputAlwaysAtBottom &&
              taskType === 'knowledge' &&
              guidedQuestions &&
              guidedQuestions.length > 0 && (
                <div className="w-full max-w-[820px] mx-auto px-4 sm:px-6 pt-4 pb-6">
                  <GuidedQuestions
                    questions={guidedQuestions}
                    onQuestionClick={question => chatState.setTaskInputMessage(question)}
                  />
                </div>
              )}
            <div className="relative w-full max-w-[820px] mx-auto px-4 sm:px-6">
              <div className="py-4">
                <ChatInputCard
                  {...inputCardProps}
                  autoFocus={!hasMessages && inputAlwaysAtBottom}
                />
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Team Edit Dialog - rendered via extension if provided */}
      {selectedTaskDetail?.id && (
        <PipelineNextStepDialog
          open={isPipelineNextStepOpen}
          messages={pipelineNextStepMessages}
          isConfirming={isPipelineNextStepConfirming}
          onOpenChange={setIsPipelineNextStepOpen}
          onConfirm={handlePipelineNextStepConfirm}
        />
      )}
      <AlertDialog
        open={isPendingReplacementOpen}
        onOpenChange={open => {
          if (isPendingReplacementConfirming) return
          setIsPendingReplacementOpen(open)
          if (!open) {
            setPendingReplacementMessage(null)
          }
        }}
      >
        <AlertDialogContent className="w-[520px] max-w-[calc(100vw-32px)] gap-0 overflow-hidden rounded-2xl border-border bg-base p-0 shadow-2xl">
          <div className="flex items-center gap-3 border-b border-border px-4 py-3">
            <Command className="h-4 w-4 text-text-secondary" />
            <span className="text-sm text-text-secondary">
              {t('chat:pending_task_confirm.eyebrow')}
            </span>
            <AlertDialogCancel
              disabled={isPendingReplacementConfirming}
              className="ml-auto mt-0 h-8 w-8 rounded-full border-0 bg-transparent p-0 text-text-muted hover:bg-surface hover:text-text-primary"
            >
              <X className="h-4 w-4" />
              <span className="sr-only">{t('chat:pending_task_confirm.wait')}</span>
            </AlertDialogCancel>
          </div>
          <AlertDialogHeader className="space-y-0 px-5 py-5 text-left">
            <div className="flex items-start gap-3 rounded-xl bg-surface p-4">
              <MessageSquareText className="mt-0.5 h-5 w-5 shrink-0 text-primary" />
              <div>
                <AlertDialogTitle className="text-sm font-semibold text-text-primary">
                  {t('chat:pending_task_confirm.title')}
                </AlertDialogTitle>
                <AlertDialogDescription className="mt-1 text-sm leading-6 text-text-secondary">
                  {t('chat:pending_task_confirm.description')}
                </AlertDialogDescription>
              </div>
            </div>
          </AlertDialogHeader>
          <AlertDialogFooter className="grid grid-cols-2 gap-2 px-5 pb-5 pt-0 sm:space-x-0">
            <AlertDialogCancel
              disabled={isPendingReplacementConfirming}
              className="mt-0 flex h-auto flex-col items-start justify-start gap-0 rounded-xl border-border bg-base px-4 py-3 text-left hover:bg-surface"
            >
              <span className="block text-sm font-medium text-text-primary">
                {t('chat:pending_task_confirm.wait')}
              </span>
              <span className="mt-1 block text-xs font-normal text-text-secondary">
                {t('chat:pending_task_confirm.wait_hint')}
              </span>
            </AlertDialogCancel>
            <AlertDialogAction
              onClick={event => {
                event.preventDefault()
                void handleConfirmPendingReplacement()
              }}
              disabled={isPendingReplacementConfirming}
              className="flex h-auto flex-col items-start justify-start gap-0 rounded-xl border border-primary bg-primary/8 px-4 py-3 text-left hover:bg-primary/12"
            >
              <span className="block text-sm font-medium text-primary">
                {isPendingReplacementConfirming
                  ? t('chat:pending_task_confirm.confirming')
                  : t('chat:pending_task_confirm.confirm')}
              </span>
              <span className="mt-1 block text-xs font-normal text-text-secondary">
                {t('chat:pending_task_confirm.confirm_hint')}
              </span>
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      {extension?.teamEdit?.renderDialog()}
    </div>
  )
}

/**
 * ChatArea Component
 *
 * Main chat interface component that wraps ChatAreaContent with QuoteProvider
 * to enable text selection quoting functionality.
 */
export default function ChatArea(props: ChatAreaProps) {
  return (
    <QuoteProvider>
      <SelectionTooltip />
      <ChatAreaContent {...props} />
    </QuoteProvider>
  )
}
