/* Intent Surface — Conversation Anchor: "The Process Is Everything" */

import { useState, useRef, useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
import ReactMarkdown from 'react-markdown';
import { useStore } from '../store/useStore';
import type { SelfModProposal, BuildProposal, BuildFailureReport, ArchitectProposalView, Agent, ChatAttachment } from '../store/types';
import { speakResponse, stripMarkdownForSpeech } from '../audio/voice';
import { startListening, stopListening, isSpeechRecognitionSupported } from '../audio/speechInput';
import {
  armWhisperStt,
  onTranscript,
  onTranscribing,
} from '../audio/whisperStt';
import { useSettingsStore } from '../store/useSettingsStore';
import {
  startWakeWordLoop,
  stopWakeWordLoop,
  getWakeWordState,
  _cancelCurrentCapture,
} from '../audio/wakeWord';
import { WakeWordIndicator } from './WakeWordIndicator';
import { soundEngine } from '../audio/soundEngine';
import { BridgePanel } from './BridgePanel';
import { ViewSwitcher } from './ViewSwitcher';
import { deriveBridgeState } from './glass/ContextRibbon';
import { Diamond, Check, XMark, StatusPending, DiamondOpen, Bullseye, ChevronDown, ChevronRight, Warning, Sparkle } from './icons/Glyphs';
import { AgentAvatarBadge } from './AgentAvatarBadge';

/* ── spring easing ── */
const spring = 'cubic-bezier(0.34, 1.56, 0.64, 1)';

/* ── style helpers ── */
const glass = (opacity = 0.75) => ({
  background: `rgba(10, 10, 18, ${opacity})`,
  backdropFilter: 'blur(16px)',
  WebkitBackdropFilter: 'blur(16px)',
  border: '1px solid rgba(240, 176, 96, 0.15)',
});

/* ── per-message feedback state ── */
type FeedbackStatus = { disabled: boolean; confirmText: React.ReactNode };

export function IntentSurface() {
  const [input, setInput] = useState('');
  const [active, setActive] = useState(false);
  const [feedbackMap, setFeedbackMap] = useState<Record<string, FeedbackStatus>>({});
  const [listening, setListening] = useState(false);
  const [vibeMode, setVibeMode] = useState<Record<string, boolean>>({});
  const [vibeInput, setVibeInput] = useState<Record<string, string>>({});
  const [enrichedSpec, setEnrichedSpec] = useState<Record<string, string | null>>({});
  const [enriching, setEnriching] = useState<Record<string, boolean>>({});
  const [buildCodeExpanded, setBuildCodeExpanded] = useState<Record<string, boolean>>({});
  const [designSpecExpanded, setDesignSpecExpanded] = useState<Record<string, boolean>>({});
  const inputRef = useRef<HTMLInputElement>(null);
  const threadRef = useRef<HTMLDivElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  // Command Surface breathing (AD-392)
  const pillRef = useRef<HTMLDivElement>(null);
  const [pillReceded, setPillReceded] = useState(false);

  // AD-719: @-picker state for multi-agent fan-out
  const [pickerOpen, setPickerOpen] = useState(false);
  const [pickerPrefix, setPickerPrefix] = useState('');
  const [pickerIndex, setPickerIndex] = useState(0);
  const agentsMap = useStore((s) => s.agents);

  // AD-720: image-paste attachments pending for the next send.
  const [pendingAttachments, setPendingAttachments] = useState<ChatAttachment[]>([]);
  const [paperclipTooltipOpen, setPaperclipTooltipOpen] = useState(false);
  const [paperclipTooltipRect, setPaperclipTooltipRect] = useState<{ top: number; right: number } | null>(null);
  // AD-720a (Wave 139): file-picker + drag-drop state.
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [isDragOver, setIsDragOver] = useState(false);

  const chatHistory = useStore((s) => s.chatHistory);
  const activeDag = useStore((s) => s.activeDag);
  const addChatMessage = useStore((s) => s.addChatMessage);
  const processing = useStore((s) => s.processing);
  const pendingRequests = useStore((s) => s.pendingRequests);
  const incPendingRequests = useStore((s) => s.incPendingRequests);
  const decPendingRequests = useStore((s) => s.decPendingRequests);
  const pendingChar = useStore((s) => s.pendingChar);
  const consumePendingChar = useStore((s) => s.consumePendingChar);
  const voiceEnabled = useStore((s) => s.voiceEnabled);
  // AD-705: opt-in wake-word loop. Single-owner lifecycle below.
  const wakeWordEnabled = useStore((s) => s.wakeWordEnabled);
  // AD-705a (Wave 179): opt-in offline STT. Subscription wired in the
  // useEffect block below the wake-word lifecycle.
  const sttEnabled = useSettingsStore((s) =>
    Boolean(s.snapshot?.config?.cognitive?.offline_stt_enabled),
  );
  const [sttTranscribing, setSttTranscribing] = useState(false);
  const transporterProgress = useStore((s) => s.transporterProgress);
  const buildQueue = useStore((s) => s.buildQueue);
  const bridgeOpen = useStore((s) => s.bridgeOpen);
  const agentTasks = useStore((s) => s.agentTasks);
  const notifications = useStore((s) => s.notifications);
  const needsAttentionCount = agentTasks?.filter(t => t.requires_action).length ?? 0;
  const unreadCount = notifications?.filter(n => !n.acknowledged).length ?? 0;
  const badgeCount = needsAttentionCount + unreadCount;

  /* ── consume pending char from global keydown ── */
  useEffect(() => {
    if (pendingChar && !active) {
      const char = consumePendingChar();
      setActive(true);
      setInput(char);
      setTimeout(() => inputRef.current?.focus(), 50);
    }
  }, [pendingChar, active, consumePendingChar]);

  /* ── auto-scroll thread to bottom ── */
  useEffect(() => {
    if (threadRef.current) {
      threadRef.current.scrollTop = threadRef.current.scrollHeight;
    }
  }, [chatHistory, pendingRequests]);

  /* ── scroll to bottom when conversation opens ── */
  useEffect(() => {
    if (active && threadRef.current) {
      setTimeout(() => {
        if (threadRef.current) {
          threadRef.current.scrollTop = threadRef.current.scrollHeight;
        }
      }, 100); // slight delay for expand animation to complete
    }
  }, [active]);

  /* ── click outside to collapse ── */
  useEffect(() => {
    if (!active) return;
    function handleClickOutside(e: MouseEvent) {
      const target = e.target as Node;
      if (containerRef.current && containerRef.current.contains(target)) return;
      // AD-719: the @-mention picker is portal'd to document.body to escape
      // the overflow:hidden of the conversation container, so it lives
      // outside containerRef. Allow clicks inside the picker to NOT collapse
      // the IntentSurface.
      const popover = document.querySelector('[data-testid="at-picker-popover"]');
      if (popover && popover.contains(target)) return;
      setActive(false);
      setInput('');
    }
    // Delay to avoid catching the click that opened
    const timer = setTimeout(() => {
      document.addEventListener('mousedown', handleClickOutside);
    }, 100);
    return () => {
      clearTimeout(timer);
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [active]);

  /* ── Command Surface breathing (AD-392) ── */
  useEffect(() => {
    const bridgeState = deriveBridgeState(agentTasks, notifications);
    if (bridgeState !== 'autonomous' || active) {
      setPillReceded(false);
      return;
    }
    // Track mouse distance to pill center
    function handleMouseMove(e: MouseEvent) {
      if (!pillRef.current) return;
      const rect = pillRef.current.getBoundingClientRect();
      const pillCx = rect.left + rect.width / 2;
      const pillCy = rect.top + rect.height / 2;
      const dist = Math.hypot(e.clientX - pillCx, e.clientY - pillCy);
      setPillReceded(dist > 200);
    }
    // Start receded (mouse presumed far)
    setPillReceded(true);
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, [agentTasks, notifications, active]);

  /* ── AD-705: wake-word loop single-owner mount.
     Off by default; Captain explicitly opts in via DecisionSurface toggle.
     The loop posts routed transcripts back through the standard chat path. */
  useEffect(() => {
    if (!wakeWordEnabled) return;
    let cancelled = false;
    // AD-718c E6: collect per-agent wake phrases from the live store map.
    // Empty / missing wake_phrase fields are skipped, so no trigger is
    // registered when an agent has not authored one.
    const agentTriggers = Array.from(agentsMap.values())
      .filter((a) => {
        const wp = a.voice_profile?.wake_phrase;
        return typeof wp === 'string' && wp.length > 0;
      })
      .map((a) => ({
        callsign: a.callsign,
        phrase: a.voice_profile!.wake_phrase!,
      }));
    const onWake = (routed: { surface: 'system' | 'agent'; agentCallsign?: string; cleanedText: string }): void => {
      if (cancelled) return;
      const text = (routed.cleanedText || '').trim();
      if (!text) return;
      // AD-733c-3: fire-and-forget engage. ONLY for agent-surface wakes --
      // a system-surface wake ("computer ...") does not imply engagement
      // with any particular agent's perception. Failure is non-blocking;
      // the chat submit proceeds regardless.
      if (routed.surface === 'agent' && routed.agentCallsign) {
        void fetch('/api/perception/engage', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            agent: routed.agentCallsign,
            phrase: routed.cleanedText.slice(0, 64),
            source: 'wake_word',
          }),
        }).catch((err) => {
          // eslint-disable-next-line no-console
          console.warn('[AD-733c-3] /api/perception/engage failed', err);
        });
      }
      // For agent-routed wakes prepend @callsign so the existing chat path
      // dispatches to that agent (mirrors the manual `@`-mention pattern).
      const finalText =
        routed.surface === 'agent' && routed.agentCallsign
          ? `@${routed.agentCallsign} ${text}`
          : text;
      setActive(true);
      setInput(finalText);
      // Submit on the next tick so React commits the input before the form
      // reads it (mirrors the click-to-talk pattern below).
      setTimeout(() => {
        const form = inputRef.current?.closest('form');
        if (form) form.requestSubmit();
      }, 50);
    };
    void startWakeWordLoop(onWake, { agentTriggers });
    return () => {
      cancelled = true;
      stopWakeWordLoop();
    };
  }, [wakeWordEnabled, agentsMap]);

  /* ── AD-705a: offline STT subscription.
     Arms only when ``cognitive.offline_stt_enabled = true``. Transcripts
     dispatch through the same handleSubmit path keyboard input takes —
     audio bytes never leave the browser (privacy invariant). */
  useEffect(() => {
    if (!sttEnabled) return;
    const unarm = armWhisperStt();
    const unsubTranscript = onTranscript((text: string) => {
      const trimmed = (text || '').trim();
      if (!trimmed) return;
      setActive(true);
      setInput(trimmed);
      setTimeout(() => {
        const form = inputRef.current?.closest('form');
        if (form) form.requestSubmit();
      }, 50);
    });
    const unsubTranscribing = onTranscribing((active: boolean) => {
      setSttTranscribing(active);
    });
    return () => {
      try { unsubTranscript(); } catch { /* Tier-2 */ }
      try { unsubTranscribing(); } catch { /* Tier-2 */ }
      try { unarm(); } catch { /* Tier-2 */ }
    };
  }, [sttEnabled]);

  /* ── DAG progress text ── */
  const dagProgress = activeDag && activeDag.length > 0
    ? (() => {
        const done = activeDag.filter((n) => n.status === 'completed').length;
        return <><Diamond size={10} /> {done}/{activeDag.length} tasks</>;
      })()
    : null;

  /* ── message count for badge ── */
  const msgCount = chatHistory.length;

  /* ── pill click ── */
  function handlePillClick() {
    if (active) return;
    setActive(true);
    setTimeout(() => inputRef.current?.focus(), 50);
  }

  /* ── submit (non-blocking) ── */
  function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const text = input.trim();
    if (!text) return;

    // AD-720b: in-chat tool capability grant.
    // Format: `/grant <agent_id> <tool_id> <permission> [hours]`. Parsed client-side
    // and routed to /api/chat/tool-grant; the command is NOT sent as a chat message.
    if (text.startsWith('/grant ')) {
      const parts = text.slice(7).trim().split(/\s+/);
      if (parts.length < 3 || parts.length > 4) {
        addChatMessage('system', '/grant usage: /grant <agent_id> <tool_id> <permission> [hours]');
        return;
      }
      const [agentId, toolId, permission, hoursStr] = parts;
      const duration_hours = hoursStr ? Number(hoursStr) : null;
      if (hoursStr && (Number.isNaN(duration_hours as number) || (duration_hours as number) < 0)) {
        addChatMessage('system', `/grant: invalid hours "${hoursStr}"`);
        return;
      }
      setInput('');
      fetch('/api/chat/tool-grant', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          agent_id: agentId,
          tool_id: toolId,
          permission,
          duration_hours,
        }),
      })
        .then(async (r) => {
          if (!r.ok) {
            let reason = `HTTP ${r.status}`;
            try {
              const body = await r.json();
              if (body?.detail?.reason) reason = body.detail.reason;
              else if (typeof body?.detail === 'string') reason = body.detail;
            } catch { /* swallow */ }
            // Preserve typed text so the Captain can correct it.
            setInput(text);
            addChatMessage('system', `/grant rejected: ${reason}`);
            return;
          }
          const body = await r.json();
          const exp = body.expires_at
            ? ` (expires in ${Math.round((body.expires_at - Date.now() / 1000) / 3600)}h)`
            : ' (no expiry)';
          addChatMessage(
            'system',
            `Granted ${body.tool_id} ${body.permission} to ${body.agent_id}${exp}`,
          );
        })
        .catch((err) => {
          setInput(text);
          addChatMessage('system', `/grant error: ${String(err?.message || err)}`);
        });
      return;
    }

    addChatMessage('user', text);
    setInput('');
    incPendingRequests();
    useStore.setState({ activeDag: [] }); // Clear stale DAG so neural pulse shows

    // Send last 10 messages as conversation context for reference resolution
    const recentHistory = chatHistory.slice(-10).map(m => ({
      role: m.role,
      text: m.text.slice(0, 300),
    }));

    // Fire-and-forget — user can keep typing immediately
    const attachmentIds = pendingAttachments.map((a) => a.attachment_id);
    setPendingAttachments([]);
    fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: text, history: recentHistory, attachment_ids: attachmentIds }),
    })
      .then((res) => res.json())
      .then((data) => {
        // AD-719: multi-agent fan-out branch — render one ChatMessage per
        // attributed reply. When per_agent_replies is non-empty, the
        // top-level `response` is intentionally empty.
        const replies = Array.isArray(data.per_agent_replies) ? data.per_agent_replies : [];
        if (replies.length > 0) {
          for (const r of replies as Array<{ agent_id: string; callsign: string; text: string }>) {
            addChatMessage('agent', r.text, { agent_id: r.agent_id, callsign: r.callsign });
          }
          if (voiceEnabled && replies[0]?.text && !replies[0].text.startsWith('(')) {
            speakResponse(stripMarkdownForSpeech(replies[0].text));
          }
          soundEngine.playIntentRouting();
          return;
        }

        const response = data.response
          || data.reflection
          || (data.results && Object.keys(data.results).length > 0
              ? Object.values(data.results as Record<string, { output?: string }>)
                  .map((r) => r.output).filter(Boolean).join('\n') || 'Done.'
              : '')
          || (data.correction ? `Correction applied: ${data.correction.changes || 'OK'}` : '')
          || '(No response)';

        if (data.self_mod_proposal) {
          addChatMessage('system', response, {
            selfModProposal: data.self_mod_proposal as SelfModProposal,
          });
        } else {
          addChatMessage('system', response);
        }
        if (voiceEnabled && response && !response.startsWith('(')) {
          // Strip markdown formatting for cleaner TTS (AD-718: shared helper)
          speakResponse(stripMarkdownForSpeech(response));
        }
        soundEngine.playIntentRouting();
      })
      .catch(() => {
        addChatMessage('system', '(Request failed or timed out)');
      })
      .finally(() => {
        decPendingRequests();
      });
  }

  // AD-719c: scroll the highlighted picker row into view as ArrowUp/ArrowDown
  // advance. Uses a guard for `scrollIntoView` because JSDOM does not
  // implement it; tests assert `pickerIndex` state advancement, not scroll
  // behavior, so the guard prevents non-deterministic JSDOM errors.
  useEffect(() => {
    if (!pickerOpen) return;
    const el = document.querySelector(`[data-picker-index="${pickerIndex}"]`);
    if (el && typeof (el as HTMLElement).scrollIntoView === 'function') {
      (el as HTMLElement).scrollIntoView({ block: 'nearest', behavior: 'smooth' });
    }
  }, [pickerOpen, pickerIndex]);

  /* ── Escape key ── */
  function handleKeyDown(e: React.KeyboardEvent) {
    if (e.key === 'Escape') {
      // AD-705 D8: while wake-word loop is capturing, Escape cancels the
      // utterance without submitting (returns to armed). The picker / input
      // close paths below still apply when the loop is not capturing.
      const wState = getWakeWordState();
      if (wState === 'capturing' || wState === 'fallback-capturing') {
        _cancelCurrentCapture();
        return;
      }
      // AD-719: Esc on the input also closes the @-picker. Full keyboard
      // state machine (↑/↓/Tab/Enter/Esc) shipped in AD-719c below.
      if (pickerOpen) {
        setPickerOpen(false);
        return;
      }
      setActive(false);
      setInput('');
      inputRef.current?.blur();
    }
    // AD-719c: Arrow keys cycle through picker matches; Tab confirms.
    if (pickerOpen && pickerMatches.length > 0) {
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        setPickerIndex((i) => (i + 1) % pickerMatches.length);
        return;
      }
      if (e.key === 'ArrowUp') {
        e.preventDefault();
        setPickerIndex((i) => (i - 1 + pickerMatches.length) % pickerMatches.length);
        return;
      }
      if (e.key === 'Tab') {
        e.preventDefault();
        confirmPickerSelection(pickerMatches[pickerIndex]?.callsign ?? pickerMatches[0].callsign);
        return;
      }
    }
    if (e.key === 'Enter' && pickerOpen && pickerMatches.length > 0) {
      e.preventDefault();
      confirmPickerSelection(pickerMatches[pickerIndex]?.callsign ?? pickerMatches[0].callsign);
    }
  }

  /* ── AD-719: @-picker derived state and handlers ── */

  // Derive crew rows from the live store's agents Map (no extra fetch).
  type CrewRow = { callsign: string; displayName: string; tier: 'core' | 'utility' | 'domain'; department: string; deptColor: string };
  const DEPT_COLORS_LOCAL: Record<string, string> = {
    engineering: '#b0a050',
    science: '#50b0a0',
    medical: '#5090d0',
    security: '#d05050',
    bridge: '#d0a030',
  };
  const crewRows: CrewRow[] = Array.from(agentsMap.values())
    .filter((a) => a.callsign && a.isCrew)
    .map((a) => {
      const dept = (a as Agent & { department?: string }).department || '';
      return {
        callsign: a.callsign,
        displayName: a.displayName || '',
        tier: a.tier,
        department: dept,
        deptColor: DEPT_COLORS_LOCAL[dept.toLowerCase()] ?? '#666',
      };
    })
    // Dedupe by callsign — multiple live agents may share a type/callsign.
    .filter((row, i, arr) => arr.findIndex((r) => r.callsign === row.callsign) === i);

  const pickerMatches: CrewRow[] = pickerOpen
    ? crewRows.filter((r) => {
        const p = pickerPrefix.toLowerCase();
        return r.callsign.toLowerCase().startsWith(p)
            || r.displayName.toLowerCase().startsWith(p);
      }).slice(0, 8)
    : [];

  // Detect "@<prefix>" at the caret tail (no whitespace in the prefix).
  function handleInputChange(value: string) {
    setInput(value);
    const tail = value.split(/\s+/).pop() ?? '';
    if (tail.startsWith('@') && !/\s/.test(tail.slice(1))) {
      setPickerOpen(true);
      setPickerPrefix(tail.slice(1));
      setPickerIndex(0);
    } else {
      setPickerOpen(false);
      setPickerPrefix('');
    }
  }

  function confirmPickerSelection(callsign: string) {
    // Replace the trailing @<prefix> with @<callsign> + trailing space.
    const tokens = input.split(/(\s+)/);
    for (let i = tokens.length - 1; i >= 0; i--) {
      if (tokens[i].startsWith('@') && !/\s/.test(tokens[i])) {
        tokens[i] = `@${callsign}`;
        break;
      }
    }
    const next = tokens.join('') + (input.endsWith(' ') ? '' : ' ');
    setInput(next);
    setPickerOpen(false);
    setPickerPrefix('');
    setTimeout(() => inputRef.current?.focus(), 0);
  }

  // Derive selected mentions from the leading @callsign run in the input.
  const selectedMentions: string[] = (() => {
    const result: string[] = [];
    const stripped = input.trimStart();
    const re = /@(\w+)\s*/g;
    let lastEnd = 0;
    let m: RegExpExecArray | null;
    while ((m = re.exec(stripped)) !== null) {
      if (m.index !== lastEnd) break;
      result.push(m[1]);
      lastEnd = m.index + m[0].length;
    }
    return result;
  })();

  function removeMention(callsign: string) {
    // Remove the @<callsign> token plus surrounding spaces.
    const re = new RegExp(`@${callsign}\\s*`, 'i');
    const next = input.replace(re, '');
    setInput(next.replace(/^\s+/, ''));
    setTimeout(() => inputRef.current?.focus(), 0);
  }

  /* ── AD-720: image paste from clipboard ── */
  const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // mirrors server default

  /* AD-720a (Wave 139): single source of truth for the upload allow-list.
     The string is reused as the <input type="file"> accept attribute. */
  const ALLOWED_ATTACHMENT_MIMES = [
    'image/png', 'image/jpeg', 'image/webp', 'image/gif',
    'application/pdf', 'text/plain', 'text/markdown',
    'application/json', 'text/csv',
  ] as const;

  async function bufferToHexSha256(buf: ArrayBuffer): Promise<string> {
    const digest = await crypto.subtle.digest('SHA-256', buf);
    const bytes = new Uint8Array(digest);
    let out = '';
    for (let i = 0; i < bytes.length; i++) {
      out += bytes[i].toString(16).padStart(2, '0');
    }
    return out;
  }

  function bufferToBase64(buf: ArrayBuffer): string {
    const bytes = new Uint8Array(buf);
    let bin = '';
    const chunk = 0x8000;
    for (let i = 0; i < bytes.length; i += chunk) {
      bin += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)));
    }
    return btoa(bin);
  }

  async function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
    const items = Array.from(event.clipboardData?.items ?? []);
    const imageItem = items.find((it) => it.type && it.type.startsWith('image/'));
    if (!imageItem) return; // text paste — let the input handle it
    event.preventDefault();
    const blob = imageItem.getAsFile();
    if (!blob) return;
    if (blob.size > MAX_ATTACHMENT_BYTES) {
      addChatMessage('system', `(Attachment too large: ${blob.size} bytes; max ${MAX_ATTACHMENT_BYTES})`);
      return;
    }
    try {
      const buf = await blob.arrayBuffer();
      const hash = await bufferToHexSha256(buf);
      const b64 = bufferToBase64(buf);
      const res = await fetch('/api/chat/attachments', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content_hash: hash, blob_b64: b64, mime: blob.type }),
      });
      if (!res.ok) {
        let reason = 'unknown';
        try {
          const errBody = await res.json();
          reason = errBody?.error ?? reason;
        } catch { /* log-and-degrade */ }
        addChatMessage('system', `(Attachment upload failed: ${reason})`);
        return;
      }
      const data = await res.json() as ChatAttachment;
      setPendingAttachments((prev) => [...prev, data]);
    } catch (err) {
      addChatMessage('system', `(Attachment upload error: ${(err as Error).message})`);
    }
  }

  function removePendingAttachment(attachmentId: string) {
    setPendingAttachments((prev) => prev.filter((a) => a.attachment_id !== attachmentId));
  }

  /* ── AD-720a (Wave 139): file upload via multipart + drag-drop ── */
  async function uploadAttachmentMultipart(file: File): Promise<ChatAttachment | null> {
    if (file.size > MAX_ATTACHMENT_BYTES) {
      addChatMessage('system', `(Attachment too large: ${file.size} bytes; max ${MAX_ATTACHMENT_BYTES})`);
      return null;
    }
    try {
      const fd = new FormData();
      fd.append('file', file, file.name);
      const res = await fetch('/api/chat/attachments/multipart', { method: 'POST', body: fd });
      if (!res.ok) {
        let reason = 'unknown';
        try {
          const errBody = await res.json();
          reason = errBody?.error ?? reason;
        } catch { /* log-and-degrade */ }
        addChatMessage('system', `(Attachment upload failed: ${reason})`);
        return null;
      }
      const data = await res.json() as ChatAttachment;
      // Preserve the original filename so the preview strip can render it
      // (the server response doesn't echo the filename — we keep what the
      // user actually picked / dropped).
      const enriched: ChatAttachment = { ...data, filename: file.name };
      setPendingAttachments((prev) => [...prev, enriched]);
      return enriched;
    } catch (err) {
      addChatMessage('system', `(Attachment upload error: ${(err as Error).message})`);
      return null;
    }
  }

  async function onFilePickerChange(event: React.ChangeEvent<HTMLInputElement>) {
    const files = Array.from(event.target.files ?? []);
    for (const file of files) {
      await uploadAttachmentMultipart(file);
    }
    // Reset so picking the same file twice in a row still fires onChange.
    if (event.target) event.target.value = '';
  }

  function onComposerDragEnter(event: React.DragEvent<HTMLFormElement>) {
    if (event.dataTransfer && Array.from(event.dataTransfer.types ?? []).includes('Files')) {
      event.preventDefault();
      setIsDragOver(true);
    }
  }
  function onComposerDragOver(event: React.DragEvent<HTMLFormElement>) {
    if (event.dataTransfer && Array.from(event.dataTransfer.types ?? []).includes('Files')) {
      event.preventDefault();
    }
  }
  function onComposerDragLeave(event: React.DragEvent<HTMLFormElement>) {
    // Only clear on leaving the composer surface itself, not on entering children.
    if (event.currentTarget === event.target) {
      setIsDragOver(false);
    }
  }
  async function onComposerDrop(event: React.DragEvent<HTMLFormElement>) {
    event.preventDefault();
    setIsDragOver(false);
    const files = Array.from(event.dataTransfer?.files ?? []);
    for (const file of files) {
      await uploadAttachmentMultipart(file);
    }
  }

  /* ── feedback helper with visual confirmation ── */
  async function handleFeedback(msgId: string, kind: 'good' | 'bad' | 'correct', msgText: string) {
    if (feedbackMap[msgId]?.disabled) return;

    // Correct: pre-fill input and return (no API call yet)
    if (kind === 'correct') {
      setInput(`correct: ${msgText.slice(0, 100)}\n`);
      setFeedbackMap((m) => ({ ...m, [msgId]: { disabled: true, confirmText: '' } }));
      setTimeout(() => inputRef.current?.focus(), 50);
      return;
    }

    const command = kind === 'good' ? '/feedback good' : '/feedback bad';
    const confirmLabel = kind === 'good' ? <><Check size={10} /> Learned</> : <><Check size={10} /> Noted</>;

    // Trigger canvas pulse
    useStore.setState({ pendingFeedbackPulse: kind });

    // Disable all icons for this message, show confirmation
    setFeedbackMap((m) => ({ ...m, [msgId]: { disabled: true, confirmText: confirmLabel } }));

    // Fade confirmation after 2s
    setTimeout(() => {
      setFeedbackMap((m) => {
        const entry = m[msgId];
        if (!entry) return m;
        return { ...m, [msgId]: { ...entry, confirmText: '' } };
      });
    }, 2000);

    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: command }),
      });
      await res.json();
      // Confirmation shown inline on button — NOT as a chat message
    } catch {
      // Show error inline on the button
      setFeedbackMap((m) => ({ ...m, [msgId]: { disabled: true, confirmText: <><XMark size={10} /> Failed</> } }));
    }
  }

  /* ── approve self-mod proposal ── */
  const approveSelfMod = useCallback(async (proposal: SelfModProposal) => {
    addChatMessage('system', 'Starting agent design...');

    try {
      await fetch('/api/selfmod/approve', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          intent_name: proposal.intent_name,
          intent_description: proposal.intent_description,
          parameters: proposal.parameters || {},
          original_message: proposal.original_message || '',
        }),
      });
      // Progress and results come via WebSocket events
    } catch {
      addChatMessage('system', '(Self-mod request failed)');
    }
  }, [addChatMessage]);

  /* ── skip self-mod proposal ── */
  const skipSelfMod = useCallback(() => {
    addChatMessage('system', 'Skipped \u2014 no agent created.');
  }, [addChatMessage]);

  /* ── approve build proposal ── */
  const approveBuild = useCallback(async (proposal: BuildProposal) => {
    addChatMessage('system', `Executing build: ${proposal.title}...`);
    try {
      await fetch('/api/build/approve', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          build_id: proposal.build_id,
          file_changes: proposal.file_changes,
          title: proposal.title,
          description: proposal.description,
          ad_number: proposal.ad_number,
        }),
      });
    } catch {
      addChatMessage('system', '(Build approval request failed)');
    }
  }, [addChatMessage]);

  /* ── reject build proposal ── */
  const rejectBuild = useCallback(() => {
    addChatMessage('system', 'Build rejected by Captain.');
  }, [addChatMessage]);

  /* ── approve architect proposal → forward to builder ── */
  const approveDesign = useCallback(async (proposal: ArchitectProposalView) => {
    addChatMessage('system', `Forwarding "${proposal.title}" to Scotty...`);
    try {
      await fetch('/api/design/approve', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          design_id: proposal.design_id,
        }),
      });
    } catch {
      addChatMessage('system', '(Design approval request failed)');
    }
  }, [addChatMessage]);

  /* ── reject architect proposal ── */
  const rejectDesign = useCallback(() => {
    addChatMessage('system', 'Design proposal rejected by Captain.');
  }, [addChatMessage]);

  return (
    <>
      {/* AD-705: wake-word listening indicator. Renders only when the loop
          is active or in a fallback state with a reason. */}
      <WakeWordIndicator />
      {/* AD-705a (HXI #5): transcript-preview pill — visible only while
          whisperStt is transcribing. Hidden entirely when STT off. */}
      {sttEnabled && sttTranscribing && (
        <div
          data-testid="stt-transcript-preview"
          style={{
            position: 'fixed',
            top: 12,
            left: '50%',
            transform: 'translateX(-50%)',
            zIndex: 200,
            padding: '4px 12px',
            borderRadius: 12,
            background: 'rgba(20, 22, 30, 0.85)',
            border: '1px solid #f0b060',
            color: '#f0b060',
            fontSize: 12,
            letterSpacing: '0.05em',
            pointerEvents: 'none',
            animation: 'sttPulse 1.4s ease-in-out infinite',
          }}
        >
          transcribing…
        </div>
      )}
      {/* ── Bridge toggle (AD-325) ── */}
      <button
        onClick={() => useStore.setState((s) => ({ bridgeOpen: !s.bridgeOpen }))}
        style={{
          position: 'fixed',
          top: 12,
          right: 12,
          zIndex: 25,
          padding: '3px 8px',
          borderRadius: 4,
          border: '1px solid rgba(255, 255, 255, 0.15)',
          background: bridgeOpen ? 'rgba(240, 176, 96, 0.2)' : 'transparent',
          color: badgeCount > 0 ? '#f0b060' : (bridgeOpen ? '#f0b060' : '#888'),
          fontSize: 9,
          fontWeight: 600,
          cursor: 'pointer',
          letterSpacing: 1,
          pointerEvents: 'auto',
          fontFamily: "'JetBrains Mono', monospace",
          opacity: bridgeOpen ? 0 : 1,
          transition: 'opacity 0.15s ease-out',
        }}
      >
        {'BRIDGE' + (badgeCount > 0 ? ` (${badgeCount})` : '')}
      </button>

      {/* ── Bridge Panel (AD-325) ── */}
      <BridgePanel open={bridgeOpen} onClose={() => useStore.setState({ bridgeOpen: false })} />

      {/* ── View Switcher (AD-325) ── */}
      <ViewSwitcher />

      {/* ── Canvas dim overlay when active ── */}
      {active && (
        <div style={{
          position: 'fixed', inset: 0, zIndex: 5,
          background: 'rgba(0, 0, 0, 0.15)',
          pointerEvents: 'none',
          transition: 'opacity 0.3s ease',
        }} />
      )}

      {/* ── Main container — bottom-center ── */}
      <div
        ref={containerRef}
        style={{
          position: 'fixed',
          bottom: 44,
          left: '50%',
          transform: 'translateX(-50%)',
          zIndex: 20,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          width: active ? '55%' : 'auto',
          maxWidth: active ? '80%' : undefined,
          transition: `width 0.3s ${spring}`,
          pointerEvents: 'none',
        }}
      >
        {/* ── Conversation container (thread + input) ── */}
        {active ? (
          <div style={{
            ...glass(0.75),
            borderRadius: 16,
            width: '100%',
            display: 'flex',
            flexDirection: 'column',
            overflow: 'hidden',
            pointerEvents: 'auto',
            animation: 'rise-in 0.3s ease-out',
            boxShadow: '0 0 24px rgba(240, 176, 96, 0.08)',
          }}>
            {/* ── Thread ── */}
            {chatHistory.length > 0 && (
              <div
                ref={threadRef}
                style={{
                  maxHeight: 'calc(100vh - 200px)',
                  overflowY: 'auto',
                  padding: '16px 20px',
                  display: 'flex',
                  flexDirection: 'column',
                  gap: 8,
                }}
              >
                {chatHistory.slice(-30).map((msg) => (
                  <div key={msg.id} style={{
                    display: 'flex',
                    flexDirection: 'column',
                    alignItems: msg.role === 'user' ? 'flex-end' : 'flex-start',
                  }}>
                    {/* AD-719: per-turn attribution for fanned-out agent replies. */}
                    {msg.role === 'agent' && msg.callsign && (
                      <div data-testid="agent-attribution" style={{
                        display: 'flex', alignItems: 'center', gap: 6,
                        marginBottom: 2, fontSize: 12, color: '#88a4c8',
                      }}>
                        <AgentAvatarBadge
                          agentId={msg.agent_id ?? ''}
                          callsign={msg.callsign}
                          department={(crewRows.find((r) => r.callsign.toLowerCase() === msg.callsign!.toLowerCase())?.department) ?? ''}
                          size={24}
                        />
                        <span style={{ color: '#f0d0a0', fontWeight: 500 }}>@{msg.callsign}</span>
                      </div>
                    )}
                    <div style={{
                      maxWidth: '80%',
                      padding: '8px 12px',
                      borderRadius: 10,
                      fontSize: 13,
                      lineHeight: 1.5,
                      fontFamily: "'Inter', sans-serif",
                      background: msg.role === 'user'
                        ? 'rgba(240, 176, 96, 0.12)'
                        : 'rgba(136, 164, 200, 0.1)',
                      color: msg.role === 'user' ? '#f0d0a0' : '#c8d0e0',
                      border: msg.role === 'user'
                        ? '1px solid rgba(240, 176, 96, 0.15)'
                        : '1px solid rgba(136, 164, 200, 0.12)',
                      whiteSpace: msg.role === 'user' ? 'pre-wrap' : undefined,
                      wordBreak: 'break-word',
                    }}>
                      {msg.role === 'user' ? msg.text : (
                        <ReactMarkdown
                          components={{
                            p: ({children}) => <p style={{ margin: '4px 0' }}>{children}</p>,
                            strong: ({children}) => <strong style={{ color: '#f0d0a0' }}>{children}</strong>,
                            h2: ({children}) => <h2 style={{ fontSize: 14, margin: '8px 0 4px', color: '#f0d0a0' }}>{children}</h2>,
                            h3: ({children}) => <h3 style={{ fontSize: 13, margin: '6px 0 3px', color: '#f0d0a0' }}>{children}</h3>,
                            ul: ({children}) => <ul style={{ margin: '4px 0', paddingLeft: 20 }}>{children}</ul>,
                            ol: ({children}) => <ol style={{ margin: '4px 0', paddingLeft: 20 }}>{children}</ol>,
                            li: ({children}) => <li style={{ margin: '2px 0' }}>{children}</li>,
                            code: ({children}) => <code style={{ background: 'rgba(240, 176, 96, 0.1)', padding: '1px 4px', borderRadius: 3, fontSize: 12 }}>{children}</code>,
                            hr: () => <hr style={{ border: 'none', borderTop: '1px solid rgba(136, 164, 200, 0.15)', margin: '8px 0' }} />,
                            a: ({href, children}) => <a href={href} target="_blank" rel="noopener noreferrer" style={{ color: '#88a4c8' }}>{children}</a>,
                          }}
                        >
                          {msg.text}
                        </ReactMarkdown>
                      )}
                    </div>

                    {/* Self-mod approval buttons */}
                    {msg.selfModProposal && msg.selfModProposal.status === 'proposed' && (
                      <div style={{ marginTop: 8 }}>
                        <div style={{ display: 'flex', gap: 8 }}>
                          <button
                            onClick={() => approveSelfMod(msg.selfModProposal!)}
                            style={{
                              background: 'rgba(102, 255, 136, 0.1)',
                              border: '1px solid rgba(102, 255, 136, 0.35)',
                              borderRadius: 8, padding: '6px 16px',
                              color: '#66ff88', cursor: 'pointer', fontSize: 13,
                              fontFamily: "'Inter', sans-serif",
                              display: 'flex', alignItems: 'center',
                              textShadow: '0 0 8px rgba(102, 255, 136, 0.5)',
                            }}
                            onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(102, 255, 136, 0.2)'; }}
                            onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(102, 255, 136, 0.1)'; }}
                          >
                            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#66ff88" strokeWidth="2" strokeLinejoin="round" style={{ marginRight: 6, filter: 'drop-shadow(0 0 4px #66ff88) drop-shadow(0 0 8px rgba(102, 255, 136, 0.4))' }}><polygon points="8,1 14,4.5 14,11.5 8,15 2,11.5 2,4.5" /></svg>
                            Build Agent
                          </button>
                          <button
                            onClick={() => setVibeMode(prev => ({ ...prev, [msg.id]: true }))}
                            style={{
                              background: 'rgba(187, 136, 255, 0.1)',
                              border: '1px solid rgba(187, 136, 255, 0.35)',
                              borderRadius: 8, padding: '6px 16px',
                              color: '#bb88ff', cursor: 'pointer', fontSize: 13,
                              fontFamily: "'Inter', sans-serif",
                              display: 'flex', alignItems: 'center',
                              textShadow: '0 0 8px rgba(187, 136, 255, 0.5)',
                            }}
                            onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(187, 136, 255, 0.2)'; }}
                            onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(187, 136, 255, 0.1)'; }}
                          >
                            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#bb88ff" strokeWidth="2" strokeLinecap="round" style={{ marginRight: 6, filter: 'drop-shadow(0 0 4px #bb88ff) drop-shadow(0 0 8px rgba(187, 136, 255, 0.4))' }}><path d="M4 2c0 4 8 4 8 8s-8 4-8 8" /><path d="M12 2c0 4-8 4-8 8s8 4 8 8" /></svg>
                            Design Agent
                          </button>
                          <button
                            onClick={skipSelfMod}
                            style={{
                              background: 'rgba(128, 128, 160, 0.1)',
                              border: '1px solid rgba(128, 128, 160, 0.2)',
                              borderRadius: 8, padding: '6px 16px',
                              color: '#8888a0', cursor: 'pointer', fontSize: 13,
                              fontFamily: "'Inter', sans-serif",
                            }}
                            onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(128, 128, 160, 0.2)'; }}
                            onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(128, 128, 160, 0.1)'; }}
                          >
                            {'\u2014'} Skip
                          </button>
                        </div>

                        {/* Vibe mode: description input */}
                        {vibeMode[msg.id] && !enrichedSpec[msg.id] && (
                          <div style={{ marginTop: 12, width: '100%' }}>
                            <div style={{ fontSize: 12, color: '#888', marginBottom: 6 }}>
                              Describe how this agent should work:
                            </div>
                            <textarea
                              value={vibeInput[msg.id] || ''}
                              onChange={(e) => setVibeInput(prev => ({ ...prev, [msg.id]: e.target.value }))}
                              placeholder="e.g., Search DuckDuckGo for the person's name, parse the top results, find LinkedIn profile links..."
                              style={{
                                width: '100%', minHeight: 60, padding: 8,
                                background: 'rgba(10, 10, 18, 0.6)',
                                border: '1px solid rgba(240, 176, 96, 0.2)',
                                borderRadius: 8, color: '#c8d0e0', fontSize: 13,
                                fontFamily: "'Inter', sans-serif", resize: 'vertical',
                              }}
                            />
                            <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
                              <button
                                onClick={async () => {
                                  setEnriching(prev => ({ ...prev, [msg.id]: true }));
                                  try {
                                    const res = await fetch('/api/selfmod/enrich', {
                                      method: 'POST',
                                      headers: { 'Content-Type': 'application/json' },
                                      body: JSON.stringify({
                                        intent_name: msg.selfModProposal!.intent_name,
                                        intent_description: msg.selfModProposal!.intent_description,
                                        parameters: msg.selfModProposal!.parameters,
                                        user_guidance: vibeInput[msg.id] || '',
                                      }),
                                    });
                                    const data = await res.json();
                                    setEnrichedSpec(prev => ({ ...prev, [msg.id]: data.enriched }));
                                  } catch {
                                    setEnrichedSpec(prev => ({ ...prev, [msg.id]: vibeInput[msg.id] || '' }));
                                  } finally {
                                    setEnriching(prev => ({ ...prev, [msg.id]: false }));
                                  }
                                }}
                                disabled={!(vibeInput[msg.id] || '').trim() || enriching[msg.id]}
                                style={{
                                  background: 'rgba(102, 255, 136, 0.1)',
                                  border: '1px solid rgba(102, 255, 136, 0.35)',
                                  borderRadius: 8, padding: '6px 16px',
                                  color: '#66ff88', cursor: 'pointer', fontSize: 13,
                                  fontFamily: "'Inter', sans-serif",
                                  opacity: (!(vibeInput[msg.id] || '').trim() || enriching[msg.id]) ? 0.5 : 1,
                                  textShadow: '0 0 8px rgba(102, 255, 136, 0.5)',
                                }}
                              >
                                {enriching[msg.id] ? <><StatusPending size={10} /> Enriching...</> : <><DiamondOpen size={10} /> Enrich Spec</>}
                              </button>
                              <button
                                onClick={() => {
                                  setVibeMode(prev => ({ ...prev, [msg.id]: false }));
                                  setVibeInput(prev => ({ ...prev, [msg.id]: '' }));
                                }}
                                style={{
                                  background: 'rgba(128, 128, 160, 0.1)',
                                  border: '1px solid rgba(128, 128, 160, 0.2)',
                                  borderRadius: 8, padding: '6px 16px',
                                  color: '#8888a0', cursor: 'pointer', fontSize: 13,
                                  fontFamily: "'Inter', sans-serif",
                                }}
                              >
                                Cancel
                              </button>
                            </div>
                          </div>
                        )}

                        {/* Enriched spec display */}
                        {enrichedSpec[msg.id] && (
                          <div style={{ marginTop: 12, width: '100%' }}>
                            <div style={{ fontSize: 12, color: '#ffcc66', marginBottom: 6, textShadow: '0 0 6px rgba(255, 204, 102, 0.4)' }}>
                              <Bullseye size={12} /> Enriched Agent Spec:
                            </div>
                            <div style={{
                              padding: 12, borderRadius: 8,
                              background: 'rgba(240, 176, 96, 0.06)',
                              border: '1px solid rgba(240, 176, 96, 0.15)',
                              fontSize: 13, lineHeight: 1.6, color: '#c8d0e0',
                              whiteSpace: 'pre-wrap',
                            }}>
                              {enrichedSpec[msg.id]}
                            </div>
                            <div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
                              <button
                                onClick={() => {
                                  approveSelfMod({
                                    ...msg.selfModProposal!,
                                    intent_description: enrichedSpec[msg.id]!,
                                  });
                                  setVibeMode(prev => ({ ...prev, [msg.id]: false }));
                                  setEnrichedSpec(prev => ({ ...prev, [msg.id]: null }));
                                  setVibeInput(prev => ({ ...prev, [msg.id]: '' }));
                                }}
                                style={{
                                  background: 'rgba(102, 255, 136, 0.1)',
                                  border: '1px solid rgba(102, 255, 136, 0.35)',
                                  borderRadius: 8, padding: '6px 16px',
                                  color: '#66ff88', cursor: 'pointer', fontSize: 13,
                                  fontFamily: "'Inter', sans-serif",
                                  textShadow: '0 0 8px rgba(102, 255, 136, 0.5)',
                                }}
                                onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(102, 255, 136, 0.2)'; }}
                                onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(102, 255, 136, 0.1)'; }}
                              >
                                {'\u2B22'} Build This Agent
                              </button>
                              <button
                                onClick={() => setEnrichedSpec(prev => ({ ...prev, [msg.id]: null }))}
                                style={{
                                  background: 'rgba(128, 128, 160, 0.1)',
                                  border: '1px solid rgba(128, 128, 160, 0.2)',
                                  borderRadius: 8, padding: '6px 16px',
                                  color: '#8888a0', cursor: 'pointer', fontSize: 13,
                                  fontFamily: "'Inter', sans-serif",
                                }}
                              >
                                <DiamondOpen size={10} /> Edit
                              </button>
                              <button
                                onClick={() => {
                                  setVibeMode(prev => ({ ...prev, [msg.id]: false }));
                                  setEnrichedSpec(prev => ({ ...prev, [msg.id]: null }));
                                  setVibeInput(prev => ({ ...prev, [msg.id]: '' }));
                                }}
                                style={{
                                  background: 'rgba(128, 128, 160, 0.1)',
                                  border: '1px solid rgba(128, 128, 160, 0.2)',
                                  borderRadius: 8, padding: '6px 16px',
                                  color: '#8888a0', cursor: 'pointer', fontSize: 13,
                                  fontFamily: "'Inter', sans-serif",
                                }}
                              >
                                Cancel
                              </button>
                            </div>
                          </div>
                        )}
                      </div>
                    )}

                    {/* Build proposal approval buttons */}
                    {msg.buildProposal && msg.buildProposal.status === 'review' && (
                      <div style={{ marginTop: 8, maxWidth: '80%' }}>
                        {/* File change summary */}
                        <div style={{
                          padding: '8px 12px',
                          borderRadius: 8,
                          background: 'rgba(176, 160, 80, 0.08)',
                          border: '1px solid rgba(176, 160, 80, 0.2)',
                          fontSize: 12,
                          color: '#c8d0e0',
                          marginBottom: 8,
                        }}>
                          <div style={{ color: '#b0a050', marginBottom: 4, fontWeight: 600 }}>
                            Generated {msg.buildProposal.change_count} file(s):
                          </div>
                          {msg.buildProposal.file_changes.map((fc, i) => (
                            <div key={i} style={{ marginLeft: 8, color: '#a0a8b8' }}>
                              {'\u2022'} {fc.path} ({fc.mode})
                            </div>
                          ))}
                        </div>

                        {/* Collapsible code view */}
                        <button
                          onClick={() => setBuildCodeExpanded(prev => ({ ...prev, [msg.id]: !prev[msg.id] }))}
                          style={{
                            background: 'rgba(128, 128, 160, 0.08)',
                            border: '1px solid rgba(128, 128, 160, 0.15)',
                            borderRadius: 6, padding: '4px 12px',
                            color: '#8888a0', cursor: 'pointer', fontSize: 12,
                            fontFamily: "'Inter', sans-serif",
                            marginBottom: 8,
                          }}
                        >
                          {buildCodeExpanded[msg.id] ? <><ChevronDown size={10} /> Hide Code</> : <><ChevronRight size={10} /> View Code</>}
                        </button>
                        {buildCodeExpanded[msg.id] && (
                          <pre style={{
                            padding: 12, borderRadius: 8,
                            background: 'rgba(10, 10, 18, 0.8)',
                            border: '1px solid rgba(128, 128, 160, 0.15)',
                            fontSize: 11, lineHeight: 1.4, color: '#a0a8b8',
                            maxHeight: 300, overflowY: 'auto',
                            whiteSpace: 'pre-wrap', wordBreak: 'break-word',
                            marginBottom: 8,
                          }}>
                            {msg.buildProposal.llm_output}
                          </pre>
                        )}

                        {/* Action buttons */}
                        <div style={{ display: 'flex', gap: 8 }}>
                          <button
                            onClick={() => approveBuild(msg.buildProposal!)}
                            style={{
                              background: 'rgba(102, 255, 136, 0.1)',
                              border: '1px solid rgba(102, 255, 136, 0.35)',
                              borderRadius: 8, padding: '6px 16px',
                              color: '#66ff88', cursor: 'pointer', fontSize: 13,
                              fontFamily: "'Inter', sans-serif",
                              textShadow: '0 0 8px rgba(102, 255, 136, 0.5)',
                            }}
                            onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(102, 255, 136, 0.2)'; }}
                            onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(102, 255, 136, 0.1)'; }}
                          >
                            {'\u2B22'} Approve Build
                          </button>
                          <button
                            onClick={rejectBuild}
                            style={{
                              background: 'rgba(128, 128, 160, 0.1)',
                              border: '1px solid rgba(128, 128, 160, 0.2)',
                              borderRadius: 8, padding: '6px 16px',
                              color: '#8888a0', cursor: 'pointer', fontSize: 13,
                              fontFamily: "'Inter', sans-serif",
                            }}
                            onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(128, 128, 160, 0.2)'; }}
                            onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(128, 128, 160, 0.1)'; }}
                          >
                            Reject
                          </button>
                        </div>
                      </div>
                    )}

                    {/* Build failure diagnostic card (AD-346) */}
                    {msg.buildFailureReport && (
                      <div style={{ marginTop: 8, maxWidth: '80%' }}>
                        {/* Failure header */}
                        <div style={{
                          padding: '8px 12px',
                          borderRadius: 8,
                          background: 'rgba(255, 85, 85, 0.08)',
                          border: '1px solid rgba(255, 85, 85, 0.2)',
                          fontSize: 12,
                          color: '#c8d0e0',
                          marginBottom: 8,
                        }}>
                          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                            <span style={{
                              color: '#ff5555',
                              fontWeight: 600,
                              fontSize: 13,
                            }}>
                              Build Failed
                            </span>
                            <span style={{
                              padding: '1px 6px',
                              borderRadius: 4,
                              background: 'rgba(255, 85, 85, 0.15)',
                              border: '1px solid rgba(255, 85, 85, 0.3)',
                              color: '#ff8888',
                              fontSize: 10,
                              textTransform: 'uppercase',
                              letterSpacing: '0.5px',
                            }}>
                              {msg.buildFailureReport.failure_category.replace('_', ' ')}
                            </span>
                          </div>
                          {msg.buildFailureReport.ad_number > 0 && (
                            <div style={{ color: '#a0a8b8', marginBottom: 2 }}>
                              AD-{msg.buildFailureReport.ad_number}: {msg.buildFailureReport.title}
                            </div>
                          )}
                          <div style={{ color: '#8888a0', fontSize: 11 }}>
                            {msg.buildFailureReport.files_written.length + msg.buildFailureReport.files_modified.length} file(s) changed
                            {msg.buildFailureReport.branch_name && ` | Branch: ${msg.buildFailureReport.branch_name}`}
                            {msg.buildFailureReport.fix_attempts > 0 && ` | ${msg.buildFailureReport.fix_attempts} fix attempt(s)`}
                          </div>
                        </div>

                        {/* Failed tests list */}
                        {msg.buildFailureReport.failed_tests.length > 0 && (
                          <div style={{
                            padding: '6px 12px',
                            borderRadius: 6,
                            background: 'rgba(255, 85, 85, 0.04)',
                            border: '1px solid rgba(255, 85, 85, 0.1)',
                            fontSize: 11,
                            color: '#a0a8b8',
                            marginBottom: 8,
                          }}>
                            <div style={{ color: '#ff8888', marginBottom: 4, fontSize: 11 }}>
                              Failed tests:
                            </div>
                            {msg.buildFailureReport.failed_tests.map((t, i) => (
                              <div key={i} style={{ marginLeft: 8, fontFamily: 'monospace', fontSize: 10 }}>
                                {'\u2022'} {t}
                              </div>
                            ))}
                          </div>
                        )}

                        {/* Collapsible raw error */}
                        {msg.buildFailureReport.raw_error && (
                          <>
                            <button
                              onClick={() => setBuildCodeExpanded(prev => ({ ...prev, [`fail-${msg.id}`]: !prev[`fail-${msg.id}`] }))}
                              style={{
                                background: 'rgba(128, 128, 160, 0.08)',
                                border: '1px solid rgba(128, 128, 160, 0.15)',
                                borderRadius: 6, padding: '4px 12px',
                                color: '#8888a0', cursor: 'pointer', fontSize: 12,
                                fontFamily: "'Inter', sans-serif",
                                marginBottom: 8,
                              }}
                            >
                              {buildCodeExpanded[`fail-${msg.id}`] ? <><ChevronDown size={10} /> Hide Error Output</> : <><ChevronRight size={10} /> View Error Output</>}
                            </button>
                            {buildCodeExpanded[`fail-${msg.id}`] && (
                              <pre style={{
                                padding: 12, borderRadius: 8,
                                background: 'rgba(10, 10, 18, 0.8)',
                                border: '1px solid rgba(255, 85, 85, 0.15)',
                                fontSize: 11, lineHeight: 1.4, color: '#a0a8b8',
                                maxHeight: 300, overflowY: 'auto',
                                whiteSpace: 'pre-wrap', wordBreak: 'break-word',
                                marginBottom: 8,
                              }}>
                                {msg.buildFailureReport.raw_error}
                              </pre>
                            )}
                          </>
                        )}

                        {/* Resolution buttons */}
                        {msg.buildFailureReport.resolution_options.length > 0 && (
                          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                            {msg.buildFailureReport.resolution_options.map((opt) => (
                              <button
                                key={opt.id}
                                title={opt.description}
                                onClick={async () => {
                                  try {
                                    await fetch('/api/build/resolve', {
                                      method: 'POST',
                                      headers: { 'Content-Type': 'application/json' },
                                      body: JSON.stringify({
                                        build_id: msg.buildFailureReport!.build_id,
                                        resolution: opt.id,
                                      }),
                                    });
                                  } catch (e) {
                                    console.error('Resolution failed:', e);
                                  }
                                }}
                                style={{
                                  background: opt.id === 'abort'
                                    ? 'rgba(128, 128, 160, 0.1)'
                                    : opt.id === 'commit_override'
                                    ? 'rgba(255, 200, 50, 0.1)'
                                    : 'rgba(102, 180, 255, 0.1)',
                                  border: `1px solid ${
                                    opt.id === 'abort'
                                      ? 'rgba(128, 128, 160, 0.2)'
                                      : opt.id === 'commit_override'
                                      ? 'rgba(255, 200, 50, 0.3)'
                                      : 'rgba(102, 180, 255, 0.3)'
                                  }`,
                                  borderRadius: 8, padding: '6px 16px',
                                  color: opt.id === 'abort'
                                    ? '#8888a0'
                                    : opt.id === 'commit_override'
                                    ? '#ffc832'
                                    : '#66b4ff',
                                  cursor: 'pointer', fontSize: 13,
                                  fontFamily: "'Inter', sans-serif",
                                }}
                                onMouseEnter={(e) => {
                                  e.currentTarget.style.opacity = '0.8';
                                }}
                                onMouseLeave={(e) => {
                                  e.currentTarget.style.opacity = '1';
                                }}
                              >
                                {opt.label}
                              </button>
                            ))}
                          </div>
                        )}
                      </div>
                    )}

                    {/* Architect proposal review */}
                    {msg.architectProposal && msg.architectProposal.status === 'review' && (
                      <div style={{ marginTop: 8, maxWidth: '80%' }}>
                        {/* Proposal overview card */}
                        <div style={{
                          padding: '10px 14px',
                          borderRadius: 8,
                          background: 'rgba(80, 160, 176, 0.08)',
                          border: '1px solid rgba(80, 160, 176, 0.2)',
                          fontSize: 12,
                          color: '#c8d0e0',
                          marginBottom: 8,
                        }}>
                          <div style={{ color: '#50a0b0', marginBottom: 6, fontWeight: 600, fontSize: 13 }}>
                            {'\u2609'} {msg.architectProposal.title}
                          </div>
                          <div style={{ marginBottom: 4 }}>
                            <strong style={{ color: '#a0a8b8' }}>Summary:</strong> {msg.architectProposal.summary}
                          </div>
                          <div style={{ marginBottom: 4 }}>
                            <strong style={{ color: '#a0a8b8' }}>Rationale:</strong> {msg.architectProposal.rationale}
                          </div>
                          {msg.architectProposal.roadmap_ref && (
                            <div style={{ marginBottom: 4 }}>
                              <strong style={{ color: '#a0a8b8' }}>Roadmap:</strong> {msg.architectProposal.roadmap_ref}
                            </div>
                          )}
                          <div style={{ marginBottom: 4 }}>
                            <strong style={{ color: '#a0a8b8' }}>Priority:</strong>{' '}
                            <span style={{ color: msg.architectProposal.priority === 'high' ? '#ff8866' : msg.architectProposal.priority === 'low' ? '#88aa88' : '#b0a050' }}>
                              {msg.architectProposal.priority}
                            </span>
                          </div>

                          {/* Build spec file targets */}
                          {msg.architectProposal.build_spec.target_files.length > 0 && (
                            <div style={{ marginTop: 6 }}>
                              <strong style={{ color: '#a0a8b8' }}>Target files:</strong>
                              {msg.architectProposal.build_spec.target_files.map((f, i) => (
                                <div key={i} style={{ marginLeft: 8, color: '#80c8a0' }}>
                                  {'\u2022'} {f}
                                </div>
                              ))}
                            </div>
                          )}

                          {/* Risks */}
                          {msg.architectProposal.risks.length > 0 && (
                            <div style={{ marginTop: 6 }}>
                              <strong style={{ color: '#cc8866' }}>Risks:</strong>
                              {msg.architectProposal.risks.map((r, i) => (
                                <div key={i} style={{ marginLeft: 8, color: '#cc9977' }}>
                                  <Warning size={10} /> {r}
                                </div>
                              ))}
                            </div>
                          )}

                          {/* Dependencies */}
                          {msg.architectProposal.dependencies.length > 0 && (
                            <div style={{ marginTop: 6 }}>
                              <strong style={{ color: '#a0a8b8' }}>Dependencies:</strong>
                              {msg.architectProposal.dependencies.map((d, i) => (
                                <div key={i} style={{ marginLeft: 8, color: '#8888a0' }}>
                                  {'\u2192'} {d}
                                </div>
                              ))}
                            </div>
                          )}
                        </div>

                        {/* Collapsible full spec */}
                        <button
                          onClick={() => setDesignSpecExpanded(prev => ({ ...prev, [msg.id]: !prev[msg.id] }))}
                          style={{
                            background: 'rgba(80, 160, 176, 0.08)',
                            border: '1px solid rgba(80, 160, 176, 0.15)',
                            borderRadius: 6, padding: '4px 12px',
                            color: '#50a0b0', cursor: 'pointer', fontSize: 12,
                            fontFamily: "'Inter', sans-serif",
                            marginBottom: 8,
                          }}
                        >
                          {designSpecExpanded[msg.id] ? <><ChevronDown size={10} /> Hide Full Spec</> : <><ChevronRight size={10} /> View Full Spec</>}
                        </button>
                        {designSpecExpanded[msg.id] && (
                          <pre style={{
                            padding: 12, borderRadius: 8,
                            background: 'rgba(10, 10, 18, 0.8)',
                            border: '1px solid rgba(80, 160, 176, 0.15)',
                            fontSize: 11, lineHeight: 1.4, color: '#a0a8b8',
                            maxHeight: 300, overflowY: 'auto',
                            whiteSpace: 'pre-wrap', wordBreak: 'break-word',
                            marginBottom: 8,
                          }}>
                            {msg.architectProposal.build_spec.description || msg.architectProposal.llm_output}
                          </pre>
                        )}

                        {/* Action buttons */}
                        <div style={{ display: 'flex', gap: 8 }}>
                          <button
                            onClick={() => approveDesign(msg.architectProposal!)}
                            style={{
                              background: 'rgba(80, 160, 176, 0.1)',
                              border: '1px solid rgba(80, 160, 176, 0.35)',
                              borderRadius: 8, padding: '6px 16px',
                              color: '#50d0e0', cursor: 'pointer', fontSize: 13,
                              fontFamily: "'Inter', sans-serif",
                              textShadow: '0 0 8px rgba(80, 160, 176, 0.5)',
                            }}
                            onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(80, 160, 176, 0.2)'; }}
                            onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(80, 160, 176, 0.1)'; }}
                          >
                            {'\u2609'} Approve & Build
                          </button>
                          <button
                            onClick={rejectDesign}
                            style={{
                              background: 'rgba(128, 128, 160, 0.1)',
                              border: '1px solid rgba(128, 128, 160, 0.2)',
                              borderRadius: 8, padding: '6px 16px',
                              color: '#8888a0', cursor: 'pointer', fontSize: 13,
                              fontFamily: "'Inter', sans-serif",
                            }}
                            onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(128, 128, 160, 0.2)'; }}
                            onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(128, 128, 160, 0.1)'; }}
                          >
                            Reject
                          </button>
                        </div>
                      </div>
                    )}

                    {/* Inline feedback icons for system messages */}
                    {msg.role === 'system' && (
                      <div style={{
                        display: 'flex', gap: 4, marginTop: 4, marginLeft: 4,
                        alignItems: 'center',
                      }}>
                        {(() => {
                          const fb = feedbackMap[msg.id];
                          const disabled = fb?.disabled ?? false;
                          const buttons: { icon: React.ReactNode; kind: 'good' | 'bad' | 'correct'; title: string }[] = [
                            { icon: <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="#8888aa" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ filter: 'drop-shadow(0 0 2px rgba(136, 136, 170, 0.3))' }}><polyline points="4,10 8,5 12,10" /></svg>, kind: 'good', title: 'Approve' },
                            { icon: <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="#8888aa" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ filter: 'drop-shadow(0 0 2px rgba(136, 136, 170, 0.3))' }}><polyline points="4,6 8,11 12,6" /></svg>, kind: 'bad', title: 'Reject' },
                            { icon: <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="#8888aa" strokeWidth="2" strokeLinejoin="round" style={{ filter: 'drop-shadow(0 0 2px rgba(136, 136, 170, 0.3))' }}><polygon points="8,2 14,8 8,14 2,8" /></svg>, kind: 'correct', title: 'Correct' },
                          ];
                          return (
                            <>
                              {buttons.map(({ icon, kind, title }) => (
                                <button
                                  key={kind}
                                  onClick={() => handleFeedback(msg.id, kind, msg.text)}
                                  title={title}
                                  disabled={disabled}
                                  style={{
                                    background: 'rgba(128, 128, 160, 0.08)',
                                    border: '1px solid rgba(128, 128, 160, 0.12)',
                                    borderRadius: 4, padding: '2px 5px',
                                    cursor: disabled ? 'default' : 'pointer',
                                    fontSize: 11,
                                    opacity: disabled ? 0.25 : 0.5,
                                    transition: 'opacity 0.2s, background 0.2s',
                                    pointerEvents: disabled ? 'none' : 'auto',
                                  }}
                                  onMouseEnter={(e) => {
                                    if (!disabled) {
                                      e.currentTarget.style.opacity = '1';
                                      e.currentTarget.style.background = 'rgba(102, 255, 255, 0.1)';
                                      const svg = e.currentTarget.querySelector('svg');
                                      if (svg) { svg.setAttribute('stroke', '#66ffff'); svg.style.filter = 'drop-shadow(0 0 4px #66ffff)'; }
                                    }
                                  }}
                                  onMouseLeave={(e) => {
                                    if (!disabled) {
                                      e.currentTarget.style.opacity = '0.5';
                                      e.currentTarget.style.background = 'rgba(128, 128, 160, 0.08)';
                                      const svg = e.currentTarget.querySelector('svg');
                                      if (svg) { svg.setAttribute('stroke', '#8888aa'); svg.style.filter = 'drop-shadow(0 0 2px rgba(136, 136, 170, 0.3))'; }
                                    }
                                  }}
                                >
                                  {icon}
                                </button>
                              ))}
                              {fb?.confirmText && (
                                <span style={{
                                  fontSize: 10, color: '#80c878', fontFamily: 'monospace',
                                  marginLeft: 4,
                                  animation: 'fade-confirm 2s ease-out forwards',
                                }}>
                                  {fb.confirmText}
                                </span>
                              )}
                            </>
                          );
                        })()}
                      </div>
                    )}
                  </div>
                ))}
              </div>
            )}

            {/* ── Transporter progress card (BF-004) ── */}
            {transporterProgress && (
              <div style={{ marginTop: 8, maxWidth: '80%', padding: '0 20px' }}>
                {/* Header: phase badge + progress fraction */}
                <div style={{
                  padding: '8px 12px',
                  borderRadius: 8,
                  background: 'rgba(80, 200, 224, 0.08)',
                  border: '1px solid rgba(80, 200, 224, 0.2)',
                  fontSize: 12,
                  color: '#c8d0e0',
                  marginBottom: 8,
                }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
                    <span style={{
                      padding: '1px 6px',
                      borderRadius: 4,
                      background: 'rgba(80, 200, 224, 0.15)',
                      border: '1px solid rgba(80, 200, 224, 0.3)',
                      color: '#50c8e0',
                      fontSize: 10,
                      fontWeight: 600,
                      textTransform: 'uppercase',
                      letterSpacing: '0.5px',
                    }}>
                      {transporterProgress.phase}
                    </span>
                    <span style={{ color: '#8888a0', fontSize: 11 }}>
                      {transporterProgress.successful} / {transporterProgress.total_chunks} chunks
                    </span>
                  </div>

                  {/* Progress bar */}
                  <div style={{
                    height: 4,
                    borderRadius: 2,
                    background: 'rgba(80, 200, 224, 0.15)',
                    overflow: 'hidden',
                    marginBottom: 8,
                  }}>
                    {transporterProgress.total_chunks > 0 && (
                      <>
                        <div style={{
                          height: '100%',
                          width: `${(transporterProgress.successful / transporterProgress.total_chunks) * 100}%`,
                          background: '#50c8e0',
                          borderRadius: 2,
                          transition: 'width 0.3s ease',
                          float: 'left',
                        }} />
                        {transporterProgress.failed > 0 && (
                          <div style={{
                            height: '100%',
                            width: `${(transporterProgress.failed / transporterProgress.total_chunks) * 100}%`,
                            background: '#ff5555',
                            borderRadius: 2,
                            transition: 'width 0.3s ease',
                            float: 'left',
                          }} />
                        )}
                      </>
                    )}
                  </div>

                  {/* Chunk list */}
                  {transporterProgress.chunks.map((chunk) => (
                    <div key={chunk.chunk_id} style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: 6,
                      marginBottom: 3,
                    }}>
                      <span style={{
                        width: 6,
                        height: 6,
                        borderRadius: '50%',
                        flexShrink: 0,
                        background: chunk.status === 'done' ? '#50c878'
                          : chunk.status === 'failed' ? '#ff5555'
                          : chunk.status === 'executing' ? '#ffaa44'
                          : '#555566',
                        ...(chunk.status === 'executing' ? {
                          animation: 'neural-pulse 1.4s ease-in-out infinite',
                        } : {}),
                      }} />
                      <span style={{ fontSize: 11, color: '#c8d0e0' }}>
                        {chunk.description}
                      </span>
                      <span style={{
                        fontSize: 10,
                        color: '#8888a0',
                        fontFamily: 'monospace',
                        marginLeft: 'auto',
                      }}>
                        {chunk.target_file}
                      </span>
                    </div>
                  ))}

                  {/* Footer stats */}
                  {(transporterProgress.waves_completed > 0 || transporterProgress.failed > 0) && (
                    <div style={{
                      display: 'flex',
                      gap: 12,
                      marginTop: 6,
                      fontSize: 10,
                      color: '#8888a0',
                    }}>
                      {transporterProgress.waves_completed > 0 && (
                        <span>Waves: {transporterProgress.waves_completed}</span>
                      )}
                      {transporterProgress.failed > 0 && (
                        <span style={{ color: '#ff5555' }}>
                          Failed: {transporterProgress.failed}
                        </span>
                      )}
                    </div>
                  )}
                </div>
              </div>
            )}

            {/* ── Build Queue Dashboard (AD-373) ── */}
            {buildQueue && buildQueue.length > 0 && (
              <div style={{ marginTop: 8, maxWidth: '80%', padding: '0 20px' }}>
                <div style={{
                  padding: '8px 12px',
                  borderRadius: 8,
                  background: 'rgba(176, 160, 80, 0.08)',
                  border: '1px solid rgba(176, 160, 80, 0.2)',
                  fontSize: 12,
                  color: '#c8d0e0',
                }}>
                  {/* Header */}
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                    <span style={{
                      padding: '1px 6px',
                      borderRadius: 4,
                      background: 'rgba(176, 160, 80, 0.15)',
                      border: '1px solid rgba(176, 160, 80, 0.3)',
                      color: '#b0a050',
                      fontSize: 10,
                      fontWeight: 600,
                      textTransform: 'uppercase' as const,
                      letterSpacing: '0.5px',
                    }}>
                      Build Queue
                    </span>
                    <span style={{ color: '#8888a0', fontSize: 11 }}>
                      {buildQueue.filter(b => !['merged', 'failed'].includes(b.status)).length} active
                    </span>
                  </div>

                  {/* Build items list */}
                  {buildQueue.map((item) => (
                    <div key={item.id} style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: 8,
                      marginBottom: 6,
                      padding: '4px 0',
                      borderBottom: '1px solid rgba(176, 160, 80, 0.1)',
                    }}>
                      {/* Status dot */}
                      <span style={{
                        width: 6,
                        height: 6,
                        borderRadius: '50%',
                        flexShrink: 0,
                        background: item.status === 'merged' ? '#50c878'
                          : item.status === 'failed' ? '#ff5555'
                          : item.status === 'reviewing' ? '#b0a050'
                          : item.status === 'building' ? '#ffaa44'
                          : item.status === 'dispatched' ? '#6688cc'
                          : '#555566',
                        ...(item.status === 'building' ? {
                          animation: 'neural-pulse 1.4s ease-in-out infinite',
                        } : {}),
                      }} />

                      {/* Title + AD number */}
                      <span style={{ fontSize: 11, color: '#c8d0e0', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                        {item.title}
                        {item.ad_number > 0 && (
                          <span style={{ color: '#8888a0', marginLeft: 4 }}>AD-{item.ad_number}</span>
                        )}
                      </span>

                      {/* Status badge */}
                      <span style={{
                        padding: '1px 5px',
                        borderRadius: 3,
                        fontSize: 9,
                        fontWeight: 600,
                        textTransform: 'uppercase' as const,
                        letterSpacing: '0.3px',
                        background: item.status === 'reviewing' ? 'rgba(176, 160, 80, 0.2)' : 'rgba(128, 128, 160, 0.15)',
                        color: item.status === 'reviewing' ? '#b0a050'
                          : item.status === 'merged' ? '#50c878'
                          : item.status === 'failed' ? '#ff5555'
                          : '#8888a0',
                        border: item.status === 'reviewing' ? '1px solid rgba(176, 160, 80, 0.3)' : '1px solid rgba(128, 128, 160, 0.2)',
                      }}>
                        {item.status}
                      </span>

                      {/* Approve / Reject buttons for reviewing items */}
                      {item.status === 'reviewing' && (
                        <div style={{ display: 'flex', gap: 4 }}>
                          <button
                            style={{
                              padding: '2px 8px',
                              borderRadius: 4,
                              border: '1px solid rgba(80, 200, 120, 0.3)',
                              background: 'rgba(80, 200, 120, 0.15)',
                              color: '#50c878',
                              fontSize: 10,
                              fontWeight: 600,
                              cursor: 'pointer',
                            }}
                            onClick={async () => {
                              try {
                                await fetch('/api/build/queue/approve', {
                                  method: 'POST',
                                  headers: { 'Content-Type': 'application/json' },
                                  body: JSON.stringify({ build_id: item.id }),
                                });
                              } catch { /* ignore */ }
                            }}
                          >
                            Approve
                          </button>
                          <button
                            style={{
                              padding: '2px 8px',
                              borderRadius: 4,
                              border: '1px solid rgba(255, 85, 85, 0.3)',
                              background: 'rgba(255, 85, 85, 0.15)',
                              color: '#ff5555',
                              fontSize: 10,
                              fontWeight: 600,
                              cursor: 'pointer',
                            }}
                            onClick={async () => {
                              try {
                                await fetch('/api/build/queue/reject', {
                                  method: 'POST',
                                  headers: { 'Content-Type': 'application/json' },
                                  body: JSON.stringify({ build_id: item.id }),
                                });
                              } catch { /* ignore */ }
                            }}
                          >
                            Reject
                          </button>
                        </div>
                      )}
                    </div>
                  ))}

                  {/* File footprint for reviewing items */}
                  {buildQueue.filter(b => b.status === 'reviewing').map((item) => (
                    item.file_footprint.length > 0 && (
                      <div key={`fp-${item.id}`} style={{
                        marginTop: 4,
                        padding: '4px 8px',
                        background: 'rgba(176, 160, 80, 0.05)',
                        borderRadius: 4,
                        fontSize: 10,
                        color: '#8888a0',
                        fontFamily: 'monospace',
                      }}>
                        {item.file_footprint.map((f, i) => (
                          <div key={i}>{f}</div>
                        ))}
                      </div>
                    )
                  ))}
                </div>
              </div>
            )}

            {/* ── Neural pulse processing indicator ── */}
            {pendingRequests > 0 && (
              <div style={{
                display: 'flex',
                alignItems: 'center',
                gap: 6,
                padding: '12px 20px',
                color: 'rgba(255, 204, 102, 0.7)',
                fontSize: 12,
                fontFamily: "'Inter', sans-serif",
              }}>
                <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
                  {[0, 1, 2].map((i) => (
                    <div key={i} style={{
                      width: 6, height: 6,
                      borderRadius: '50%',
                      background: 'linear-gradient(135deg, #ffcc66, #66ccff)',
                      filter: 'drop-shadow(0 0 3px #ffcc66)',
                      animation: `neural-pulse 1.4s ease-in-out ${i * 0.2}s infinite`,
                    }} />
                  ))}
                </div>
                <span style={{
                  color: '#ffcc66',
                  textShadow: '0 0 6px rgba(255, 204, 102, 0.4)',
                  animation: 'neural-pulse 2s ease-in-out infinite',
                  letterSpacing: '0.5px',
                }}>
                  {dagProgress || 'thinking'}
                </span>
              </div>
            )}

            {/* AD-719: Recipient chip strip — one chip per leading @callsign in input. */}
            {selectedMentions.length > 0 && (
              <div
                data-testid="recipient-chip-strip"
                style={{
                  display: 'flex', flexWrap: 'wrap', gap: 6,
                  padding: '6px 16px 0 16px',
                }}
              >
                {selectedMentions.map((cs) => {
                  const row = crewRows.find((r) => r.callsign.toLowerCase() === cs.toLowerCase());
                  const color = row?.deptColor ?? '#666';
                  return (
                    <span
                      key={cs}
                      data-testid="recipient-chip"
                      style={{
                        display: 'inline-flex', alignItems: 'center', gap: 6,
                        padding: '2px 8px',
                        borderRadius: 12,
                        background: 'rgba(240, 176, 96, 0.08)',
                        border: '1px solid rgba(240, 176, 96, 0.2)',
                        fontSize: 12,
                        color: '#f0d0a0',
                      }}
                    >
                      <span style={{ width: 6, height: 6, borderRadius: '50%', background: color }} />
                      <span>@{cs}</span>
                      <button
                        type="button"
                        data-testid="chip-remove"
                        aria-label={`Remove ${cs}`}
                        onClick={() => removeMention(cs)}
                        style={{
                          background: 'transparent',
                          border: 'none',
                          padding: 0,
                          cursor: 'pointer',
                          color: '#666680',
                          display: 'inline-flex',
                          alignItems: 'center',
                        }}
                        onMouseEnter={(e) => { e.currentTarget.style.color = '#f0b060'; }}
                        onMouseLeave={(e) => { e.currentTarget.style.color = '#666680'; }}
                      >
                        <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
                          <line x1="3" y1="3" x2="9" y2="9" />
                          <line x1="9" y1="3" x2="3" y2="9" />
                        </svg>
                      </button>
                    </span>
                  );
                })}
              </div>
            )}

            {/* ── Input + Reset ── */}
            <form
              onSubmit={handleSubmit}
              onDragEnter={onComposerDragEnter}
              onDragOver={onComposerDragOver}
              onDragLeave={onComposerDragLeave}
              onDrop={onComposerDrop}
              style={{
                display: 'flex', alignItems: 'center',
                padding: '8px 16px',
                borderTop: chatHistory.length > 0 ? '1px solid rgba(240, 176, 96, 0.1)' : 'none',
                gap: 8,
                position: 'relative',
              }}
            >
              {/* AD-719: @-picker popover (mouse + Enter only; arrow/Esc/Tab deferred to AD-719c).
                  Portal'd to document.body to escape the conversation container's overflow:hidden,
                  which was clipping it and breaking pointer hit-testing (the canvas underneath would
                  intercept clicks because the popover rendered outside the wrapper's bounding box). */}
              {pickerOpen && pickerMatches.length > 0 && inputRef.current && createPortal(
                (() => {
                  const rect = inputRef.current.getBoundingClientRect();
                  return (
                <div
                  data-testid="at-picker-popover"
                  style={{
                    position: 'fixed',
                    bottom: window.innerHeight - rect.top + 4,
                    left: rect.left,
                    width: rect.width,
                    maxHeight: 200,
                    overflowY: 'auto',
                    background: 'rgba(10, 10, 18, 0.96)',
                    border: '1px solid rgba(240, 176, 96, 0.25)',
                    borderRadius: 8,
                    backdropFilter: 'blur(12px)',
                    zIndex: 1000,
                    pointerEvents: 'auto',
                  }}
                >
                  {pickerMatches.map((m, i) => (
                    <div
                      key={m.callsign}
                      data-testid="at-picker-row"
                      data-picker-index={i}
                      onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); confirmPickerSelection(m.callsign); }}
                      onMouseEnter={() => setPickerIndex(i)}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 8,
                        padding: '6px 12px',
                        background: i === pickerIndex ? 'rgba(240, 176, 96, 0.08)' : 'transparent',
                        cursor: 'pointer',
                        fontSize: 13,
                        color: '#e0dcd4',
                      }}
                    >
                      <span style={{
                        width: 8, height: 8, borderRadius: '50%',
                        background: m.deptColor,
                        flexShrink: 0,
                      }} />
                      <span style={{ color: '#f0d0a0', fontWeight: 500 }}>@{m.callsign}</span>
                      {m.displayName && (
                        <span style={{ color: '#888899', fontSize: 12 }}>{m.displayName}</span>
                      )}
                      <span style={{
                        marginLeft: 'auto',
                        fontSize: 10,
                        padding: '1px 6px',
                        borderRadius: 4,
                        background: 'rgba(136, 164, 200, 0.1)',
                        color: '#88a4c8',
                        textTransform: 'uppercase',
                        letterSpacing: 0.5,
                      }}>{m.tier}</span>
                    </div>
                  ))}
                </div>
                  );
                })(),
                document.body,
              )}
              {/* AD-720: image-paste preview thumbnails */}
              {pendingAttachments.length > 0 && (
                <div data-testid="attachment-preview-strip" style={{
                  position: 'absolute',
                  bottom: '100%',
                  left: 16,
                  marginBottom: 4,
                  display: 'flex',
                  gap: 8,
                  flexWrap: 'wrap',
                  zIndex: 49,
                }}>
                  {pendingAttachments.map((att) => (
                    <div key={att.attachment_id} data-testid="attachment-preview" style={{
                      position: 'relative',
                      borderRadius: 4,
                      border: '1px solid rgba(240, 176, 96, 0.25)',
                      background: 'rgba(10, 10, 18, 0.96)',
                      padding: 2,
                    }}>
                      {att.mime.startsWith('image/') ? (
                        <img
                          src={att.url}
                          alt={att.attachment_id.slice(0, 8)}
                          style={{ maxWidth: 128, maxHeight: 128, display: 'block', borderRadius: 2 }}
                        />
                      ) : att.mime.startsWith('audio/') ? (
                        <audio
                          controls
                          src={att.url}
                          preload="metadata"
                          style={{ maxWidth: 220, display: 'block' }}
                          aria-label={`audio attachment ${att.attachment_id.slice(0, 8)}`}
                        />
                      ) : (
                        <div style={{
                          display: 'flex', alignItems: 'center', gap: 6,
                          padding: '6px 10px',
                          minWidth: 80, maxWidth: 180,
                          color: '#e0dcd4', fontSize: 12,
                          fontFamily: "'Inter', sans-serif",
                        }}>
                          <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#f0b060" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                            <path d="M3 2h7l3 3v9a1 1 0 01-1 1H3a1 1 0 01-1-1V3a1 1 0 011-1z" />
                            <path d="M10 2v3h3" />
                          </svg>
                          <span style={{
                            overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                          }}>{att.filename ?? att.attachment_id.slice(0, 8)}</span>
                        </div>
                      )}
                      <button
                        type="button"
                        data-testid="attachment-remove"
                        aria-label="remove attachment"
                        onClick={() => removePendingAttachment(att.attachment_id)}
                        style={{
                          position: 'absolute',
                          top: -6, right: -6,
                          width: 16, height: 16,
                          borderRadius: '50%',
                          background: 'rgba(10, 10, 18, 0.96)',
                          border: '1px solid rgba(240, 176, 96, 0.5)',
                          color: '#f0b060',
                          cursor: 'pointer',
                          padding: 0,
                          display: 'flex', alignItems: 'center', justifyContent: 'center',
                        }}
                      >
                        <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
                          <line x1="3" y1="3" x2="9" y2="9" />
                          <line x1="9" y1="3" x2="3" y2="9" />
                        </svg>
                      </button>
                    </div>
                  ))}
                </div>
              )}
              <input
                ref={inputRef}
                value={input}
                onChange={(e) => handleInputChange(e.target.value)}
                onKeyDown={handleKeyDown}
                onPaste={handlePaste}
                onBlur={() => { setTimeout(() => setPickerOpen(false), 150); }}
                placeholder="Ask ProbOS..."
                style={{
                  flex: 1,
                  background: 'transparent',
                  border: 'none',
                  outline: 'none',
                  color: '#e0dcd4',
                  fontSize: 14,
                  fontFamily: "'Inter', sans-serif",
                  padding: '6px 0',
                }}
              />
              {/* Mic button for voice input */}
              {/* AD-720a (Wave 139): file picker + drag-drop overlay (replaces tooltip-only paperclip).
                  Hidden <input type="file"> is opened by the paperclip button. */}
              <input
                ref={fileInputRef}
                type="file"
                multiple
                accept={ALLOWED_ATTACHMENT_MIMES.join(',')}
                onChange={onFilePickerChange}
                // Off-screen rather than display:none — some browsers/webviews block
                // file picker dialogs triggered from inputs with display:none or visibility:hidden.
                style={{
                  position: 'absolute',
                  width: 1,
                  height: 1,
                  opacity: 0,
                  pointerEvents: 'none',
                  left: -9999,
                }}
                tabIndex={-1}
                aria-hidden="true"
                data-testid="attachment-file-input"
              />
              <button
                type="button"
                data-testid="attachment-paperclip"
                aria-label="attach file"
                onMouseEnter={(e) => {
                  const r = (e.currentTarget as HTMLButtonElement).getBoundingClientRect();
                  setPaperclipTooltipRect({ top: r.top, right: r.right });
                  setPaperclipTooltipOpen(true);
                }}
                onMouseLeave={() => setPaperclipTooltipOpen(false)}
                onClick={() => fileInputRef.current?.click()}
                title="Attach a file (or drag-drop)"
                style={{
                  position: 'relative',
                  background: 'transparent',
                  border: 'none',
                  color: '#f0b060',
                  cursor: 'pointer',
                  padding: '4px',
                  borderRadius: 4,
                  flexShrink: 0,
                }}
              >
                <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
                  <path d="M11 4l-5 5a2 2 0 002.8 2.8l5-5a3 3 0 00-4.2-4.2l-5 5a4 4 0 005.7 5.7" />
                </svg>
                {paperclipTooltipOpen && paperclipTooltipRect && createPortal(
                  <span data-testid="attachment-paperclip-tooltip" style={{
                    position: 'fixed',
                    top: paperclipTooltipRect.top - 28,
                    left: paperclipTooltipRect.right,
                    transform: 'translateX(-100%)',
                    background: 'rgba(10,10,18,0.96)', border: '1px solid rgba(240,176,96,0.25)',
                    borderRadius: 4, padding: '4px 8px', fontSize: 11, color: '#e0dcd4',
                    whiteSpace: 'nowrap',
                    pointerEvents: 'none',
                    zIndex: 1000,
                  }}>Attach a file (or drag-drop)</span>,
                  document.body,
                )}
              </button>
              {/* AD-720a: drag-drop overlay covering the composer. Stroke-SVG only — no emoji. */}
              {isDragOver && (
                <div data-testid="drag-drop-overlay" style={{
                  position: 'absolute',
                  inset: 0,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  gap: 8,
                  background: 'rgba(240, 176, 96, 0.12)',
                  border: '1px dashed rgba(240, 176, 96, 0.55)',
                  borderRadius: 6,
                  color: '#f0b060',
                  fontSize: 13,
                  fontFamily: "'Inter', sans-serif",
                  pointerEvents: 'none',
                  zIndex: 50,
                }}>
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M7 16a4 4 0 01-1-7.9 6 6 0 0111.7-1.7A4.5 4.5 0 0117 16" />
                    <path d="M12 11v9" />
                    <path d="M9 14l3-3 3 3" />
                  </svg>
                  <span>Drop to attach</span>
                </div>
              )}
              {isSpeechRecognitionSupported() && (
                <button
                  type="button"
                  onClick={() => {
                    if (listening) {
                      stopListening();
                      setListening(false);
                    } else {
                      setListening(true);
                      startListening(
                        (text) => {
                          setInput(text);
                          setListening(false);
                          // Auto-submit after recognition
                          setTimeout(() => {
                            const form = inputRef.current?.closest('form');
                            if (form) form.requestSubmit();
                          }, 100);
                        },
                        () => setListening(false),
                        () => setListening(false),
                      );
                    }
                  }}
                  title={listening ? 'Stop listening' : 'Voice input'}
                  style={{
                    background: listening ? 'rgba(255, 102, 102, 0.15)' : 'transparent',
                    border: 'none',
                    color: listening ? '#ff6666' : '#8888aa',
                    cursor: 'pointer',
                    fontSize: 14,
                    padding: '4px',
                    borderRadius: 4,
                    transition: 'color 0.2s, filter 0.2s',
                    flexShrink: 0,
                    animation: listening ? 'pulse-mic 1s ease-in-out infinite' : undefined,
                    filter: listening ? 'drop-shadow(0 0 4px #ff6666)' : 'drop-shadow(0 0 2px rgba(136, 136, 170, 0.3))',
                  }}
                  onMouseEnter={(e) => { if (!listening) { e.currentTarget.style.color = '#ffcc66'; e.currentTarget.style.filter = 'drop-shadow(0 0 4px #ffcc66)'; } }}
                  onMouseLeave={(e) => { if (!listening) { e.currentTarget.style.color = '#8888aa'; e.currentTarget.style.filter = 'drop-shadow(0 0 2px rgba(136, 136, 170, 0.3))'; } }}
                >
                  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke={listening ? '#ff6666' : 'currentColor'} strokeWidth="2" strokeLinecap="round">
                    <line x1="8" y1="2" x2="8" y2="9" />
                    <path d="M5 7c0 1.7 1.3 3 3 3s3-1.3 3-3" />
                    <line x1="8" y1="12" x2="8" y2="14" />
                    <line x1="6" y1="14" x2="10" y2="14" />
                  </svg>
                </button>
              )}
              {chatHistory.length > 0 && (
                <button
                  type="button"
                  onClick={() => {
                    useStore.setState({ chatHistory: [] });
                    localStorage.removeItem('hxi_chat_history');
                  }}
                  title="Clear conversation"
                  style={{
                    background: 'transparent',
                    border: 'none',
                    color: '#8888aa',
                    cursor: 'pointer',
                    fontSize: 14,
                    padding: '4px',
                    borderRadius: 4,
                    transition: 'color 0.2s, filter 0.2s',
                    flexShrink: 0,
                    filter: 'drop-shadow(0 0 2px rgba(136, 136, 170, 0.3))',
                  }}
                  onMouseEnter={(e) => { e.currentTarget.style.color = '#ff6666'; e.currentTarget.style.filter = 'drop-shadow(0 0 4px #ff6666) drop-shadow(0 0 8px rgba(255, 102, 102, 0.4))'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.color = '#8888aa'; e.currentTarget.style.filter = 'drop-shadow(0 0 2px rgba(136, 136, 170, 0.3))'; }}
                >
                  <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
                    <circle cx="8" cy="8" r="6" opacity="0.5" />
                    <line x1="5" y1="5" x2="11" y2="11" />
                    <line x1="11" y1="5" x2="5" y2="11" />
                  </svg>
                </button>
              )}
            </form>
          </div>
        ) : (
          /* ── Resting Pill (AD-392: breathing) ── */
          <div
            ref={pillRef}
            onClick={handlePillClick}
            style={{
              width: pillReceded ? 80 : 160,
              height: pillReceded ? 4 : 40,
              ...glass(pillReceded ? 0.3 : 0.5),
              borderRadius: pillReceded ? 2 : 24,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              padding: pillReceded ? 0 : '0 16px',
              cursor: 'pointer',
              pointerEvents: 'auto',
              position: 'relative',
              opacity: pillReceded ? 0.4 : undefined,
              animation: !pillReceded && pendingRequests === 0 ? 'pulse-pill 1.2s ease-in-out infinite' : undefined,
              boxShadow: pillReceded
                ? '0 0 12px rgba(56, 200, 192, 0.15)'
                : '0 0 8px rgba(240, 176, 96, 0.05)',
              transition: pillReceded
                ? 'width 0.8s ease-in, height 0.8s ease-in, opacity 0.8s ease-in, border-radius 0.8s ease-in, box-shadow 0.8s ease-in, padding 0.8s ease-in'
                : 'width 0.3s ease-out, height 0.3s ease-out, opacity 0.3s ease-out, border-radius 0.3s ease-out, box-shadow 0.3s ease-out, padding 0.3s ease-out',
            }}
          >
            <span style={{
              color: 'rgba(224, 220, 212, 0.5)',
              fontSize: 13,
              fontFamily: "'Inter', sans-serif",
              userSelect: 'none',
              display: 'flex',
              alignItems: 'center',
              gap: 6,
              whiteSpace: 'nowrap',
              opacity: pillReceded ? 0 : 1,
              transition: pillReceded ? 'opacity 0.3s ease-in' : 'opacity 0.3s ease-out 0.15s',
            }}>
              <span style={{ fontSize: 14 }}><Sparkle size={14} /></span>
              Ask ProbOS...
            </span>

            {/* Message count badge */}
            {msgCount > 0 && (
              <span style={{
                position: 'absolute',
                top: -8,
                right: -4,
                fontSize: 10,
                color: 'rgba(224, 220, 212, 0.4)',
                fontFamily: 'monospace',
                minWidth: 16,
                textAlign: 'center',
                opacity: pillReceded ? 0 : 1,
                transition: pillReceded ? 'opacity 0.3s ease-in' : 'opacity 0.3s ease-out 0.15s',
              }}>
                {msgCount > 99 ? '99+' : msgCount}
              </span>
            )}
          </div>
        )}
      </div>

      {/* ── Keyframe animations (injected once) ── */}
      <style>{`
        @keyframes pulse-pill {
          0%, 100% { opacity: 0.85; }
          50% { opacity: 0.95; }
        }
        @keyframes neural-pulse {
          0%, 100% { opacity: 0.2; transform: scale(0.8); }
          50% { opacity: 1.0; transform: scale(1.2); }
        }
        @keyframes rise-in {
          from { opacity: 0; transform: translateY(30px); }
          to { opacity: 1; transform: translateY(0); }
        }
        @keyframes fade-confirm {
          0% { opacity: 1; }
          70% { opacity: 1; }
          100% { opacity: 0; }
        }
        @keyframes pulse-mic {
          0%, 100% { opacity: 0.6; }
          50% { opacity: 1; }
        }
      `}</style>
    </>
  );
}
