import { Fragment, useEffect, useLayoutEffect, useRef, useState } from "react";
import { ConversationTurn } from "./ConversationTurn.jsx";
import {
  conversationDirectSendSpacerHeight,
  conversationDirectSendScrollGuard,
  conversationDirectSendMaxScrollTop,
  conversationScrollTop,
  conversationTurnAnchorScrollTop,
  isNearConversationBottom,
  shouldAnchorConversationSend,
  shouldReleaseConversationDirectSendGuard,
  shouldScrollConversation
} from "./conversation-scroll.js";
import { loadConversationScroll, saveConversationScroll } from "./conversation-scroll-persistence.js";
import { ComposerModelMenu } from "./ComposerModelMenu.jsx";
import { ComposerPermissionMenu } from "./ComposerPermissionMenu.jsx";
import { ComposerToolsMenu } from "./ComposerToolsMenu.jsx";
import { AttachmentLoadingIcon, BackIcon, CloseIcon, LatestMessageIcon, MicrophoneIcon, ModelIcon, SendIcon, StopIcon } from "../../shared/ActionIcons.jsx";
import { ComposerTextInput } from "./ComposerTextInput.jsx";
import { QueuedMessageList } from "./QueuedMessageList.jsx";
import { appendVoiceTranscript, buildVoiceWaveBars, createVoiceRecognition, supportsVoiceInput, voiceInputErrorKey } from "./voice-input.js";
import { setupErrorMessage } from "../setup/setup-errors.js";
import { useI18n } from "../../shared/I18nProvider.jsx";
import { WorkspaceAttachPicker } from "./WorkspaceAttachPicker.jsx";
import { AccountHandoffDialog } from "./AccountHandoffDialog.jsx";
import { accountHandoffRequestIsCurrent, shouldConfirmAccountHandoff } from "./account-handoff.js";
import { selectAttachmentUploads } from "./attachment-policy.js";
import { conversationWindow, DEFAULT_VISIBLE_CONVERSATION_TURNS } from "./conversation-window.js";

const VOICE_METER_FRAME_MS = 50;

function VoiceWaveform({ bars }) {
  return <div className="voice-waveform" aria-hidden="true">
    {bars.map((bar, index) => <span className="voice-waveform-bar" key={index} style={{ "--voice-level": bar }} />)}
  </div>;
}

function ConversationLiveIndicator({ label }) {
  return <div className="conversation-live-indicator" role="status" aria-label={label}>
    <span className="conversation-live-wave" aria-hidden="true">
      {[0, 1, 2].map((item) => <span key={item}></span>)}
    </span>
  </div>;
}

function ModelChangeNotice({ from, to }) {
  const { t } = useI18n();
  return <div className="model-change-notice" role="status" aria-live="polite">
    <div className="model-change-content">
      <ModelIcon className="model-change-icon" />
      <div className="model-change-copy">
        <strong className="model-change-title">{t("conversation.modelChangedTitle")}</strong>
        <span className="model-change-transition">{t("conversation.modelChangedFromTo", { from, to })}</span>
        <span className="model-change-warning">{t("conversation.modelChangedPerformance")}</span>
        <span className="model-change-warning">{t("conversation.modelChangedCompaction")}</span>
      </div>
    </div>
  </div>;
}

