"use client";

import React, {
  useEffect,
  useRef,
  useState,
  useCallback,
  type ReactNode,
  type CSSProperties,
} from "react";
import Link from "next/link";
import { useSession } from "next-auth/react";
import { useParams, usePathname } from "next/navigation";
import {
  MessageSquare,
  X,
  ChevronRight,
  Send,
  Plus,
  Loader2,
  Wrench,
  ChevronDown,
  Trash2,
  Square,
  MousePointerClick,
  Paperclip,
  ChevronLeft,
} from "lucide-react";
import { ProjectIconRail } from "@/components/project/project-icon-rail";
import {
  PreviewBridgeProvider,
  previewMessagePrepRef,
  usePreviewBridge,
} from "@/components/project/preview-bridge-context";

// ── Types ─────────────────────────────────────────────────────────────────────

interface Thread {
  id: string;
  title: string;
  updatedAt: string;
}

interface Message {
  id?: string;
  role: "user" | "assistant" | "tool";
  content: string;
  toolCalls?: { id: string; name: string; args: Record<string, unknown> }[];
  toolName?: string;
  createdAt?: string;
  /**
   * Chronological turn timeline interleaving the model's thinking
   * narration and the tool calls it fired. Rendered as a stack of
   * pills INSIDE the bubble above the final text content, so the
   * user sees the actual flow:
   *   [thought] [tool ×N] [thought] [tool] ... [summary]
   * Each thought is its own collapsed pill (click to expand);
   * adjacent runs of the same tool name collapse into one pill
   * with a ×N counter. The final assistant text is rendered
   * separately, below the timeline.
   */
  timeline?: TimelineEntry[];
}

type TimelineEntry =
  | { kind: "thought"; text: string }
  | { kind: "phase"; phase: string; label: string }
  | {
      kind: "tool";
      name: string;
      status: "running" | "done" | "error";
      result?: string;
      args?: Record<string, unknown>;
    }
  // A text segment from one round of the assistant's tool loop.
  // Each text SSE event from the server starts a new entry; subsequent
  // streaming chunks for that same round append to the most-recent
  // text entry. Tool/thought entries between text segments break the
  // accumulation so multi-round turns render as separate bubbles.
  | { kind: "text"; text: string };

// ── Helpers ───────────────────────────────────────────────────────────────────

function getFriendlyCategory(name: string): string {
  const dotted = name.replace(/_/g, ".");
  if (dotted.startsWith("databases.")) return "Provisioning database service";
  if (dotted.startsWith("domains."))
    return "Registering & attaching custom domains";
  if (dotted.startsWith("apps.envs."))
    return "Updating configuration environment";
  if (dotted.startsWith("apps.")) return "Deploying web application container";
  if (dotted.startsWith("gitea.")) return "Configuring Git repositories";
  if (dotted.startsWith("devcontainer."))
    return "Spinning up secure Alpine environment";
  if (dotted.startsWith("plan."))
    return "Structuring development roadmap & specs";

  if (
    name.includes("fs.edit") ||
    name.includes("fs.write") ||
    name.includes("fs_edit") ||
    name.includes("fs_write")
  )
    return "Surgically writing & editing code";
  if (
    name.includes("fs.read") ||
    name.includes("fs.list") ||
    name.includes("fs.grep") ||
    name.includes("fs.tree") ||
    name.includes("fs_read") ||
    name.includes("fs_list") ||
    name.includes("fs_grep") ||
    name.includes("fs_tree")
  )
    return "Scanning & reading project files";
  if (name.includes("shell.exec") || name.includes("shell_exec"))
    return "Executing sandbox terminal scripts";
  if (name.includes("dev_server.start") || name.includes("dev_server_start"))
    return "Initializing local dev-server";
  if (name.includes("dev_server.logs") || name.includes("dev_server_logs"))
    return "Analyzing compilation build logs";
  if (
    name.includes("browser.navigate") ||
    name.includes("browser.console") ||
    name.includes("browser_navigate") ||
    name.includes("browser_console")
  )
    return "Performing live design QA & console audits";
  if (name.includes("request_visual_qa") || name.includes("request.visual.qa"))
    return "Running world-class visual design QA";
  if (name.includes("ship")) return "Bundling & shipping live release";

  return name;
}

function timeAgo(dateStr?: string): string {
  if (!dateStr) return "";
  const diff = (Date.now() - new Date(dateStr).getTime()) / 1000;
  if (diff < 60) return "just now";
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
  return `${Math.floor(diff / 86400)}d ago`;
}

function friendlyToolName(name: string): string {
  const dotted = name.replace(/_/g, ".");

  const map: Record<string, string> = {
    // Filesystem Tools
    "fs.read": "Reading file",
    "fs.write": "Writing file",
    "fs.edit": "Editing code",
    "fs.list": "Listing folder",
    "fs.tree": "Mapping structure",
    "fs.delete": "Deleting file",
    "fs.glob": "Finding files",
    "fs.grep": "Searching code",

    // Server & Browser Tools
    "dev.server.start": "Starting server",
    "dev.server.stop": "Stopping server",
    "dev.server.list": "Checking servers",
    "dev.server.logs": "Reading logs",
    "browser.console": "Checking console",
    "browser.navigate": "Testing page",
    "request.visual.qa": "Running visual QA",

    // Core Platform Tools
    "projects.list": "Listing workspace projects",
    "projects.get": "Retrieving project spec sheets",
    "workspace.describe": "Fetching workspace details",
    "gitea.credentials": "Resolving repository Git credentials",
    "shell.exec": "Running command",
    ship: "Shipping",
    "generate.media": "Generating visual media assets",
    "get.design.template": "Retrieving design templates",
    "apps.templates.scaffold": "Scaffolding bento-grid layouts",

    // App & Database Deployment Tools
    "apps.list": "Listing deployed web services",
    "apps.get": "Checking application build status",
    "apps.create": "Creating app",
    "apps.update": "Updating application settings",
    "apps.delete": "Removing deployed application",
    "apps.deploy": "Deploying app",
    "apps.deployments": "Fetching recent deployment history",
    "apps.envs.list": "Loading environment variables",
    "apps.envs.upsert": "Injecting environment variables",
    "apps.envs.delete": "Removing environment variables",
    "apps.domains.list": "Checking application domain routing",
    "apps.domains.set": "Binding custom domains",
    "apps.logs": "Fetching application logs",
    "apps.exec": "Running command inside container",
    "databases.list": "Listing database clusters",
    "databases.create": "Provisioning database service",
    "databases.get": "Retrieving database connection credentials",
    "databases.update": "Updating database configuration",
    "databases.delete": "Removing database service",

    // Domain & Git Tools
    "domains.search": "Searching open domain names",
    "domains.list": "Listing registered domains",
    "domains.get": "Retrieving domain details",
    "domains.register": "Registering domain name",
    "domains.attach": "Attaching domain reverse-proxy rules",
    "gitea.repos.list": "Listing Gitea repositories",
    "gitea.repo.get": "Loading Gitea repository info",
    "gitea.repo.create": "Initializing Gitea repository",
    "gitea.file.read": "Reading file from Gitea",
    "gitea.file.write": "Saving file to Gitea",
    "gitea.file.delete": "Deleting file from Gitea",
    "gitea.branches.list": "Checking repository branches",
    "gitea.branch.create": "Creating Git branch",
    "devcontainer.ensure": "Spinning up secure Alpine dev container",
    "devcontainer.status": "Probing dev container liveness",
    "devcontainer.suspend": "Suspending dev container",

    // Planning / Specs Tools
    "plan.get": "Loading specifications checklist",
    "plan.vision.set": "Saving feature product specification",
    "plan.idea.add": "Adding planning ideation",
    "plan.task.add": "Adding task to development roadmap",
    "plan.task.edit": "Updating development roadmap task",
    "plan.task.complete": "Toggling checklist milestone as completed",
    "plan.document.update": "Updating specs documentation",
  };

  return map[dotted] || dotted;
}

/**
 * Turn a raw tool result string (often a JSON blob like
 * `{ "code": 1, "stdout": "...", "stderr": "..." }`) into a short,
 * human-readable status. We NEVER show raw JSON in the chat — only a
 * clean verb + outcome (e.g. "Failed — exit 1: connection refused").
 */
function summarizeToolResult(result?: string): {
  ok: boolean;
  label: string;
} | null {
  if (!result) return null;
  const raw = String(result).trim();
  if (!raw) return null;

  let parsed: Record<string, unknown> | null = null;
  try {
    const p = JSON.parse(raw);
    if (p && typeof p === "object") parsed = p as Record<string, unknown>;
  } catch {
    // not JSON — fall through to text heuristics
  }

  const firstLine = (s: string) => s.replace(/\s+/g, " ").trim().slice(0, 80);

  if (parsed) {
    const code = parsed.code;
    const stderr = typeof parsed.stderr === "string" ? parsed.stderr : "";
    const stdout = typeof parsed.stdout === "string" ? parsed.stdout : "";
    const errMsg = typeof parsed.error === "string" ? parsed.error : "";

    // Explicit non-zero exit code → failure
    if (typeof code === "number" && code !== 0) {
      const detail = firstLine(stderr || errMsg || stdout);
      return {
        ok: false,
        label: detail
          ? `Failed (exit ${code}) — ${detail}`
          : `Failed (exit ${code})`,
      };
    }
    if (errMsg && !/^null$/i.test(errMsg)) {
      return { ok: false, label: `Failed — ${firstLine(errMsg)}` };
    }
    if (parsed.ok === false) {
      return {
        ok: false,
        label: `Failed${stderr ? " — " + firstLine(stderr) : ""}`,
      };
    }
    // Success-ish: surface a tiny hint when available, else just "Done"
    if (typeof code === "number" && code === 0)
      return { ok: true, label: "Done" };
    return { ok: true, label: "Done" };
  }

  // Plain-text heuristics
  const lower = raw.toLowerCase();
  if (
    /(econnrefused|enoent|error|failed|traceback|exception|not found|permission denied|cannot)/.test(
      lower,
    )
  ) {
    return { ok: false, label: `Failed — ${firstLine(raw)}` };
  }
  return { ok: true, label: "Done" };
}

// ── Markdown-lite renderer ────────────────────────────────────────────────────

function escapeHtmlAttr(s: string): string {
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
}

const LINK_STYLE =
  "color:#4f46e5;text-decoration:none;font-weight:500;border-bottom:1px solid #c7d2fe;transition:all 0.2s ease;overflow-wrap:anywhere;word-break:break-all";