export function ConversationPanel({
  session,
  title,
  conversation,
  loading,
  transition,
  changedFiles,
  now,
  message,
  capabilities,
  composerOptions,
  attachments,
  selectedSkills,
  workspaces = [],
  workspaceId = "",
  submitPending,
  stopPending,
  queuedActionPending,
  approvalDecisionPending,
  onBack,
  onTransitionRetry,
  onMessageChange,
  onSend,
  onStop,
  onSteerQueued,
  onDeleteQueued,
  onDecision,
  onOpenChangedFile,
  onAttach,
  onWorkspaceChange,
  onRegisterProject,
  onUploadAttachment,
  onRemoveAttachment,
  onComposerOption,
  onToggleSkill
}) {
  const { t } = useI18n();
  const inputRef = useRef(null);
  const fileRef = useRef(null);
  const eventListRef = useRef(null);
  const sessionIdRef = useRef(null);
  const turnCountRef = useRef(0);
  const followsLatestRef = useRef(true);
  const scrollTopRef = useRef(0);
  const pendingScrollRef = useRef(null);
  const pendingPrependScrollRef = useRef(null);
  const pendingDirectSendAnchorRef = useRef(null);
  const directSendScrollGuardRef = useRef(null);
  const voiceRef = useRef(null);
  const voiceMeterRef = useRef(null);
  const voiceMeterTokenRef = useRef(null);
  const voiceCompletionActionRef = useRef("");
  const scrollLatestAnimationRef = useRef(null);
  const voiceBaseMessageRef = useRef("");
  const voiceTranscriptRef = useRef("");
  const pendingSendMessageRef = useRef(undefined);
  const composerMessageRef = useRef(message);
  const [sendError, setSendError] = useState("");
  const [voiceSupported] = useState(() => supportsVoiceInput());
  const [voiceStatus, setVoiceStatus] = useState("");
  const [voiceDiagnostic, setVoiceDiagnostic] = useState("");
  const [voiceBars, setVoiceBars] = useState(() => buildVoiceWaveBars(0));
  const [voiceTranscript, setVoiceTranscript] = useState("");
  const [inputFocused, setInputFocused] = useState(false);
  const [composerHasText, setComposerHasText] = useState(() => Boolean(message.trim()));
  const [attachPickerOpen, setAttachPickerOpen] = useState(false);
  const [handoffRequest, setHandoffRequest] = useState(null);
  const [modelChangeNotices, setModelChangeNotices] = useState([]);
  const [showScrollLatest, setShowScrollLatest] = useState(false);
  const [directSendSpaceSessionId, setDirectSendSpaceSessionId] = useState(null);
  const [conversationWindowState, setConversationWindowState] = useState(() => ({ sessionId: session?.id, count: DEFAULT_VISIBLE_CONVERSATION_TURNS }));
  const visibleTurnCount = conversationWindowState.sessionId === session?.id
    ? conversationWindowState.count
    : DEFAULT_VISIBLE_CONVERSATION_TURNS;
  const { hiddenTurnCount, visibleTurns } = conversationWindow(conversation, visibleTurnCount);
  const accountId = session?.selectedExecutionAccountId;
  const accounts = session?.executionAccounts ?? [];
  const activeTurnRunning = session?.status === "running";
  const activeTurnThinking = session?.status === "thinking";
  const activeTurnInProgress = activeTurnRunning || activeTurnThinking || session?.status === "starting";
  const transitionPending = transition?.status === "loading";
  const existingChatTransitionPending = transitionPending && Boolean(transition?.target?.codexThreadId);
  const newChatTransitionPending = transitionPending && !transition?.target?.codexThreadId;
  const emptyNewChatTransitionPending = newChatTransitionPending && conversation.length === 0;
  const transitionActive = Boolean(transition);
  const controlsBusy = transitionActive || !session || !session.workspaceId || activeTurnInProgress || session.status === "waiting_approval";
  const messageBlocked = transitionActive || !session || !session.workspaceId || session.status === "waiting_approval";
  const uploadDisabled = transitionActive || !session || !session.workspaceId;
  const models = capabilities?.models ?? [];
  const skills = capabilities?.skills ?? [];
  const submitLabel = submitPending === "queue" ? t("conversation.queueing") : submitPending === "send" ? t("conversation.sending") : t("conversation.send");
  const attachmentsPending = attachments.some((item) => item.pending);
  const hasComposerContent = composerHasText || attachments.length > 0;
  const composerEngaged = inputFocused || hasComposerContent;
  const listening = voiceStatus === "listening";
  const canSubmit = listening ? voiceTranscript.trim().length > 0 : !attachmentsPending && hasComposerContent;
  const stopActionVisible = activeTurnInProgress
    && (session?.status === "starting" || submitPending !== "send")
    && !hasComposerContent && !listening;
  const actionLabel = stopActionVisible ? (stopPending ? t("conversation.stopping") : t("conversation.stop")) : submitLabel;
  const showVoiceButton = !activeTurnInProgress && voiceSupported && !hasComposerContent && !listening;

  async function performSend(messageOverride) {
    setSendError("");
    let directSendAnchor = null;
    if (session?.id && shouldAnchorConversationSend(session?.status)) {
      const eventList = eventListRef.current;
      directSendAnchor = {
        sessionId: session.id,
        preserveSpace: directSendSpaceSessionId === session.id,
        previousSpacerHeight: eventList?.style.getPropertyValue("--direct-send-response-space") ?? ""
      };
      eventList?.style.setProperty("--direct-send-response-space", "0px");
      pendingDirectSendAnchorRef.current = directSendAnchor;
      setDirectSendSpaceSessionId(session.id);
    }
    try {
      await onSend(messageOverride);
    } catch (error) {
      if (pendingDirectSendAnchorRef.current === directSendAnchor) pendingDirectSendAnchorRef.current = null;
      if (directSendAnchor && !directSendAnchor.preserveSpace) {
        setDirectSendSpaceSessionId((current) => current === directSendAnchor.sessionId ? null : current);
      }
      if (directSendAnchor?.preserveSpace) {
        eventListRef.current?.style.setProperty("--direct-send-response-space", directSendAnchor.previousSpacerHeight);
      } else {
        eventListRef.current?.style.removeProperty("--direct-send-response-space");
      }
      setSendError(setupErrorMessage(error, "chat_message_failed"));
    }
  }

  function requestSend(messageOverride) {
    if (shouldConfirmAccountHandoff(session?.accountId, accountId)) {
      pendingSendMessageRef.current = messageOverride;
      setHandoffRequest({ sessionId: session.id, fromAccountId: session.accountId, toAccountId: accountId });
      return;
    }
    return performSend(messageOverride);
  }

  async function submit(messageOverride = composerMessageRef.current) {
    setSendError("");
    if (listening) return finishVoice("send");
    return requestSend(messageOverride);
  }

  function replaceComposerMessage(nextMessage) {
    composerMessageRef.current = nextMessage;
    setComposerHasText(Boolean(nextMessage.trim()));
    onMessageChange(nextMessage, session?.id);
  }

  function confirmAccountHandoff() {
    if (!accountHandoffRequestIsCurrent(handoffRequest, session?.id, session?.accountId, accountId)) {
      cancelAccountHandoff();
      return;
    }
    setHandoffRequest(null);
    const messageOverride = pendingSendMessageRef.current;
    pendingSendMessageRef.current = undefined;
    void performSend(messageOverride);
  }

  function cancelAccountHandoff() {
    pendingSendMessageRef.current = undefined;
    setHandoffRequest(null);
  }

  async function stop() {
    setSendError("");
    if (listening) finishVoice("draft");
    try {
      await onStop();
    } catch (error) {
      setSendError(setupErrorMessage(error, "stop_failed"));
    }
  }

  async function startVoiceMeter() {
    const mediaDevices = globalThis.navigator?.mediaDevices;
    const AudioContextCtor = globalThis.AudioContext ?? globalThis.webkitAudioContext;
    if (!mediaDevices?.getUserMedia || !AudioContextCtor) return;
    const token = Symbol("voice-meter");
    voiceMeterTokenRef.current = token;
    try {
      const stream = await mediaDevices.getUserMedia({ audio: true });
      const context = new AudioContextCtor();
      if (voiceMeterTokenRef.current !== token) {
        stream.getTracks?.().forEach((track) => track.stop());
        void context.close?.();
        return;
      }
      const analyser = context.createAnalyser();
      analyser.fftSize = 256;
      const source = context.createMediaStreamSource(stream);
      source.connect(analyser);
      const data = new Uint8Array(analyser.fftSize);
      let frame = 0;
      let lastSampleAt = 0;
      const tick = (time = 0) => {
        if (time - lastSampleAt >= VOICE_METER_FRAME_MS) {
          lastSampleAt = time;
          analyser.getByteTimeDomainData(data);
          let sum = 0;
          for (const sample of data) {
            const centered = (sample - 128) / 128;
            sum += centered * centered;
          }
          const rms = Math.sqrt(sum / data.length);
          const level = Math.min(1, rms * 7);
          setVoiceBars((current) => buildVoiceWaveBars(level, current));
        }
        frame = requestAnimationFrame(tick);
      };
      voiceMeterRef.current = { context, frame, stream };
      tick();
    } catch {
      setVoiceBars(buildVoiceWaveBars(0));
    }
  }

  function stopVoiceMeter() {
    const meter = voiceMeterRef.current;
    voiceMeterRef.current = null;
    voiceMeterTokenRef.current = null;
    if (meter?.frame) cancelAnimationFrame(meter.frame);
    meter?.stream?.getTracks?.().forEach((track) => track.stop());
    void meter?.context?.close?.();
    setVoiceBars(buildVoiceWaveBars(0));
  }

  function startVoice() {
    if (!voiceSupported || messageBlocked) return;
    setSendError("");
    setVoiceDiagnostic("starting");
    setVoiceBars(buildVoiceWaveBars(0));
    voiceCompletionActionRef.current = "";
    voiceBaseMessageRef.current = composerMessageRef.current;
    voiceTranscriptRef.current = "";
    setVoiceTranscript("");
    const voice = createVoiceRecognition({
      onDiagnostic: (status) => setVoiceDiagnostic(status),
      onTranscript: (text) => {
        voiceTranscriptRef.current = text;
        setVoiceTranscript(text);
      },
      onError: (error) => {
        voiceCompletionActionRef.current = "";
        const completedMessage = appendVoiceTranscript(voiceBaseMessageRef.current, voiceTranscriptRef.current);
        if (completedMessage) replaceComposerMessage(completedMessage);
        voiceTranscriptRef.current = "";
        setVoiceTranscript("");
        setVoiceDiagnostic("");
        setSendError(t(voiceInputErrorKey(error)));
        setVoiceStatus("");
        stopVoiceMeter();
      },
      onEnd: () => {
        const shouldSend = voiceCompletionActionRef.current === "send";
        const completedMessage = appendVoiceTranscript(voiceBaseMessageRef.current, voiceTranscriptRef.current);
        voiceCompletionActionRef.current = "";
        voiceTranscriptRef.current = "";
        setVoiceTranscript("");
        setVoiceStatus("");
        voiceRef.current = null;
        if (completedMessage.trim()) {
          if (shouldSend) void requestSend(completedMessage);
          else replaceComposerMessage(completedMessage);
        }
        stopVoiceMeter();
        requestAnimationFrame(() => inputRef.current?.focus());
      }
    });
    if (!voice.supported) {
      setVoiceDiagnostic("error:not_supported");
      return;
    }
    voiceRef.current = voice;
    setVoiceStatus("listening");
    try {
      voice.start();
    } catch {
      setVoiceDiagnostic("error:start_failed");
      setVoiceStatus("");
      voiceRef.current = null;
      stopVoiceMeter();
    }
  }
  function finishVoice(action = "draft") {
    if (voiceCompletionActionRef.current) return;
    const voice = voiceRef.current;
    if (!voice) return;
    voiceCompletionActionRef.current = action;
    voice?.stop();
  }

  function showEarlierConversation() {
    const eventList = eventListRef.current;
    if (eventList) {
      pendingPrependScrollRef.current = {
        sessionId: session?.id,
        scrollHeight: eventList.scrollHeight,
        scrollTop: eventList.scrollTop
      };
    }
    setConversationWindowState({ sessionId: session?.id, count: visibleTurnCount + DEFAULT_VISIBLE_CONVERSATION_TURNS });
  }

  function animateConversationScroll(viewport, target, onComplete) {
    if (scrollLatestAnimationRef.current !== null) cancelAnimationFrame(scrollLatestAnimationRef.current);
    const start = viewport.scrollTop;
    let startedAt;
    const animate = (timestamp) => {
      if (startedAt === undefined) startedAt = timestamp;
      const elapsed = timestamp - startedAt;
      viewport.scrollTop = conversationScrollTop(start, target, elapsed, 220);
      scrollTopRef.current = viewport.scrollTop;
      if (elapsed < 220) {
        scrollLatestAnimationRef.current = requestAnimationFrame(animate);
        return;
      }
      viewport.scrollTop = target;
      scrollTopRef.current = target;
      onComplete?.();
      scrollLatestAnimationRef.current = null;
    };
    scrollLatestAnimationRef.current = requestAnimationFrame(animate);
  }

  function scrollToLatest() {
    const viewport = eventListRef.current;
    if (!viewport) return;
    const target = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
    animateConversationScroll(viewport, target, () => {
      followsLatestRef.current = true;
      setShowScrollLatest(false);
    });
  }

  function recordModelChange(previousModel, nextModel) {
    const afterTurnId = conversation.at(-1)?.id;
    if (!session?.id || !afterTurnId || !previousModel || !nextModel) return;
    setModelChangeNotices((current) => [...current, {
      id: crypto.randomUUID(),
      sessionId: session.id,
      afterTurnId,
      from: previousModel.displayName || previousModel.model,
      to: nextModel.displayName || nextModel.model
    }]);
  }

  useLayoutEffect(() => {
    const chat = eventListRef.current?.parentElement;
    const composerDock = chat?.querySelector(".composer-dock");
    if (!chat || !composerDock) return undefined;
    const updateComposerDockHeight = () => {
      const height = composerDock.getBoundingClientRect().height;
      if (height > 0) chat.style.setProperty("--composer-dock-height", `${Math.ceil(height)}px`);
    };
    updateComposerDockHeight();
    const observer = new ResizeObserver(updateComposerDockHeight);
    observer.observe(composerDock);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    const viewport = eventListRef.current;
    if (!viewport) return undefined;
    const updateLatestControl = () => {
      const nearBottom = isNearConversationBottom(viewport);
      setShowScrollLatest(!nearBottom);
    };
    updateLatestControl();
    viewport.addEventListener("scroll", updateLatestControl, { passive: true });
    return () => viewport.removeEventListener("scroll", updateLatestControl);
  }, [session?.id]);

  useLayoutEffect(() => {
    const eventList = eventListRef.current;
    if (!eventList) return;
    const pendingPrepend = pendingPrependScrollRef.current;
    if (pendingPrepend?.sessionId === session?.id) {
      eventList.scrollTop = pendingPrepend.scrollTop + eventList.scrollHeight - pendingPrepend.scrollHeight;
      scrollTopRef.current = eventList.scrollTop;
      followsLatestRef.current = isNearConversationBottom(eventList);
      pendingPrependScrollRef.current = null;
      sessionIdRef.current = session?.id;
      turnCountRef.current = conversation.length;
      return;
    }
    if (pendingPrepend) pendingPrependScrollRef.current = null;
    const sessionChanged = sessionIdRef.current !== session?.id;
    const turnAdded = !sessionChanged && conversation.length > turnCountRef.current;
    if (sessionChanged && sessionIdRef.current) {
      saveConversationScroll(globalThis.localStorage, sessionIdRef.current, scrollTopRef.current);
    }
    const pendingDirectSendAnchor = pendingDirectSendAnchorRef.current;
    if (turnAdded && pendingDirectSendAnchor?.sessionId === session?.id) {
      const latestTurn = eventList.querySelector(".conversation-turn:last-of-type");
      if (latestTurn) {
        const viewportRect = eventList.getBoundingClientRect();
        const target = conversationTurnAnchorScrollTop({
          currentScrollTop: eventList.scrollTop,
          viewportTop: viewportRect.top,
          turnTop: latestTurn.getBoundingClientRect().top
        });
        const spacerHeight = conversationDirectSendSpacerHeight({
          targetScrollTop: target,
          scrollHeight: eventList.scrollHeight,
          clientHeight: eventList.clientHeight
        });
        eventList.style.setProperty("--direct-send-response-space", `${spacerHeight}px`);
        const lastContentBottom = eventList.lastElementChild?.getBoundingClientRect().bottom ?? viewportRect.bottom;
        const initialAvailableSpace = Math.max(0, viewportRect.bottom - lastContentBottom + target - eventList.scrollTop);
        directSendScrollGuardRef.current = conversationDirectSendScrollGuard({
          reservedSpaceHeight: spacerHeight,
          sessionId: session.id,
          anchorScrollTop: target,
          initialScrollHeight: eventList.scrollHeight,
          initialAvailableSpace
        });
        followsLatestRef.current = false;
        setShowScrollLatest(!isNearConversationBottom(eventList));
        pendingDirectSendAnchorRef.current = null;
        sessionIdRef.current = session?.id;
        turnCountRef.current = conversation.length;
        animateConversationScroll(eventList, target);
        return;
      }
    } else if (pendingDirectSendAnchor && pendingDirectSendAnchor.sessionId !== session?.id) {
      pendingDirectSendAnchorRef.current = null;
    }
    if (sessionChanged) pendingScrollRef.current = loadConversationScroll(globalThis.localStorage, session?.id);
    if (pendingScrollRef.current !== null) {
      eventList.scrollTop = pendingScrollRef.current;
      scrollTopRef.current = eventList.scrollTop;
      followsLatestRef.current = isNearConversationBottom(eventList);
      if (conversation.length > 0 || (session?.queuedMessages?.length ?? 0) > 0) pendingScrollRef.current = null;
    } else if (shouldScrollConversation({ sessionChanged, wasNearBottom: followsLatestRef.current, turnAdded })) {
      eventList.scrollTop = eventList.scrollHeight;
      scrollTopRef.current = eventList.scrollTop;
      followsLatestRef.current = true;
    }
    sessionIdRef.current = session?.id;
    turnCountRef.current = conversation.length;
  }, [session?.id, conversation, session?.queuedMessages, visibleTurnCount]);

  useEffect(() => {
    const saveCurrentScroll = () => saveConversationScroll(globalThis.localStorage, sessionIdRef.current, scrollTopRef.current);
    const saveWhenHidden = () => {
      if (globalThis.document?.visibilityState === "hidden") saveCurrentScroll();
    };
    globalThis.addEventListener?.("pagehide", saveCurrentScroll);
    globalThis.document?.addEventListener?.("visibilitychange", saveWhenHidden);
    return () => {
      saveCurrentScroll();
      globalThis.removeEventListener?.("pagehide", saveCurrentScroll);
      globalThis.document?.removeEventListener?.("visibilitychange", saveWhenHidden);
    };
  }, []);

  useEffect(() => () => {
    if (scrollLatestAnimationRef.current !== null) cancelAnimationFrame(scrollLatestAnimationRef.current);
    voiceCompletionActionRef.current = "draft";
    voiceRef.current?.stop();
    stopVoiceMeter();
  }, []);

  useEffect(() => {
    setModelChangeNotices([]);
  }, [session?.id]);

  useLayoutEffect(() => {
    if (directSendSpaceSessionId && directSendSpaceSessionId !== session?.id) {
      setDirectSendSpaceSessionId(null);
      directSendScrollGuardRef.current = null;
      eventListRef.current?.style.removeProperty("--direct-send-response-space");
    }
  }, [session?.id, directSendSpaceSessionId]);

  useLayoutEffect(() => {
    const directSendGuard = directSendScrollGuardRef.current;
    if (!shouldReleaseConversationDirectSendGuard({
      guardSessionId: directSendGuard?.sessionId,
      sessionId: session?.id,
      status: session?.status
    })) return;
    directSendScrollGuardRef.current = null;
    setDirectSendSpaceSessionId((current) => current === session.id ? null : current);
    eventListRef.current?.style.removeProperty("--direct-send-response-space");
  }, [session?.id, session?.status, conversation.length]);

  useLayoutEffect(() => {
    const eventList = eventListRef.current;
    if (!eventList || !followsLatestRef.current || modelChangeNotices.length === 0) return;
    eventList.scrollTop = eventList.scrollHeight;
    scrollTopRef.current = eventList.scrollTop;
  }, [modelChangeNotices]);

  const accountLabel = (id) => accounts.find((account) => account.id === id)?.label || t("conversation.accountFallback");
  const workspacePromptVisible = !loading && !transitionActive && Boolean(session) && !session.workspaceId && !session.projectless;

  return <section className={`chat${workspacePromptVisible ? " chat--workspace-prompt" : ""}`}><AccountHandoffDialog open={Boolean(handoffRequest)} fromAccount={accountLabel(handoffRequest?.fromAccountId)} toAccount={accountLabel(handoffRequest?.toAccountId)} onCancel={cancelAccountHandoff} onConfirm={confirmAccountHandoff} /><header className="conversation-header"><button type="button" className="screen-back conversation-back" aria-label={t("conversation.back")} onClick={onBack}><BackIcon /></button><h1 className="conversation-title" title={title}>{title}</h1><span className="conversation-header-spacer" aria-hidden="true"></span></header><div className={`event-list${directSendSpaceSessionId === session?.id ? " direct-send-response-space" : ""}`} ref={eventListRef} onScroll={(event) => { const viewport = event.currentTarget; const directSendGuard = directSendScrollGuardRef.current; if (directSendGuard?.sessionId === session?.id) { const maxScrollTop = conversationDirectSendMaxScrollTop({ anchorScrollTop: directSendGuard.anchorScrollTop, initialScrollHeight: directSendGuard.initialScrollHeight, currentScrollHeight: viewport.scrollHeight, initialAvailableSpace: directSendGuard.initialAvailableSpace }); if (viewport.scrollTop > maxScrollTop) viewport.scrollTop = maxScrollTop; } scrollTopRef.current = viewport.scrollTop; followsLatestRef.current = isNearConversationBottom(viewport); }}>
    {transitionPending && <div className="conversation-opening-status" role="status" aria-live="polite">{t("transition.opening")}</div>}
    {emptyNewChatTransitionPending && <div className="conversation-new-chat-loading" role="status" aria-busy="true" aria-label={t("transition.opening")}><div className="startup-mark" aria-hidden="true"><span /></div></div>}
    {existingChatTransitionPending && <div className="conversation-loading" role="status" aria-busy="true" aria-label={t("transition.opening")}><span className="chat-transition-spinner" aria-hidden="true"></span></div>}
    {transition?.status === "failed" && <div className="conversation-open-error" role="alert"><strong>{t("transition.failed")}</strong><span>{transition.error || t("transition.requestFailed")}</span><button type="button" onClick={onTransitionRetry}>{t("chats.retry")}</button></div>}
    {loading && <div className="conversation-loading" role="status" aria-busy="true" aria-label={t("transition.opening")}><span className="chat-transition-spinner" aria-hidden="true"></span></div>}
    {!loading && hiddenTurnCount > 0 && <button type="button" className="conversation-show-earlier" onClick={showEarlierConversation}>{t("conversation.showEarlier")}</button>}
    {!loading && visibleTurns.map((turn) => <Fragment key={turn.id}><ConversationTurn turn={turn} now={now} approvalDecisionPending={approvalDecisionPending} onDecision={onDecision} changedFiles={changedFiles} onOpenChangedFile={onOpenChangedFile} onRetry={(text) => { replaceComposerMessage(text); requestAnimationFrame(() => inputRef.current?.focus()); }} />{modelChangeNotices.filter((notice) => notice.sessionId === session?.id && notice.afterTurnId === turn.id).map((notice) => <ModelChangeNotice key={notice.id} from={notice.from} to={notice.to} />)}</Fragment>)}
    {!loading && activeTurnInProgress && <ConversationLiveIndicator label={session?.status === "starting" ? t("activity.started") : t("activity.running")} />}
    {!loading && newChatTransitionPending && conversation.length > 0 && <ConversationLiveIndicator label={t("activity.running")} />}
    {!loading && !transitionActive && session && conversation.length === 0 && (session.queuedMessages?.length ?? 0) === 0 && <p className="empty-chat">{t("conversation.empty")}</p>}
  </div>{showScrollLatest && <button type="button" className={`conversation-scroll-latest${activeTurnInProgress ? " is-live" : ""}`} aria-label={t("conversation.scrollToLatest")} onClick={scrollToLatest}><LatestMessageIcon /></button>}<WorkspaceAttachPicker open={attachPickerOpen} onClose={() => setAttachPickerOpen(false)} onRegisterProject={onRegisterProject} onAttach={async (selectedWorkspaceId) => { onWorkspaceChange(selectedWorkspaceId); await onAttach(selectedWorkspaceId); }} /><div className="composer-dock">
    <QueuedMessageList messages={session?.queuedMessages} pendingAction={queuedActionPending} canSteer={activeTurnRunning} onEdit={async (item) => { await onDeleteQueued(item.id); replaceComposerMessage(item.text); requestAnimationFrame(() => inputRef.current?.focus()); }} onSteer={onSteerQueued} onDelete={onDeleteQueued} />
    <div className="composer">
    <div className="composer-quick-options">
      <ComposerModelMenu models={models} options={composerOptions} disabled={controlsBusy} onOption={onComposerOption} onModelChange={recordModelChange} />
      <ComposerPermissionMenu permissions={capabilities?.permissions ?? []} options={composerOptions} disabled={controlsBusy} onOption={onComposerOption} />
      {composerOptions.planMode && <button className="composer-pill composer-active-chip" type="button" disabled={controlsBusy} onClick={() => onComposerOption("planMode", false)}><span>{t("conversation.plan")} <CloseIcon /></span></button>}
      {selectedSkills.map((skill) => <button className="composer-pill composer-active-chip" key={skill.name} type="button" disabled={controlsBusy} onClick={() => onToggleSkill(skill)}><span>{skill.displayName || skill.name} <CloseIcon /></span></button>)}
    </div>
    {workspacePromptVisible && <div className="composer-workspace-prompt" role="status"><span>{t("conversation.workspaceRequired")}</span><button type="button" onClick={() => setAttachPickerOpen(true)}>{t("conversation.selectProject")}</button></div>}
    {attachments.length > 0 && <div className="composer-attachments">
      {attachments.map((item) => <div className="composer-attachment-card" key={item.id}>
        {item.previewUrl ? <img className="composer-attachment-preview" src={item.previewUrl} alt={item.name || t("conversation.attachedImage")} /> : <span className="composer-attachment-preview">{item.name || t("conversation.image")}</span>}
        {item.pending && <span className="composer-attachment-pending" aria-label={t("conversation.uploading")}><AttachmentLoadingIcon className="composer-attachment-pending-icon" /></span>}
        <button className="composer-attachment-remove" type="button" onClick={() => { void onRemoveAttachment(item.id).catch((error) => setSendError(setupErrorMessage(error, "request_failed"))); }} aria-label={t("conversation.removeImage", { name: item.name || t("conversation.image") })}><CloseIcon /></button>
      </div>)}
    </div>}
    <div className={`composer-row ${listening ? "listening" : ""}`}>
      {!listening && <ComposerToolsMenu busy={controlsBusy} uploadDisabled={uploadDisabled} fileRef={fileRef} planMode={composerOptions.planMode} skills={skills} selectedSkills={selectedSkills} onOption={onComposerOption} onToggleSkill={onToggleSkill} />}
      <input ref={fileRef} className="composer-file" type="file" accept="image/*" multiple onChange={(event) => { const selection = selectAttachmentUploads(attachments, Array.from(event.target.files ?? [])); event.target.value = ""; if (selection.rejected.length) setSendError(setupErrorMessage(new Error("attachment_limit_exceeded"), "upload_failed")); for (const file of selection.accepted) onUploadAttachment(file).catch((error) => setSendError(setupErrorMessage(error, "upload_failed"))); }} />
      <div className={`composer-input-wrap ${listening ? "composer-input-wrap--listening" : ""}`}>
        {listening ? <div className="composer-voice-surface">
          <VoiceWaveform bars={voiceBars} />
          {voiceTranscript.trim() && <span className="voice-transcript">{voiceTranscript.trim()}</span>}
          <button className="composer-voice-stop" type="button" aria-label={t("conversation.stopVoice")} onClick={() => finishVoice("draft")}><span className="composer-stop-icon composer-action-icon" aria-hidden="true"><StopIcon /></span></button>
        </div> : <ComposerTextInput sessionId={session?.id} value={message} inputRef={inputRef} placeholder={t("conversation.message")} disabled={messageBlocked} onDraftChange={onMessageChange} onValueChange={(nextMessage) => { composerMessageRef.current = nextMessage; setSendError(""); }} onPresenceChange={setComposerHasText} onFocus={() => setInputFocused(true)} onBlur={() => setInputFocused(false)} onSubmit={submit} />}
      </div>
      {showVoiceButton ? <button className="composer-voice" type="button" aria-label={t("conversation.startVoice")} onClick={startVoice} disabled={messageBlocked}><span className="composer-mic-icon composer-action-icon" aria-hidden="true"><MicrophoneIcon /></span></button> : <button className="composer-send" aria-label={actionLabel} onClick={() => listening ? finishVoice("send") : stopActionVisible ? void stop() : void submit()} disabled={stopActionVisible ? stopPending : (stopPending || Boolean(submitPending) || messageBlocked || !canSubmit)}>{stopActionVisible ? <span className="composer-stop-icon composer-action-icon" aria-hidden="true"><StopIcon /></span> : <span className="composer-send-icon composer-action-icon" aria-hidden="true"><SendIcon /></span>}</button>}
    </div>
    {voiceDiagnostic && <span className="voice-diagnostic">{voiceDiagnostic}</span>}
    {session?.attachmentUploadFailureCount > 0 && <p className="send-error" role="status">{t("conversation.uploadPartial", { count: session.attachmentUploadFailureCount })}</p>}
    {sendError && <p className="send-error">{sendError}</p>}
    </div>
  </div></section>;
}