/** [label](https://...) — href restricted to http(s) */
function markdownLinksToHtml(s: string): string {
  return s.replace(
    /\[([^\]]+)\]\((https?:\/\/[^\s)<>]+)\)/gi,
    (_m, label: string, url: string) => {
      return `<a href="${escapeHtmlAttr(url)}" target="_blank" rel="noopener noreferrer" style="${LINK_STYLE}">${label}</a>`;
    },
  );
}

/** Bare https:// in prose (skips when prefix is `>` so href=/code aren't touched) */
function autoLinkBareUrls(s: string): string {
  return s.replace(
    /(^|[\s\-—:(\[{])(https?:\/\/[^\s<>"']+)/gi,
    (match, pre: string, url: string) =>
      `${pre}<a href="${escapeHtmlAttr(url)}" target="_blank" rel="noopener noreferrer" style="${LINK_STYLE}">${url}</a>`,
  );
}

function renderMarkdown(text: string): string {
  const codeBlocks: string[] = [];
  let s = text;

  // Extract triple backtick code blocks first to protect them from other formatting
  s = s.replace(/```(\w*)[\s\n]([\s\S]*?)```/g, (match, lang, code) => {
    const id = `___CODE_BLOCK_${codeBlocks.length}___`;
    const escapedCode = code
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
    const languageLabel = lang ? lang.toUpperCase() : "CODE";

    const containerStyle = `
      margin: 12px 0;
      border: 1px solid #e4e4e7;
      border-radius: 8px;
      overflow: hidden;
      background: #fafafa;
      box-shadow: 0 1px 3px rgba(0,0,0,0.02);
      font-family: var(--font-ibm-plex-mono), SFMono-Regular, Consolas, monospace;
    `
      .trim()
      .replace(/\s+/g, " ");

    const headerStyle = `
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 6px 12px;
      background: #f4f4f5;
      border-bottom: 1px solid #e4e4e7;
      font-size: 0.68rem;
      font-weight: 600;
      color: #52525b;
    `
      .trim()
      .replace(/\s+/g, " ");

    const preStyle = `
      display: block;
      padding: 12px;
      margin: 0;
      overflow-x: auto;
      font-size: 0.78rem;
      line-height: 1.55;
      color: #1a1a1a;
      white-space: pre;
    `
      .trim()
      .replace(/\s+/g, " ");

    const buttonStyle = `
      background: #ffffff;
      border: 1px solid #e4e4e7;
      border-radius: 4px;
      padding: 2px 6px;
      font-size: 0.65rem;
      cursor: pointer;
      color: #52525b;
      display: flex;
      align-items: center;
      gap: 4px;
      font-family: var(--font-inter), ui-sans-serif, sans-serif;
      transition: all 0.1s ease;
    `
      .trim()
      .replace(/\s+/g, " ");

    const copyId = `btn-copy-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
    const textId = `txt-code-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;

    const copyScript = `
      navigator.clipboard.writeText(document.getElementById('${textId}').innerText)
        .then(() => {
          const btn = document.getElementById('${copyId}');
          btn.innerHTML = 'Copied ✓';
          setTimeout(() => { btn.innerHTML = 'Copy'; }, 1500);
        });
    `
      .trim()
      .replace(/\s+/g, " ");

    const blockHtml = `
      <div style="${containerStyle}">
        <div style="${headerStyle}">
          <span>${languageLabel}</span>
          <button id="${copyId}" onclick="${copyScript}" style="${buttonStyle}">Copy</button>
        </div>
        <pre id="${textId}" style="${preStyle}"><code>${escapedCode}</code></pre>
      </div>
    `
      .trim()
      .replace(/\s*\n\s*/g, "");

    codeBlocks.push(blockHtml);
    return id;
  });

  // Safe escape of remaining content
  s = s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

  s = markdownLinksToHtml(s);

  s = s
    .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
    .replace(
      /`([^`]+)`/g,
      '<code style="background:#f4f4f5;padding:1px 5px;border-radius:3px;font-family:var(--font-ibm-plex-mono),monospace;font-size:0.85em;overflow-wrap:anywhere;word-break:break-word">$1</code>',
    )
    .replace(
      /^### (.+)$/gm,
      '<h3 style="font-weight:600;margin:12px 0 4px;font-size:0.88rem;overflow-wrap:anywhere;word-break:break-word">$1</h3>',
    )
    .replace(
      /^## (.+)$/gm,
      '<h2 style="font-weight:600;margin:14px 0 4px;font-size:0.9rem;overflow-wrap:anywhere;word-break:break-word">$1</h2>',
    )
    .replace(
      /^- (.+)$/gm,
      '<li style="margin-left:16px;list-style:disc;overflow-wrap:anywhere;word-break:break-word">$1</li>',
    )
    .replace(
      /(<li[^>]*>.*<\/li>\n?)+/g,
      (m) => `<ul style="margin:6px 0">${m}</ul>`,
    )
    .replace(
      /\n\n/g,
      '</p><p style="margin:0 0 8px;overflow-wrap:anywhere;word-break:break-word">',
    )
    .replace(/\n/g, "<br>");

  s = autoLinkBareUrls(s);

  // Restore the formatted code blocks
  codeBlocks.forEach((html, index) => {
    s = s.replace(`___CODE_BLOCK_${index}___`, html);
  });

  return s;
}

// ── Message bubble ─────────────────────────────────────────────────────────────

// NOTE: the model's raw reasoning ("thinking") is intentionally NOT rendered in
// the chat — `thought` timeline entries are dropped at render time. We keep the
// per-round narration (the model's plain "I am editing X…" text), which is
// shown as normal text. The old collapsible ThinkingBubble was removed when we
// stopped surfacing reasoning walls.

function stripRawToolLogs(text: string): string {
  if (!text) return text;
  let out = text.replace(
    /(?:\r?\n)*\[tools executed this turn:[\s\S]*?\]/g,
    "",
  );
  // Safety net: strip the internal "Phase Checkpoint" planning block
  // (Goal / Current Findings / Suspected Cause / Verification Plan) if it
  // ever reaches a user-facing message. This is loop-control machinery, not
  // something the end user should read. We drop from the heading to the end
  // of that block (until a blank line followed by non-bulleted prose, or EOF).
  out = out.replace(/(?:^|\n)\s*#{0,3}\s*Phase Checkpoint[\s\S]*$/i, "").trim();
  return out.trim();
}

const MessageBubble = React.memo(function MessageBubble({
  msg,
  isLast,
  sending,
}: {
  msg: Message;
  isLast: boolean;
  sending: boolean;
}) {
  const isUser = msg.role === "user";
  const proseWrap: React.CSSProperties = {
    overflowWrap: "anywhere",
    wordBreak: "break-word",
    minWidth: 0,
  };
  return (
    <div
      style={{
        display: "flex",
        justifyContent: isUser ? "flex-end" : "flex-start",
        marginBottom: 12,
        minWidth: 0,
      }}
    >
      {!isUser && (
        <div
          style={{
            width: 24,
            height: 24,
            borderRadius: "50%",
            background: "#f4f4f5", // Zinc-100 instead of black
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            marginRight: 8,
            flexShrink: 0,
            marginTop: 2,
            border: "1px solid #e4e4e7",
          }}
        >
          <span
            style={{
              color: "#18181b", // Dark gray instead of white
              fontSize: "0.6rem",
              fontWeight: 700,
              fontFamily: "var(--font-lora),serif",
              fontStyle: "italic",
            }}
          >
            V.
          </span>
        </div>
      )}
      <div
        style={{
          maxWidth: "82%",
          minWidth: 0,
          display: "flex",
          flexDirection: "column",
        }}
      >
        {!isUser && msg.timeline && msg.timeline.length > 0 && (
          <Timeline entries={msg.timeline} isActiveStream={sending && isLast} />
        )}
        {/*
          Render the legacy bottom content bubble ONLY when:
          - the message is from the user (their bubble is always the
            content slot), OR
          - the assistant message has no timeline at all (very old
            messages from before timeline existed).
          When the timeline contains text entries the prose is already
          rendered there, and showing it again here would duplicate
          every paragraph below the timeline.
        */}
        {((msg.content && isUser) ||
          (msg.content &&
            !isUser &&
            (!msg.timeline || msg.timeline.length === 0))) && (
          <div
            style={{
              padding: isUser ? "9px 14px" : "4px 0px",
              borderRadius: isUser ? "14px 14px 4px 14px" : "0",
              background: isUser ? "#111827" : "transparent",
              color: isUser ? "#fff" : "#111827",
              fontSize: "0.84rem",
              lineHeight: 1.6,
              letterSpacing: "-0.01em",
              fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
              ...proseWrap,
            }}
          >
            {isUser ? (
              <span style={{ whiteSpace: "pre-wrap", ...proseWrap }}>
                {msg.content}
              </span>
            ) : (
              <span
                style={proseWrap}
                dangerouslySetInnerHTML={{
                  __html: renderMarkdown(stripRawToolLogs(msg.content)),
                }}
              />
            )}
          </div>
        )}
      </div>
    </div>
  );
});

/**
 * Renders the chronological turn timeline: thoughts as their own
 * collapsed pills, tool calls grouped by adjacent runs of the same
 * name with a ×N counter. The flow visually mirrors what actually
 * happened: thought → tools → thought → tools → ... → final summary.
 */
function Timeline({ entries, isActiveStream }: { entries: TimelineEntry[], isActiveStream?: boolean }) {
  // Walk the entries and emit a renderable list. Adjacent same-category
  // tool entries get bundled into a TimelineToolGroup; thought and
  // text entries pass through as-is.
  type Item =
    | { kind: "thought"; text: string }
    | { kind: "text"; text: string }
    | { kind: "phase"; phase: string; label: string }
    | {
        kind: "toolGroup";
        category: string;
        entries: Array<Extract<TimelineEntry, { kind: "tool" }>>;
      };
  const items: Item[] = [];
  for (const e of entries) {
    if (e.kind === "thought") {
      items.push({ kind: "thought", text: e.text });
    } else if (e.kind === "text") {
      items.push({ kind: "text", text: e.text });
    } else if (e.kind === "phase") {
      // ignore
    } else {
      const last = items[items.length - 1];
      const category = getFriendlyCategory(e.name);
      if (last && last.kind === "toolGroup" && last.category === category) {
        last.entries.push(e);
      } else {
        items.push({ kind: "toolGroup", category, entries: [e] });
      }
    }
  }
  return (
    <div style={{ marginBottom: 6 }}>
      {items.map((item, i) => {
        const isLast = i === items.length - 1;
        if (item.kind === "thought") {
          return <TimelineThought key={i} text={item.text} isStreaming={isActiveStream && isLast} />;
        }
        if (item.kind === "text") {
          return <TimelineText key={i} text={item.text} isStreaming={isActiveStream && isLast} />;
        }
        if (item.kind === "toolGroup") {
          return (
            <TimelineToolGroup
              key={i}
              category={item.category}
              entries={item.entries}
            />
          );
        }
        return null;
      })}
    </div>
  );
}

/**
 * One text segment in the assistant's timeline. Rendered as its own
 * bubble so each round of multi-tool-loop output reads as a discrete
 * step instead of concatenating into a wall of text.
 */

function TimelineThought({ text, isStreaming }: { text: string; isStreaming?: boolean }) {
  const [expanded, setExpanded] = React.useState(true);
  const textLenRef = React.useRef(text.length);

  React.useEffect(() => {
    // If not streaming, auto-collapse after a short delay so the user isn't stuck with huge thinking blocks
    if (!isStreaming) {
      const t = setTimeout(() => setExpanded(false), 500);
      return () => clearTimeout(t);
    }
  }, [isStreaming]);

  const proseWrap: React.CSSProperties = {
    overflowWrap: "anywhere",
    wordBreak: "break-word",
    minWidth: 0,
  };

  return (
    <div
      style={{
        margin: "12px 0",
        fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
      }}
    >
      <button
        onClick={() => setExpanded((v) => !v)}
        style={{
          display: "inline-flex",
          alignItems: "center",
          gap: 6,
          background: "#f4f4f5",
          border: "1px solid #e4e4e7",
          borderRadius: "999px",
          padding: "4px 10px",
          cursor: "pointer",
          fontSize: "0.75rem",
          color: "#52525b",
          fontWeight: 500,
          transition: "all 0.2s ease",
          boxShadow: "0 1px 2px rgba(0,0,0,0.02)",
        }}
      >
        <Sparkles 
          size={14} 
          className={isStreaming ? "animate-pulse" : ""} 
          style={{ color: "#8b5cf6" }} 
        />
        <span className={isStreaming ? "animate-pulse" : ""} style={{ transition: "opacity 0.2s ease" }}>
          {isStreaming ? "Thinking..." : "Thought Process"}
        </span>
        <ChevronRight
          size={14}
          style={{
            transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
            transition: "transform 0.2s ease",
            color: "#a1a1aa"
          }}
        />
      </button>
      {expanded && (
        <div
          style={{
            marginTop: 8,
            padding: "10px 14px",
            borderLeft: "2px solid #c4b5fd",
            fontSize: "0.85rem",
            color: "#52525b",
            background: "linear-gradient(to right, #f5f3ff 0%, transparent 100%)",
            borderRadius: "0 8px 8px 0",
            ...proseWrap,
          }}
        >
          <span
            style={proseWrap}
            dangerouslySetInnerHTML={{
              __html: renderMarkdown(stripRawToolLogs(text)) + (isStreaming ? `<span class="animate-pulse" style="display:inline-block; width:6px; height:13px; background-color:#9ca3af; vertical-align:-1px; margin-left:2px; border-radius:1px;"></span>` : ""),
            }}
          />
        </div>
      )}
    </div>
  );
}

function TimelineText({ text, isStreaming }: { text: string; isStreaming?: boolean }) {
  const proseWrap: React.CSSProperties = {
    overflowWrap: "anywhere",
    wordBreak: "break-word",
    minWidth: 0,
  };
  return (
    <div
      style={{
        padding: "4px 0px",
        borderRadius: "0",
        background: "transparent",
        color: "#111827",
        fontSize: "0.84rem",
        lineHeight: 1.6,
        letterSpacing: "-0.01em",
        fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
        marginBottom: 6,
        ...proseWrap,
      }}
    >
      <span
        style={proseWrap}
        dangerouslySetInnerHTML={{
          __html: renderMarkdown(stripRawToolLogs(text)) + (isStreaming ? `<span class="animate-pulse" style="display:inline-block; width:6px; height:13px; background-color:#9ca3af; vertical-align:-1px; margin-left:2px; border-radius:1px;"></span>` : ""),
        }}
      />
    </div>
  );
}

function TimelineToolGroup({
  entries,
}: {
  category: string;
  entries: Array<Extract<TimelineEntry, { kind: "tool" }>>;
}) {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: 6,
        margin: "8px 0",
        fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
      }}
    >
      {entries.map((e, i) => {
        const isError = e.status === "error";
        const isDone = e.status === "done";

        let argSummary = "";
        if (
          e.args &&
          !e.name.includes("dev_server_start") &&
          !e.name.includes("dev.server.start") &&
          !e.name.includes("shell_exec") &&
          !e.name.includes("shell.exec")
        ) {
          if (e.args.path) argSummary = String(e.args.path);
          else if (e.args.url)
            argSummary = String(e.args.url).replace(/^https?:\/\//, "");
          else if (e.args.name) argSummary = String(e.args.name);
        }

        return (
          <div
            key={i}
            style={{
              display: "inline-flex",
              alignItems: "center",
              gap: 6,
              background: isError ? "#fef2f2" : "#f3f4f6",
              border: `1px solid ${isError ? '#fecaca' : '#e5e7eb'}`,
              borderRadius: "999px",
              padding: "4px 10px 4px 6px",
              fontSize: "0.75rem",
              color: isError ? "#dc2626" : "#4b5563",
              width: "fit-content",
              maxWidth: "100%",
              boxShadow: "0 1px 2px rgba(0,0,0,0.02)",
            }}
          >
            <span style={{ display: "flex", alignItems: "center", justifyContent: "center", color: isError ? "#ef4444" : "#9ca3af" }}>
              {isError ? (
                <XCircle size={14} />
              ) : isDone ? (
                <CheckCircle2 size={14} style={{ color: "#10b981" }} />
              ) : (
                <Loader2
                  style={{ width: 12, height: 12, color: "#8b5cf6" }}
                  className="animate-spin"
                />
              )}
            </span>
            
            <div style={{ display: "flex", alignItems: "center", gap: 4, minWidth: 0 }}>
              <span style={{ color: isError ? "#dc2626" : "#6b7280", display: "flex" }}>
                 {getToolIcon(e.name)}
              </span>
              <span style={{ fontWeight: 500, whiteSpace: "nowrap", letterSpacing: "-0.01em" }}>
                {friendlyToolName(e.name)}
              </span>
              
              {argSummary && (
                <span
                  style={{
                    color: isError ? "#ef4444" : "#6b7280",
                    marginLeft: 2,
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                    opacity: 0.8,
                  }}
                  title={argSummary}
                >
                  {argSummary}
                </span>
              )}
              
              {(() => {
                const summary = summarizeToolResult(e.result);
                if (!summary) return null;
                return (
                  <span 
                    style={{ 
                      color: summary.ok ? (isError ? "#dc2626" : "#6b7280") : "#dc2626", 
                      marginLeft: 2,
                      overflow: "hidden",
                      textOverflow: "ellipsis",
                      whiteSpace: "nowrap",
                      fontStyle: "italic",
                      opacity: 0.8
                    }}
                    title={summary.label}
                  >
                    — {summary.label}
                  </span>
                );
              })()}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ── Main panel ────────────────────────────────────────────────────────────────

interface ChatPanelProps {
  /**
   * When true, the panel renders inline as a flex child of its parent
   * (a structural left column on project pages). Skips the fixed-position
   * slide-out treatment, the collapsed-tab affordance, and the
   * --chat-panel-width side-effect. Always "open" — there's no close
   * button because the panel IS the column.
   *
   * When false / omitted: legacy behavior — fixed slide-out on the
   * right, collapsible, sets --chat-panel-width so the workspace
   * content shifts left to make room.
   */
  structural?: boolean;
  /**
   * When set with `structural` on a project route, renders a unified shell:
   * full-width top bar (chat controls | section icons) and a split row
   * below (chat column | artifact slot). Omit on slide-out chat.
   */
  artifactSlot?: ReactNode;
}

/** Shared dimensions for preview-select + send icon buttons in the composer. */
const COMPOSER_ACTION_BTN_BASE: CSSProperties = {
  flexShrink: 0,
  width: 32,
  height: 32,
  boxSizing: "border-box",
  borderRadius: 8,
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  padding: 0,
};

/** Preview pick chip + select-mode toggle for unified project shell chat composer. */
function ProjectPreviewChatInputWrap({
  unifiedShell,
  children,
}: {
  unifiedShell: boolean;
  children: (selectToggle: React.ReactNode) => React.ReactNode;
}) {
  const bridge = usePreviewBridge();

  if (!unifiedShell || !bridge) {
    return <>{children(null)}</>;
  }

  const { selectMode, setSelectMode, picked, clearPick } = bridge;

  const chip = picked ? (
    <div
      style={{
        display: "flex",
        alignItems: "flex-start",
        gap: 8,
        marginBottom: 8,
        padding: "8px 10px",
        borderRadius: 8,
        background: "#eef2ff",
        border: "1px solid #c7d2fe",
        fontSize: "0.72rem",
        lineHeight: 1.45,
        color: "#312e81",
      }}
    >
      <MousePointerClick
        style={{
          width: 14,
          height: 14,
          flexShrink: 0,
          marginTop: 2,
          opacity: 0.85,
        }}
        aria-hidden
      />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontWeight: 600, marginBottom: 2 }}>
          Preview selection
        </div>
        <div style={{ opacity: 0.92 }}>
          <span
            style={{
              fontFamily: "var(--font-ibm-plex-mono),monospace",
              fontSize: "0.68rem",
            }}
          >
            {picked.tagName}
          </span>
          {" · "}
          <span style={{ wordBreak: "break-word" }}>{picked.selector}</span>
        </div>
        {picked.textSnippet ? (
          <div style={{ marginTop: 4, opacity: 0.88, wordBreak: "break-word" }}>
            {`"${picked.textSnippet.slice(0, 140)}${picked.textSnippet.length > 140 ? "..." : ""}"`}
          </div>
        ) : null}
      </div>
      <button
        type="button"
        onClick={clearPick}
        aria-label="Clear preview selection"
        style={{
          flexShrink: 0,
          background: "none",
          border: "none",
          padding: 2,
          cursor: "pointer",
          borderRadius: 4,
          color: "#4338ca",
          display: "flex",
          alignItems: "center",
        }}
      >
        <X style={{ width: 13, height: 13 }} />
      </button>
    </div>
  ) : null;

  const selectToggle = (
    <button
      type="button"
      aria-pressed={selectMode}
      aria-label={
        selectMode ? "Exit preview select mode" : "Select element from preview"
      }
      title={
        selectMode
          ? "Click preview to choose an element"
          : "Choose element from preview"
      }
      onClick={() => setSelectMode(!selectMode)}
      style={{
        ...COMPOSER_ACTION_BTN_BASE,
        border: selectMode ? "1px solid #6366f1" : "1px solid #e8e4dc",
        background: selectMode ? "#eef2ff" : "#faf8f5",
        color: selectMode ? "#4338ca" : "#6b6560",
        cursor: "pointer",
      }}
    >
      <MousePointerClick style={{ width: 15, height: 15 }} strokeWidth={2} />
    </button>
  );

  return (
    <>
      {chip}
      {children(selectToggle)}
    </>
  );
}

const suggestionChipStyle: React.CSSProperties = {
  background: "#f4f4f5",
  border: "1px solid #e4e4e7",
  borderRadius: 16,
  padding: "4px 10px",
  fontSize: "0.72rem",
  color: "#52525b",
  cursor: "pointer",
  whiteSpace: "nowrap",
  transition: "all 0.1s ease",
};

import { 
  FileText, 
  Terminal, 
  Search, 
  Globe, 
  FolderTree, 
  CheckCircle2,
  XCircle,
  Database,
  CloudUpload
} from "lucide-react";

function getToolIcon(name: string) {
  const n = name.toLowerCase();
  if (n.includes("read") || n.includes("edit") || n.includes("write")) return <FileText size={12} />;
  if (n.includes("shell") || n.includes("exec") || n.includes("command")) return <Terminal size={12} />;
  if (n.includes("search") || n.includes("grep") || n.includes("find")) return <Search size={12} />;
  if (n.includes("fetch") || n.includes("curl") || n.includes("browser")) return <Globe size={12} />;
  if (n.includes("list") || n.includes("tree")) return <FolderTree size={12} />;
  if (n.includes("deploy") || n.includes("ship")) return <CloudUpload size={12} />;
  if (n.includes("database") || n.includes("sql")) return <Database size={12} />;
  return <Wrench size={12} />;
}

export function ChatPanel({
  structural = false,
  artifactSlot,
}: ChatPanelProps = {}) {
  const { status } = useSession();
  const params = useParams();
  const pathname = usePathname() ?? "";
  const workspace = (params?.workspace as string) || "";
  // When the user is on a /project/<id>/* route, scope the chat to
  // that project. The threads list, the new-thread create call, and
  // the system prompt all branch on this; the chat header surfaces it
  // so the user knows the AI is "talking about" the right thing.
  const projectId = (params?.projectId as string) || "";
  /** Full project shell (chat | artifact); must render even while auth is loading or signed out. */
  const unifiedProjectShell =
    structural && Boolean(projectId) && artifactSlot !== undefined;
  const [activeProjectName, setActiveProjectName] = useState<string | null>(
    null,
  );

  const [open, setOpen] = useState(() => {
    // Structural mode is always-open by definition — the panel IS the
    // column, there's no "closed" state to persist.
    if (structural) return true;
    if (typeof window === "undefined") return false;
    return localStorage.getItem("vibn-chat-open") !== "false";
  });
  const [threads, setThreads] = useState<Thread[]>([]);
  // threadsLoaded flips to true after the FIRST loadThreads() resolves.
  // Used to gate the auto-create effect — without it we race the fetch
  // and spawn an empty thread before history loads.
  const [threadsLoaded, setThreadsLoaded] = useState(false);
  const [activeThread, setActiveThread] = useState<string | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [chatMode, setChatMode] = useState<"collaborate" | "vibe" | "delegate">(
    "vibe",
  );
  const [projectFiles, setProjectFiles] = useState<string[]>([]);
  const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
  const [showSuggestions, setShowSuggestions] = useState(false);
  const [suggestionsFilter, setSuggestionsFilter] = useState("");
  const [suggestionIndex, setSuggestionIndex] = useState(0);

  // Fetch codebase files list inside the scoped project
  useEffect(() => {
    if (!projectId || !workspace || status !== "authenticated") return;
    fetch(`/api/projects/${projectId}/files?workspace=${workspace}`)
      .then((res) => res.json())
      .then((data) => {
        if (Array.isArray(data.files)) {
          setProjectFiles(data.files);
        }
      })
      .catch(() => {});
  }, [projectId, workspace, status]);
  const [sending, setSending] = useState(false);
  const [showScrollButton, setShowScrollButton] = useState(false);
  const [currentPhaseLabel, setCurrentPhaseLabel] = useState<string | null>(
    null,
  );
  const [showThreads, setShowThreads] = useState(false);
  const [mcpToken, setMcpToken] = useState<string | null>(null);
  const [isChatMinimized, setIsChatMinimized] = useState<boolean>(false);

  // Auto-minimize when navigating to dashboard, auto-open when navigating to preview
  useEffect(() => {
    setIsChatMinimized(!pathname.includes("/preview"));
  }, [pathname]);

  const messagesEndRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLTextAreaElement>(null);
  // AbortController for the in-flight /api/chat fetch. Lives in a ref
  // so the Stop button can reach it without re-rendering on every
  // streaming chunk.
  const abortRef = useRef<AbortController | null>(null);

  const scrollToBottom = useCallback(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, []);

  // Persist open state + adjust main content margin
  useEffect(() => {
    if (structural) return;
    localStorage.setItem("vibn-chat-open", String(open));
    document.documentElement.style.setProperty(
      "--chat-panel-width",
      open ? "380px" : "0px",
    );
  }, [open, structural]);

  // Load MCP token — fetch fresh from API on mount to avoid stale, revoked tokens.
  useEffect(() => {
    if (!workspace || status !== "authenticated") return;
    fetch("/api/workspaces?include_default_token=true")
      .then((r) => (r.ok ? r.json() : null))
      .then((d) => {
        if (d?.defaultToken) {
          setMcpToken(d.defaultToken);
        }
      });
  }, [workspace, status]);

  // Load threads (scoped to the current project when one is in the URL).
  // Reset the loaded flag when projectId changes so the resume effect
  // re-runs against the correct list and doesn't restore a thread from
  // the previous project.
  const loadThreads = useCallback(async () => {
    if (!workspace || status !== "authenticated") return;
    try {
      const qs = new URLSearchParams({ workspace });
      if (projectId) qs.set("projectId", projectId);
      const res = await fetch(`/api/chat/threads?${qs.toString()}`);
      const data = await res.json();
      setThreads(data.threads || []);
    } catch {
      /* silent */
    } finally {
      setThreadsLoaded(true);
    }
  }, [workspace, projectId, status]);

  useEffect(() => {
    setThreadsLoaded(false);
    setActiveThread(null);
    setMessages([]);
    // Clear the threads array immediately so the resume effect doesn't
    // race the loadThreads() fetch and resume a stale project-scoped
    // thread when the user navigates from /project/X back to /projects.
    setThreads([]);
    loadThreads();
  }, [loadThreads, projectId]);

  // Look up the active project's display name once we have a projectId,
  // so the chat header can show "Talking about: <name>".
  useEffect(() => {
    if (!projectId) {
      setActiveProjectName(null);
      return;
    }
    let cancelled = false;
    fetch(`/api/projects/${projectId}/anatomy`, { credentials: "include" })
      .then((r) => (r.ok ? r.json() : null))
      .then((d) => {
        if (cancelled) return;
        const name = d?.project?.name;
        if (name) setActiveProjectName(name);
      })
      .catch(() => {});
    return () => {
      cancelled = true;
    };
  }, [projectId]);

  // Create and activate a new thread (tagged to the active project, if any).
  const newThread = useCallback(async () => {
    try {
      const res = await fetch("/api/chat/threads", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ workspace, projectId: projectId || undefined }),
      });
      const data = await res.json();
      if (data.thread) {
        setThreads((prev) => [data.thread, ...prev]);
        setActiveThread(data.thread.id);
        setMessages([]);
        setShowThreads(false);
      }
    } catch {
      /* silent */
    }
  }, [workspace, projectId]);

  // Load thread messages
  const loadThread = useCallback(async (id: string) => {
    setActiveThread(id);
    setShowThreads(false);
    setMessages([]);
    try {
      const res = await fetch(`/api/chat/threads/${id}`);
      const data = await res.json();
      // Hydrate the timeline from persisted textSegments + toolCalls
      // so a reloaded thread renders the same per-round bubbles the
      // user saw during streaming. Older messages without
      // textSegments fall back to the legacy single-bubble path.
      const hydrated = (data.messages || []).map(
        (m: {
          role: string;
          textSegments?: string[];
          toolCalls?: Array<{
            name: string;
            args: Record<string, unknown>;
            result?: string;
          }>;
        }) => {
          if (m.role !== "assistant") return m as unknown as Message;
          const segs: string[] = Array.isArray(m.textSegments)
            ? m.textSegments
            : [];
          if (segs.length === 0) return m as unknown as Message;
          const timeline: TimelineEntry[] = segs.map((t) => ({
            kind: "text",
            text: t,
          }));
          // We don't have round-level interleaving for tool calls in
          // the persisted shape (the schema flattens them), so we drop
          // the toolCalls into the timeline at the end. The streamed
          // shape preserves true ordering; this is just a reload
          // approximation. Good enough — what the user really cares
          // about is the text segments not run-on'ing into one blob.
          if (Array.isArray(m.toolCalls)) {
            for (const tc of m.toolCalls) {
              timeline.push({ kind: "tool", name: tc.name, status: "done" });
            }
          }
          return { ...m, timeline, content: "" } as unknown as Message;
        },
      );
      setMessages(hydrated);
    } catch {
      /* silent */
    }
  }, []);

  // Auto-resume previous thread (or create a fresh one if the user has
  // never chatted in this workspace). We MUST wait for threadsLoaded
  // before deciding — otherwise we race the fetch and spawn an empty
  // thread before history arrives. Last-active thread is restored from
  // localStorage so a page reload (deploy, refresh) lands the user back
  // in the conversation they were in.
  useEffect(() => {
    if (!open || status !== "authenticated" || !workspace) return;
    if (!threadsLoaded) return;
    if (activeThread) return;

    if (threads.length === 0) {
      newThread();
      return;
    }

    const scopeKey = projectId ? `${workspace}:${projectId}` : workspace;
    const savedKey = `vibn-chat-active-thread:${scopeKey}`;
    const saved =
      typeof window !== "undefined" ? localStorage.getItem(savedKey) : null;
    const target =
      saved && threads.some((t) => t.id === saved) ? saved : threads[0].id;
    loadThread(target);
  }, [
    open,
    status,
    workspace,
    projectId,
    threadsLoaded,
    threads,
    activeThread,
    newThread,
    loadThread,
  ]);

  // Persist active thread so reload re-opens the same conversation,
  // keyed per-project so each project has its own "last conversation".
  useEffect(() => {
    if (typeof window === "undefined" || !workspace) return;
    const scopeKey = projectId ? `${workspace}:${projectId}` : workspace;
    const savedKey = `vibn-chat-active-thread:${scopeKey}`;
    if (activeThread) localStorage.setItem(savedKey, activeThread);
  }, [activeThread, workspace, projectId]);

  useEffect(() => {
    scrollToBottom();
  }, [messages, scrollToBottom]);

  const deleteThread = useCallback(
    async (id: string, e: React.MouseEvent) => {
      e.stopPropagation();
      await fetch(`/api/chat/threads/${id}`, { method: "DELETE" });
      setThreads((prev) => prev.filter((t) => t.id !== id));
      if (activeThread === id) {
        setActiveThread(null);
        setMessages([]);
      }
      loadThreads();
    },
    [activeThread, loadThreads],
  );

  const sendMessage = useCallback(
    async (override?: string) => {
      let raw = (override ?? input).trim();
      if (!raw || sending || !activeThread) return;
      if (unifiedProjectShell && previewMessagePrepRef.current) {
        raw = previewMessagePrepRef.current(raw);
      }
      const text = raw;
      if (!override) setInput("");
      setSending(true);
      setCurrentPhaseLabel("Starting...");

      const userMsg: Message = { role: "user", content: text };
      setMessages((prev) => [...prev, userMsg]);

      let assistantContent = "";
      let lastKind = "";
      let currentThoughtText = "";
      const assistantMsg: Message = { role: "assistant", content: "" };
      let msgIndex = -1;

      const controller = new AbortController();
      abortRef.current = controller;

      try {
        // If Delegate mode is selected, route to the background runner instead of streaming chat!
        if (chatMode === "delegate") {
          const r = await fetch(`/api/projects/${projectId}/agent/sessions`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              appName: "frontend",
              appPath: ".",
              task: text,
            }),
          });
          if (!r.ok) {
            const err = await r.json().catch(() => ({}));
            throw new Error(err.error || `HTTP ${r.status}`);
          }

          setAttachedFiles([]);
          setMessages((prev) => [
            ...prev,
            {
              role: "assistant",
              content:
                "I have started a background runner for this task. You can safely close this browser or work on something else. I will commit and ship the code when I am finished!",
            },
          ]);
          setSending(false);
          return;
        }

        const res = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            thread_id: activeThread,
            message: text,
            workspace,
            mcp_token: mcpToken,
            chatMode,
            attachedFiles,
          }),
          signal: controller.signal,
        });

        setAttachedFiles([]);

        if (!res.ok || !res.body) throw new Error("Stream failed");

        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buf = "";

        setMessages((prev) => {
          msgIndex = prev.length;
          return [...prev, { ...assistantMsg }];
        });

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          buf += decoder.decode(value, { stream: true });

          const lines = buf.split("\n");
          buf = lines.pop() ?? "";

          for (const line of lines) {
            if (!line.startsWith("data: ")) continue;
            let ev: {
              type: string;
              text?: string;
              name?: string;
              result?: string;
              error?: string;
              phase?: string;
              label?: string;
              goal?: string;
              findings?: string;
              args?: Record<string, unknown>;
            };
            try {
              ev = JSON.parse(line.slice(6));
            } catch {
              continue;
            }

            if (ev.type === "phase" && ev.phase && ev.label) {
              setCurrentPhaseLabel(ev.label);
              // Legacy phase entries in history are ignored
            } else if (ev.type === "text" && ev.text) {
              // Each text SSE event = one round of the model's text
              // output. Push a new "text" timeline entry so the
              // renderer can show multi-round turns as separate
              // bubbles instead of one run-on paragraph. We still
              // maintain `assistantContent` (joined with blank lines)
              // so the legacy single-bubble fallback path and any
              // post-stream consumers still work.
              assistantContent += (assistantContent ? "\n\n" : "") + ev.text;
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  next[msgIndex] = {
                    ...next[msgIndex],
                    // Don't write to msg.content during streaming —
                    // the timeline is the source of truth. Setting
                    // content on every text event re-renders one
                    // giant bubble in the bottom slot AND the
                    // segmented timeline above it, duplicating the
                    // same prose. Persisted messages pick up
                    // content via the final flush below.
                    timeline: [...tl, { kind: "text", text: ev.text }],
                  };
                }
                return next;
              });
            } else if (ev.type === "text_delta" && ev.text) {
              if (lastKind === "text") {
                assistantContent += ev.text;
              } else {
                assistantContent += (assistantContent ? "\n\n" : "") + ev.text;
                lastKind = "text";
              }
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = [...(next[msgIndex].timeline ?? [])];
                  const last = tl[tl.length - 1];
                  if (last && last.kind === "text") {
                    tl[tl.length - 1] = { ...last, text: last.text + ev.text };
                  } else {
                    tl.push({ kind: "text", text: ev.text });
                  }
                  next[msgIndex] = { ...next[msgIndex], timeline: tl };
                }
                return next;
              });
            } else if (ev.type === "thinking_delta" && ev.text) {
              lastKind = "thought";
              currentThoughtText += ev.text;
              
              // Extract the latest bold heading to display in the spinner label
              const matches = [...currentThoughtText.matchAll(/\*\*([^*]+)\*\*/g)];
              if (matches.length > 0) {
                setCurrentPhaseLabel(matches[matches.length - 1][1].trim() + "...");
              } else {
                setCurrentPhaseLabel("Thinking...");
              }

              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = [...(next[msgIndex].timeline ?? [])];
                  const last = tl[tl.length - 1];
                  if (last && last.kind === "thought") {
                    tl[tl.length - 1] = { ...last, text: last.text + ev.text };
                  } else {
                    tl.push({ kind: "thought", text: ev.text });
                  }
                  next[msgIndex] = { ...next[msgIndex], timeline: tl };
                }
                return next;
              });
            } else if (ev.type === "thinking" && ev.text) {
              // Each thinking event from the server is one round of the
              // model's reasoning. Push as a separate timeline entry so
              // the renderer can show it as its own collapsed pill —
              // 12 rounds become 12 small pills the user can each
              // expand independently, not one giant blob.
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  next[msgIndex] = {
                    ...next[msgIndex],
                    timeline: [...tl, { kind: "thought", text: ev.text }],
                  };
                }
                return next;
              });
            } else if (ev.type === "tool_start") {
              // Update bottom status label dynamically based on tool name
              const toolLabel =
                friendlyToolName(ev.name ?? "")
                  .replace(/^[^\w\s]+/, "")
                  .trim() + "...";
              setCurrentPhaseLabel(toolLabel);

              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  next[msgIndex] = {
                    ...next[msgIndex],
                    timeline: [
                      ...tl,
                      {
                        kind: "tool",
                        name: ev.name,
                        args: ev.args,
                        status: "running",
                      },
                    ],
                  };
                }
                return next;
              });
            } else if (ev.type === "tool_result") {
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  // Walk backward to the most recent matching running
                  // tool entry and mark it done. Avoids cross-matching
                  // earlier same-named entries.
                  let updated = false;
                  const newTl: TimelineEntry[] = [];

                  let isToolErr = false;
                  if (typeof ev.result === "string") {
                    try {
                      // 1. Try to parse as JSON first (e.g. {"ok": true, "errors": []})
                      const parsed = JSON.parse(ev.result);
                      if (parsed && typeof parsed === "object") {
                        if (parsed.ok === true) {
                          isToolErr = false;
                        } else if (
                          parsed.ok === false ||
                          parsed.error ||
                          parsed.errors?.length > 0
                        ) {
                          isToolErr = true;
                        }
                      }
                    } catch {
                      // 2. If it's a raw string (like a bash crash), scan for fatal keywords
                      const lower = ev.result.toLowerCase();
                      if (
                        lower.includes("error") ||
                        lower.includes("failed") ||
                        lower.includes("unexpected") ||
                        lower.includes("not found")
                      ) {
                        isToolErr = true;
                      }
                    }
                  }

                  for (let i = tl.length - 1; i >= 0; i--) {
                    const e = tl[i];
                    if (
                      !updated &&
                      e.kind === "tool" &&
                      e.name === ev.name &&
                      e.status === "running"
                    ) {
                      newTl.unshift({
                        ...e,
                        status: isToolErr ? "error" : "done",
                        result: ev.result,
                      });
                      updated = true;
                    } else {
                      newTl.unshift(e);
                    }
                  }
                  next[msgIndex] = { ...next[msgIndex], timeline: newTl };
                }
                return next;
              });
            } else if (ev.type === "error") {
              const errText = ev.error || "Unknown error";
              const isToolErr = /tool|mcp|coolify|gitea/i.test(errText);
              const errBubble = isToolErr
                ? `⚠️ **Tool error:** ${errText}`
                : `⚠️ ${errText}`;
              assistantContent += (assistantContent ? "\n\n" : "") + errBubble;
              setMessages((prev) => {
                const next = [...prev];
                if (msgIndex >= 0 && next[msgIndex]) {
                  const tl = next[msgIndex].timeline ?? [];
                  next[msgIndex] = {
                    ...next[msgIndex],
                    timeline: [...tl, { kind: "text", text: errBubble }],
                  };
                }
                return next;
              });
            }
          }
        }

        // Auto-title thread from first message
        const thisThread = threads.find((t) => t.id === activeThread);
        if (thisThread?.title === "New conversation") {
          const title = text.slice(0, 50);
          await fetch(`/api/chat/threads/${activeThread}`, {
            method: "PATCH",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ title }),
          });
          setThreads((prev) =>
            prev.map((t) => (t.id === activeThread ? { ...t, title } : t)),
          );
        }

        loadThreads();
      } catch (e) {
        const isAbort = e instanceof DOMException && e.name === "AbortError";
        if (isAbort) {
          // Server-side will have appended "(stopped by user)" to the
          // partial response and persisted it. We just need to make
          // sure the local UI reflects whatever streamed in before the
          // user clicked Stop — which it already does, because we've
          // been mutating `messages[msgIndex]` chunk-by-chunk above.
          setMessages((prev) => {
            const next = [...prev];
            if (
              msgIndex >= 0 &&
              next[msgIndex] &&
              !next[msgIndex].content.includes("(stopped by user)")
            ) {
              next[msgIndex] = {
                ...next[msgIndex],
                content:
                  (next[msgIndex].content || "") + "\n\n_(stopped by user)_",
              };
            }
            return next;
          });
        } else {
          const errMsg = e instanceof Error ? e.message : String(e);
          const isNetwork = /fetch|network|failed to fetch/i.test(errMsg);
          const friendlyError = isNetwork
            ? "⚠️ Network error — check your connection and try again."
            : `⚠️ Something went wrong: ${errMsg.slice(0, 200)}\n\nYou can try again or start a new message.`;
          setMessages((prev) => {
            const next = [...prev];
            if (msgIndex >= 0 && next[msgIndex]) {
              next[msgIndex] = { ...next[msgIndex], content: friendlyError };
            }
            return next;
          });
        }
      } finally {
        abortRef.current = null;
        setSending(false);
        setCurrentPhaseLabel(null);
      }
    },
    [
      input,
      sending,
      activeThread,
      workspace,
      mcpToken,
      threads,
      loadThreads,
      unifiedProjectShell,
      chatMode,
      projectId,
      attachedFiles,
    ],
  );

  const cancelMessage = useCallback(() => {
    abortRef.current?.abort();
  }, []);

  // External components (e.g. ProjectHeaderUrls' "Start preview" button)
  // can ask the chat to send a canned prompt without prop-drilling. Open
  // the panel if collapsed, then fire the prompt as if the user typed it.
  useEffect(() => {
    function onPrompt(e: Event) {
      const ce = e as CustomEvent<{ prompt?: string; scopeProjectId?: string }>;
      const prompt = ce.detail?.prompt;
      if (!prompt) return;
      // If the dispatcher scopes the prompt to a specific project, only
      // accept it when the chat panel is currently bound to that project.
      // Prevents a "Start preview on Manifest" click from accidentally
      // landing in a chat that's scoped to a different project.
      if (ce.detail?.scopeProjectId && ce.detail.scopeProjectId !== projectId) {
        return;
      }
      setOpen(true);
      void sendMessage(prompt);
    }
    window.addEventListener("vibn:chat-prompt", onPrompt as EventListener);
    return () =>
      window.removeEventListener("vibn:chat-prompt", onPrompt as EventListener);
  }, [sendMessage, projectId]);

  const handleInputChange = (val: string) => {
    setInput(val);
    const match = val.match(/\/(\S*)$/);
    if (match) {
      setShowSuggestions(true);
      setSuggestionsFilter(match[1] || "");
      setSuggestionIndex(0);
    } else {
      setShowSuggestions(false);
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (showSuggestions) {
      const filtered = projectFiles
        .filter((f) =>
          f.toLowerCase().includes(suggestionsFilter.toLowerCase()),
        )
        .slice(0, 8);

      if (e.key === "ArrowDown") {
        e.preventDefault();
        setSuggestionIndex((prev) => (prev + 1) % Math.max(1, filtered.length));
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        setSuggestionIndex(
          (prev) => (prev - 1 + filtered.length) % Math.max(1, filtered.length),
        );
      } else if (e.key === "Enter" || e.key === "Tab") {
        e.preventDefault();
        if (filtered[suggestionIndex]) {
          const selectedFile = filtered[suggestionIndex];
          const before = input.slice(0, input.lastIndexOf("/"));
          setInput(before + " ");
          if (!attachedFiles.includes(selectedFile)) {
            setAttachedFiles((prev) => [...prev, selectedFile]);
          }
          setShowSuggestions(false);
        }
      } else if (e.key === "Escape") {
        e.preventDefault();
        setShowSuggestions(false);
      }
    } else {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        sendMessage();
      } else if (e.key === "Escape" && sending) {
        e.preventDefault();
        cancelMessage();
      }
    }
  };

  // Slide-out chat hidden until signed in. Structural project shell always
  // mounts so Preview / Product / Plan pages render; chat column shows loading
  // or sign-in instead of wiping the whole viewport (blank page).
  if (!unifiedProjectShell && status !== "authenticated") return null;

  // ── Collapsed tab ──────────────────────────────────────────────────────────
  // Structural mode is always-open; skip the collapsed-tab branch entirely.
  if (!open && !structural) {
    return (
      <button
        onClick={() => setOpen(true)}
        style={{
          position: "fixed",
          right: 0,
          top: "50%",
          transform: "translateY(-50%)",
          zIndex: 1000,
          background: "#1a1a1a",
          color: "#fff",
          border: "none",
          borderRadius: "8px 0 0 8px",
          padding: "14px 10px",
          cursor: "pointer",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 6,
          boxShadow: "-2px 0 12px #1a1a1a14",
        }}
        title="Open Vibn AI"
      >
        <MessageSquare style={{ width: 16, height: 16 }} />
        <span
          style={{
            writingMode: "vertical-rl",
            textOrientation: "mixed",
            fontSize: "0.65rem",
            fontWeight: 600,
            letterSpacing: "0.08em",
            fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
            transform: "rotate(180deg)",
          }}
        >
          VIBN AI
        </span>
      </button>
    );
  }

  const conversationColumn = (
    <>
      {/* Thread list dropdown */}
      {showThreads && (
        <div
          style={{
            borderBottom: "1px solid #e8e4dc",
            background: "#faf8f5",
            maxHeight: 200,
            overflowY: "auto",
            flexShrink: 0,
          }}
        >
          {threads.length === 0 && (
            <div
              style={{
                padding: "12px 16px",
                fontSize: "0.78rem",
                color: "#a09a90",
              }}
            >
              No conversations yet
            </div>
          )}
          {threads.map((t) => (
            <div
              key={t.id}
              onClick={() => loadThread(t.id)}
              style={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                padding: "9px 16px",
                background: activeThread === t.id ? "#f0ede8" : "transparent",
                cursor: "pointer",
                borderBottom: "1px solid #f0ede8",
              }}
              onMouseEnter={(e) => {
                if (activeThread !== t.id)
                  e.currentTarget.style.background = "#f7f4ef";
              }}
              onMouseLeave={(e) => {
                if (activeThread !== t.id)
                  e.currentTarget.style.background = "transparent";
              }}
            >
              <div style={{ minWidth: 0 }}>
                <div
                  style={{
                    fontSize: "0.8rem",
                    fontWeight: 500,
                    color: "#1a1a1a",
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                  }}
                >
                  {t.title}
                </div>
                <div style={{ fontSize: "0.7rem", color: "#a09a90" }}>
                  {timeAgo(t.updatedAt)}
                </div>
              </div>
              <button
                onClick={(e) => deleteThread(t.id, e)}
                style={{
                  background: "none",
                  border: "none",
                  cursor: "pointer",
                  padding: "2px 4px",
                  color: "#c0bab2",
                  flexShrink: 0,
                }}
              >
                <Trash2 style={{ width: 12, height: 12 }} />
              </button>
            </div>
          ))}
        </div>
      )}

      {/* Messages */}
      <div
        onScroll={(e) => {
          const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
          const distanceToBottom = scrollHeight - scrollTop - clientHeight;
          setShowScrollButton(distanceToBottom > 150);
        }}
        style={{
          flex: 1,
          minWidth: 0,
          overflowY: "auto",
          overflowX: "hidden",
          padding: "16px 14px",
        }}
      >
        {messages.length === 0 && !sending && (
          <div
            style={{ padding: "24px 14px", maxWidth: 640, margin: "0 auto" }}
          >
            <div style={{ display: "flex", gap: 12 }}>
              <div
                style={{
                  width: 28,
                  height: 28,
                  borderRadius: 6,
                  background: "#1a1a1a",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  flexShrink: 0,
                  color: "#fff",
                  fontSize: "0.85rem",
                  fontFamily: "var(--font-lora),serif",
                }}
              >
                V
              </div>
              <div
                style={{
                  background: "#fff",
                  border: "1px solid #e8e4dc",
                  padding: "14px 18px",
                  borderRadius: "0 12px 12px 12px",
                  fontSize: "0.9rem",
                  color: "#1a1a1a",
                  lineHeight: 1.6,
                  boxShadow: "0 1px 2px #1a1a1a05",
                  maxWidth: "85%",
                }}
              >
                Welcome to {activeProjectName ? activeProjectName : "Vibn"}!
                Tell me what you want to build and I&apos;ll scaffold it, run it
                in a preview, and ship it when you say so.
              </div>
            </div>
          </div>
        )}

        {messages.map((msg, i) => (
          <MessageBubble key={msg.id || i} msg={msg} />
        ))}

        <div ref={messagesEndRef} />
      </div>

      {/* Input */}
      <div
        style={{
          padding: "12px 14px",
          borderTop: "1px solid #e8e4dc",
          background: "#faf8f5",
          flexShrink: 0,
          position: "relative",
        }}
      >
        {!mcpToken && (
          <div
            style={{
              fontSize: "0.7rem",
              color: "#9a7b3a",
              background: "#d4a04a12",
              border: "1px solid #d4a04a30",
              borderRadius: 6,
              padding: "5px 10px",
              marginBottom: 8,
              lineHeight: 1.4,
            }}
          >
            Read-only mode — add your MCP token in Settings to enable actions.
          </div>
        )}
        {/* File Autocomplete Suggestions */}
        {showSuggestions &&
          (() => {
            const filtered = projectFiles
              .filter((f) =>
                f.toLowerCase().includes(suggestionsFilter.toLowerCase()),
              )
              .slice(0, 8);
            if (filtered.length === 0) return null;
            return (
              <div
                style={{
                  position: "absolute",
                  bottom: "calc(100% - 10px)",
                  left: 14,
                  right: 14,
                  background: "#fff",
                  border: "1px solid #e8e4dc",
                  borderRadius: 8,
                  boxShadow:
                    "0 -4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.02)",
                  zIndex: 1000,
                  maxHeight: 200,
                  overflowY: "auto",
                  padding: "4px 0",
                }}
              >
                <div
                  style={{
                    fontSize: "0.65rem",
                    fontWeight: 600,
                    color: "#a09a90",
                    padding: "6px 12px 4px",
                    borderBottom: "1px solid #f0ede8",
                    marginBottom: 4,
                    textTransform: "uppercase",
                    letterSpacing: "0.05em",
                  }}
                >
                  Codebase Files
                </div>
                {filtered.map((file, idx) => (
                  <div
                    key={file}
                    onClick={() => {
                      const before = input.slice(0, input.lastIndexOf("/"));
                      setInput(before + " ");
                      if (!attachedFiles.includes(file)) {
                        setAttachedFiles((prev) => [...prev, file]);
                      }
                      setShowSuggestions(false);
                    }}
                    onMouseEnter={() => setSuggestionIndex(idx)}
                    style={{
                      padding: "6px 12px",
                      fontSize: "0.76rem",
                      fontFamily: "var(--font-mono), monospace",
                      color: idx === suggestionIndex ? "#3d5afe" : "#1a1a1a",
                      background:
                        idx === suggestionIndex ? "#3d5afe0c" : "transparent",
                      cursor: "pointer",
                      display: "flex",
                      justifyContent: "space-between",
                      alignItems: "center",
                    }}
                  >
                    <span
                      style={{
                        overflow: "hidden",
                        textOverflow: "ellipsis",
                        whiteSpace: "nowrap",
                      }}
                    >
                      📄 {file}
                    </span>
                    {idx === suggestionIndex && (
                      <span
                        style={{
                          fontSize: "0.6rem",
                          color: "#3d5afe",
                          opacity: 0.7,
                          fontFamily: "var(--font-inter), sans-serif",
                        }}
                      >
                        press Enter
                      </span>
                    )}
                  </div>
                ))}
              </div>
            );
          })()}

        {/* Attached Files Chips */}
        {attachedFiles.length > 0 && (
          <div
            style={{
              display: "flex",
              flexWrap: "wrap",
              gap: 6,
              marginBottom: 8,
            }}
          >
            {attachedFiles.map((file) => (
              <div
                key={file}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: 6,
                  background: "#eef2ff",
                  border: "1px solid #c7d2fe",
                  borderRadius: 6,
                  padding: "4px 8px",
                  fontSize: "0.7rem",
                  color: "#312e81",
                  fontFamily: "var(--font-mono), monospace",
                }}
              >
                <span>📄 {file.split("/").pop()}</span>
                <button
                  type="button"
                  onClick={() =>
                    setAttachedFiles((prev) => prev.filter((f) => f !== file))
                  }
                  style={{
                    background: "none",
                    border: "none",
                    padding: 0,
                    cursor: "pointer",
                    color: "#4338ca",
                    display: "flex",
                    alignItems: "center",
                  }}
                  title="Remove file"
                >
                  <X style={{ width: 12, height: 12 }} />
                </button>
              </div>
            ))}
          </div>
        )}



        {/* Scroll to bottom button */}
        {showScrollButton && (
          <button
            onClick={scrollToBottom}
            style={{
              position: "absolute",
              bottom: "calc(100% + 12px)",
              left: "50%",
              transform: "translateX(-50%)",
              background: "#ffffff",
              border: "1px solid #e4e4e7",
              boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
              borderRadius: "50%",
              width: 32,
              height: 32,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              cursor: "pointer",
              zIndex: 50,
              color: "#52525b",
              transition: "all 0.2s ease",
            }}
            aria-label="Scroll to bottom"
          >
            <ArrowDown size={16} />
          </button>
        )}

        <ProjectPreviewChatInputWrap unifiedShell={unifiedProjectShell}>
          {(selectToggle) => (
            <div
              style={{
                display: "flex",
                flexDirection: "column",
                gap: 8,
                background: "#fff",
                borderRadius: 12,
                border: "1px solid #e4e4e7",
                padding: "10px 12px",
                boxShadow: "0 2px 6px rgba(0,0,0,0.03)",
              }}
            >
              <textarea
                ref={inputRef}
                value={input}
                onChange={(e) => handleInputChange(e.target.value)}
                onKeyDown={handleKeyDown}
                placeholder={
                  sending ? "Esc to stop generating…" : "Ask Vibn AI anything…"
                }
                rows={3}
                disabled={!activeThread}
                style={{
                  width: "100%",
                  border: "none",
                  outline: "none",
                  background: "transparent",
                  fontSize: "0.86rem",
                  lineHeight: 1.55,
                  resize: "none",
                  fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
                  color: "#1a1a1a",
                  maxHeight: 240,
                  overflowY: "auto",
                }}
                onInput={(e) => {
                  const el = e.currentTarget;
                  // Only resize if height actually changed
                  el.style.height = "auto";
                  el.style.height = Math.min(el.scrollHeight, 240) + "px";
                }}
              />
              <div
                style={{
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "flex-end",
                  gap: 8,
                  paddingTop: 4,
                }}
              >
                <div style={{ display: "flex", gap: 6, flex: 1 }}>
                  {/* Mode Toggles */}
                  <select
                    value={chatMode}
                    onChange={(e) => setChatMode(e.target.value as any)}
                    style={{
                      appearance: "none",
                      background: "transparent",
                      border: "none",
                      fontSize: "0.75rem",
                      fontWeight: 500,
                      color: "#71717a",
                      cursor: "pointer",
                      outline: "none",
                      padding: "4px 8px",
                    }}
                  >
                    <option value="vibe">Discuss</option>
                    <option value="collaborate">Plan</option>
                  </select>
                </div>
                <div style={{ display: "flex", gap: 6 }}>
                  <button
                    type="button"
                    onClick={() => {
                      const fname = window.prompt(
                        "Enter exact file path to attach (e.g. src/app/page.tsx):",
                      );
                      if (fname && fname.trim()) {
                        setAttachedFiles((prev) => [...prev, fname.trim()]);
                      }
                    }}
                    style={{
                      ...COMPOSER_ACTION_BTN_BASE,
                      background: "transparent",
                      color: "#a1a1aa",
                      border: "1px solid transparent",
                      cursor: "pointer",
                      transition: "all 0.1s ease",
                    }}
                    onMouseEnter={(e) => {
                      e.currentTarget.style.background = "#f4f4f5";
                      e.currentTarget.style.color = "#18181b";
                    }}
                    onMouseLeave={(e) => {
                      e.currentTarget.style.background = "transparent";
                      e.currentTarget.style.color = "#a1a1aa";
                    }}
                    title="Attach file to context"
                  >
                    <Paperclip style={{ width: 14, height: 14 }} />
                  </button>
                </div>
                {(() => {
                  // While the AI is streaming or running tools, the button
                  // turns into a Stop control. Click → AbortController fires,
                  // server bails between rounds, partial text gets persisted.
                  const isActive = sending;
                  const canSend = !sending && input.trim() && activeThread;
                  return (
                    <button
                      type="button"
                      onClick={isActive ? cancelMessage : () => sendMessage()}
                      disabled={!isActive && !canSend}
                      aria-label={isActive ? "Stop generating" : "Send message"}
                      title={isActive ? "Stop generating (Esc)" : "Send"}
                      style={{
                        ...COMPOSER_ACTION_BTN_BASE,
                        background: isActive
                          ? "#111827"
                          : canSend
                            ? "#111827"
                            : "#e4e4e7",
                        color: isActive || canSend ? "#fff" : "#a1a1aa",
                        border: "none",
                        cursor: isActive || canSend ? "pointer" : "not-allowed",
                        transition: "all 0.15s ease",
                      }}
                    >
                      {isActive ? (
                        <>
                          <Square
                            style={{
                              width: 11,
                              height: 11,
                              fill: "currentColor",
                              strokeWidth: 3,
                            }}
                          />
                        </>
                      ) : (
                        <Send style={{ width: 14, height: 14 }} />
                      )}
                    </button>
                  );
                })()}
              </div>
            </div>
          )}
        </ProjectPreviewChatInputWrap>
      </div>
    </>
  );

  const structuralChatLoadingColumn = (
    <div
      style={{
        flex: 1,
        minHeight: 0,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        padding: 24,
        gap: 14,
      }}
    >
      <Loader2
        className="animate-spin"
        style={{ width: 22, height: 22, color: "#9c9590" }}
      />
      <p style={{ fontSize: "0.84rem", color: "#6b6560", margin: 0 }}>
        Signing you in…
      </p>
    </div>
  );

  const authHref = pathname
    ? `/signin?callbackUrl=${encodeURIComponent(pathname)}`
    : "/signin";

  const structuralChatSignedOutColumn = (
    <div
      style={{
        flex: 1,
        minHeight: 0,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        padding: 24,
        gap: 12,
      }}
    >
      <p
        style={{
          fontSize: "0.88rem",
          fontWeight: 500,
          color: "#1a1a1a",
          margin: 0,
          textAlign: "center",
        }}
      >
        Sign in to use Vibn AI
      </p>
      <p
        style={{
          fontSize: "0.76rem",
          color: "#a09a90",
          margin: 0,
          textAlign: "center",
          maxWidth: 260,
          lineHeight: 1.5,
        }}
      >
        Preview and tabs still work here. Chat needs an account.
      </p>
      <Link
        href={authHref}
        style={{
          marginTop: 4,
          padding: "10px 18px",
          borderRadius: 10,
          background: "#1a1a1a",
          color: "#fff",
          fontSize: "0.82rem",
          fontWeight: 500,
          textDecoration: "none",
        }}
      >
        Sign in
      </Link>
    </div>
  );

  const structuralChatBody =
    status === "loading"
      ? structuralChatLoadingColumn
      : status !== "authenticated"
        ? structuralChatSignedOutColumn
        : conversationColumn;

  if (unifiedProjectShell) {
    return (
      <>
        <PreviewBridgeProvider>
          <div
            style={{
              flex: 1,
              minHeight: 0,
              minWidth: 0,
              display: "flex",
              flexDirection: "column",
              fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
              background: "#fff",
            }}
          >
            <div
              style={{
                display: "flex",
                flexDirection: "row",
                alignItems: "stretch",
                flexShrink: 0,
                height: 48,
                borderBottom: "1px solid #e4e4e7",
                background: "#faf8f5",
                boxSizing: "border-box",
              }}
            >
              <div
                style={{
                  width: 380,
                  flexShrink: 0,
                  height: "100%",
                  display: "flex",
                  alignItems: "center",
                  padding: "0 12px",
                  gap: 6,
                  boxSizing: "border-box",
                  borderRight: "1px solid #e4e4e7",
                }}
              >
                <div
                  style={{
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "space-between",
                    width: "100%",
                    gap: 6,
                  }}
                >
                  <div
                    style={{
                      display: "flex",
                      alignItems: "center",
                      gap: 8,
                      minWidth: 0,
                    }}
                  >
                    <Link
                      href={workspace ? `/${workspace}/projects` : "/"}
                      title="All projects"
                      style={{ flexShrink: 0, display: "flex" }}
                    >
                      <img
                        src="/vibn-black-circle-logo.png"
                        alt="VIBN"
                        width={26}
                        height={26}
                        style={{
                          borderRadius: 7,
                          objectFit: "cover",
                          display: "block",
                        }}
                      />
                    </Link>
                    <button
                      type="button"
                      onClick={() => setShowThreads((v) => !v)}
                      aria-label="Conversations"
                      style={{
                        display: "flex",
                        alignItems: "center",
                        gap: 4,
                        background: "none",
                        border: "none",
                        cursor: "pointer",
                        padding: "2px 4px",
                        borderRadius: 6,
                        minWidth: 0,
                      }}
                    >
                      <ChevronDown
                        style={{
                          width: 12,
                          height: 12,
                          color: "#a09a90",
                          transition: "transform 0.15s",
                          flexShrink: 0,
                          transform: showThreads ? "rotate(180deg)" : "none",
                        }}
                      />
                      {projectId && (
                        <span
                          title={`Chat is scoped to ${activeProjectName ?? "this project"}. Tool calls assume projectId=${projectId}.`}
                          style={{
                            marginLeft: 4,
                            padding: "1px 6px",
                            fontSize: "0.6rem",
                            fontWeight: 600,
                            letterSpacing: "0.02em",
                            color: "#3d5afe",
                            background: "#3d5afe14",
                            borderRadius: 999,
                            maxWidth: 140,
                            whiteSpace: "nowrap",
                            overflow: "hidden",
                            textOverflow: "ellipsis",
                          }}
                        >
                          ▸ {activeProjectName ?? "this project"}
                        </span>
                      )}
                    </button>
                  </div>
                  <button
                    type="button"
                    onClick={newThread}
                    style={{
                      background: "none",
                      border: "none",
                      cursor: "pointer",
                      padding: "4px 5px",
                      borderRadius: 6,
                      color: "#6b6560",
                      display: "flex",
                      alignItems: "center",
                    }}
                    title="New conversation"
                  >
                    <Plus style={{ width: 14, height: 14 }} />
                  </button>
                </div>
              </div>
              <div
                style={{
                  flex: 1,
                  minWidth: 0,
                  minHeight: 0,
                  display: "flex",
                  alignItems: "stretch",
                }}
              >
                <ProjectIconRail
                  workspace={workspace}
                  projectId={projectId}
                  actions={
                    <button
                      type="button"
                      onClick={() => {
                        if (activeThread && !sending) {
                          sendMessage("ship it");
                        }
                      }}
                      disabled={!activeThread || sending}
                      style={{
                        background: "#18181b",
                        border: "none",
                        cursor:
                          activeThread && !sending ? "pointer" : "not-allowed",
                        padding: "6px 14px",
                        borderRadius: 6,
                        color: "#fff",
                        display: "flex",
                        alignItems: "center",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        opacity: activeThread && !sending ? 1 : 0.5,
                        transition: "opacity 0.15s ease",
                      }}
                      title="Ship to production"
                    >
                      Publish
                    </button>
                  }
                />
              </div>
            </div>

            <div
              style={{
                flex: 1,
                display: "flex",
                flexDirection: "row",
                minHeight: 0,
                minWidth: 0,
                position: "relative", // Ensure relative positioning for z-index stacking context
              }}
            >
              <div
                style={{
                  width: isChatMinimized ? 52 : 380,
                  flexShrink: 0,
                  borderRight: "1px solid #e4e4e7",
                  background: "#fff",
                  display: "flex",
                  flexDirection: "column",
                  minHeight: 0,
                  minWidth: 0,
                  transition: "width 0.2s ease-in-out",
                  overflow: "visible", // Changed from hidden to visible so the collapse button can hang over the edge
                  position: "relative",
                  zIndex: 40, // High z-index to pop over the right panel
                }}
              >
                <div
                  style={{
                    position: "absolute",
                    zIndex: 50,
                    right: -12,
                    top: 12,
                  }}
                >
                  <button
                    onClick={() => setIsChatMinimized(!isChatMinimized)}
                    style={{
                      background: "#fff",
                      border: "1px solid #e4e4e7",
                      width: 24,
                      height: 24,
                      borderRadius: "50%",
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      cursor: "pointer",
                      color: "#a1a1aa",
                      boxShadow: "0 2px 4px rgba(0,0,0,0.05)",
                    }}
                    title={isChatMinimized ? "Expand Chat" : "Minimize Chat"}
                  >
                    {isChatMinimized ? (
                      <ChevronRight size={14} />
                    ) : (
                      <ChevronLeft size={14} />
                    )}
                  </button>
                </div>

                {isChatMinimized ? (
                  <div
                    style={{
                      flex: 1,
                      display: "flex",
                      flexDirection: "column",
                      alignItems: "center",
                      paddingTop: 16,
                    }}
                  ></div>
                ) : (
                  <div
                    style={{
                      flex: 1,
                      display: "flex",
                      flexDirection: "column",
                      overflow: "hidden",
                    }}
                  >
                    {structuralChatBody}
                  </div>
                )}
              </div>
              <div
                key={pathname}
                style={{
                  flex: 1,
                  minWidth: 0,
                  minHeight: 0,
                  overflow: "auto",
                  background: "#faf8f5",
                  display: "flex",
                  flexDirection: "column",
                  position: "relative",
                  zIndex: 10, // Lower z-index so the collapse button stays on top
                }}
              >
                {artifactSlot}
              </div>
            </div>
          </div>
        </PreviewBridgeProvider>

        <style>{`
        @keyframes vibn-bounce {
          0%, 60%, 100% { transform: translateY(0); }
          30% { transform: translateY(-4px); }
        }
      `}</style>
      </>
    );
  }

  // ── Open panel ─────────────────────────────────────────────────────────────
  // Structural mode: fill the parent column. Default mode: fixed slide-out
  // anchored to the right edge of the viewport (legacy behavior).
  return (
    <div
      style={
        structural
          ? {
              flex: 1,
              minHeight: 0,
              background: "#fff",
              display: "flex",
              flexDirection: "column",
              fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
            }
          : {
              position: "fixed",
              right: 0,
              top: 0,
              bottom: 0,
              zIndex: 999,
              width: 380,
              background: "#fff",
              borderLeft: "1px solid #e8e4dc",
              display: "flex",
              flexDirection: "column",
              boxShadow: "-4px 0 24px #1a1a1a08",
              fontFamily: "var(--font-inter),ui-sans-serif,sans-serif",
            }
      }
    >
      {/* Header — structural: circle logo + threads; slide-out: wordmark */}
      <div
        style={
          structural
            ? {
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                height: 48,
                padding: "0 12px",
                boxSizing: "border-box",
                borderBottom: "1px solid #e8e4dc",
                background: "#faf8f5",
                flexShrink: 0,
                gap: 6,
              }
            : {
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                padding: "14px 16px",
                borderBottom: "1px solid #e8e4dc",
                background: "#faf8f5",
                flexShrink: 0,
              }
        }
      >
        {structural ? (
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 8,
              minWidth: 0,
            }}
          >
            <Link
              href={workspace ? `/${workspace}/projects` : "/"}
              title="All projects"
              style={{ flexShrink: 0, display: "flex" }}
            >
              <img
                src="/vibn-black-circle-logo.png"
                alt="VIBN"
                width={26}
                height={26}
                style={{
                  borderRadius: 7,
                  objectFit: "cover",
                  display: "block",
                }}
              />
            </Link>
            <button
              type="button"
              onClick={() => setShowThreads((v) => !v)}
              aria-label="Conversations"
              style={{
                display: "flex",
                alignItems: "center",
                gap: 4,
                background: "none",
                border: "none",
                cursor: "pointer",
                padding: "2px 4px",
                borderRadius: 6,
                minWidth: 0,
              }}
            >
              <ChevronDown
                style={{
                  width: 12,
                  height: 12,
                  color: "#a09a90",
                  transition: "transform 0.15s",
                  flexShrink: 0,
                  transform: showThreads ? "rotate(180deg)" : "none",
                }}
              />
              {projectId && (
                <span
                  title={`Chat is scoped to ${activeProjectName ?? "this project"}. Tool calls assume projectId=${projectId}.`}
                  style={{
                    marginLeft: 4,
                    padding: "1px 6px",
                    fontSize: "0.6rem",
                    fontWeight: 600,
                    letterSpacing: "0.02em",
                    color: "#3d5afe",
                    background: "#3d5afe14",
                    borderRadius: 999,
                    maxWidth: 140,
                    whiteSpace: "nowrap",
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                  }}
                >
                  ▸ {activeProjectName ?? "this project"}
                </span>
              )}
            </button>
          </div>
        ) : (
          <button
            type="button"
            onClick={() => setShowThreads((v) => !v)}
            style={{
              display: "flex",
              alignItems: "center",
              gap: 6,
              background: "none",
              border: "none",
              cursor: "pointer",
              padding: "4px 6px",
              borderRadius: 6,
              minWidth: 0,
            }}
          >
            <span
              style={{
                fontFamily: "var(--font-lora),serif",
                fontSize: "0.95rem",
                fontWeight: 400,
                color: "#1a1a1a",
                letterSpacing: "-0.01em",
                whiteSpace: "nowrap",
              }}
            >
              Vibn AI
            </span>
            <ChevronDown
              style={{
                width: 13,
                height: 13,
                color: "#a09a90",
                transition: "transform 0.15s",
                flexShrink: 0,
                transform: showThreads ? "rotate(180deg)" : "none",
              }}
            />
            {projectId && (
              <span
                title={`Chat is scoped to ${activeProjectName ?? "this project"}. Tool calls assume projectId=${projectId}.`}
                style={{
                  marginLeft: 6,
                  padding: "2px 8px",
                  fontSize: "0.65rem",
                  fontWeight: 600,
                  letterSpacing: "0.02em",
                  color: "#3d5afe",
                  background: "#3d5afe14",
                  borderRadius: 999,
                  maxWidth: 160,
                  whiteSpace: "nowrap",
                  overflow: "hidden",
                  textOverflow: "ellipsis",
                }}
              >
                ▸ {activeProjectName ?? "this project"}
              </span>
            )}
          </button>
        )}
        <div style={{ display: "flex", gap: 4 }}>
          <button
            type="button"
            onClick={newThread}
            style={{
              background: "none",
              border: "none",
              cursor: "pointer",
              padding: structural ? "4px 5px" : "5px 6px",
              borderRadius: 6,
              color: "#6b6560",
              display: "flex",
              alignItems: "center",
            }}
            title="New conversation"
          >
            <Plus
              style={{
                width: structural ? 14 : 15,
                height: structural ? 14 : 15,
              }}
            />
          </button>
          {!structural && (
            <button
              type="button"
              onClick={() => setOpen(false)}
              style={{
                background: "none",
                border: "none",
                cursor: "pointer",
                padding: "5px 6px",
                borderRadius: 6,
                color: "#6b6560",
                display: "flex",
                alignItems: "center",
              }}
              title="Close"
            >
              <ChevronRight style={{ width: 15, height: 15 }} />
            </button>
          )}
        </div>
      </div>

      {conversationColumn}

      <style>{`
        @keyframes vibn-bounce {
          0%, 60%, 100% { transform: translateY(0); }
          30% { transform: translateY(-4px); }
        }
      `}</style>
    </div>
  );
}

