import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import type { TargetedTouchEvent } from "preact";
import {
  Check,
  ChevronDown,
  GitFork,
  Globe2,
  LoaderCircle,
  Menu,
  MessageSquare,
  Pencil,
  Plus,
  Rabbit,
  Search,
  SendHorizontal,
  Settings2,
  Star,
  Trash2,
  X,
} from "lucide-preact";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import { AuthScreen } from "@/components/auth/auth-screen";
import { ChatAttachmentList } from "@/components/chat/chat-attachment-list";
import { ChatComposer } from "@/components/chat/chat-composer";
import { ChatMessagesPanel } from "@/components/chat/chat-messages-panel";
import { SearchResultsPanel } from "@/components/search/search-results-panel";
import { SybilCharacter } from "@/components/sybil-character";
import {
  createChat,
  createChatFromSearch,
  createSearch,
  deleteChat,
  deleteSearch,
  forkChat,
  attachCompletionStream,
  attachSearchStream,
  getActiveRuns,
  getChat,
  listChatTools,
  listModels,
  getSearch,
  listWorkspaceItems,
  runCompletionStream,
  runSearchStream,
  suggestChatTitle,
  updateChatTitle,
  updateChatStar,
  updateSearchStar,
  updateChatSettings,
  getMessageAttachments,
  type ChatAttachment,
  type ActiveRunsResponse,
  type ModelCatalogResponse,
  type Provider,
  type ChatDetail,
  type ChatSummary,
  type ChatToolInfo,
  type CompletionRequestMessage,
  type Message,
  type SearchDetail,
  type SearchSummary,
  type ToolCallEvent,
  type WorkspaceItem,
} from "@/lib/api";
import { useSessionAuth } from "@/hooks/use-session-auth";
import {
  getChatModelSelection,
  getChatModelSelectionSyncKey,
} from "@/lib/chat-model-selection";
import {
  filterSidebarItemsWithForkGroups,
  getForkGroupPresentation,
  groupForkedChatItems,
  shouldRequestChatTitle,
} from "@/lib/chat-forking";
import {
  resolveSidebarSelectionAfterRefresh,
  type SidebarSelection,
} from "@/lib/sidebar-selection";
import { cn } from "@/lib/utils";

type DraftSelectionKind = "chat" | "search";
type SidebarItem = SidebarSelection & {
  title: string;
  parentChatId: string | null;
  updatedAt: string;
  createdAt: string;
  starred: boolean;
  starredAt: string | null;
  initiatedProvider: Provider | null;
  initiatedModel: string | null;
  lastUsedProvider: Provider | null;
  lastUsedModel: string | null;
};
type ContextMenuState = {
  kind: "sidebar";
  item: SidebarSelection;
  x: number;
  y: number;
} | {
  kind: "message";
  chatId: string;
  messageId: string;
  x: number;
  y: number;
};
type RenameChatDialogState = {
  chatId: string;
};
type PendingChatState = {
  messages: Message[];
};
type ActiveRunsState = {
  chats: Record<string, true>;
  searches: Record<string, true>;
};
type RefreshCollectionsOptions = {
  initialSelection?: SidebarSelection;
  reportTransientError?: boolean;
  selectFallback?: boolean;
};

function readSidebarSelectionFromUrl(): SidebarSelection | null {
  if (typeof window === "undefined") return null;
  const params = new URLSearchParams(window.location.search);
  const chatId = params.get("chat")?.trim();
  if (chatId) {
    return { kind: "chat", id: chatId };
  }
  const searchId = params.get("search")?.trim();
  if (searchId) {
    return { kind: "search", id: searchId };
  }
  return null;
}

function buildWorkspaceUrl(selection: SidebarSelection | null) {
  if (typeof window === "undefined") return "/";
  const params = new URLSearchParams(window.location.search);
  params.delete("chat");
  params.delete("search");
  if (selection) {
    params.set(selection.kind === "chat" ? "chat" : "search", selection.id);
  }
  const query = params.toString();
  return `${window.location.pathname}${query ? `?${query}` : ""}`;
}

const PROVIDER_FALLBACK_MODELS: Record<Provider, string[]> = {
  openai: ["gpt-4.1-mini"],
  anthropic: ["claude-3-5-sonnet-latest"],
  xai: ["grok-3-mini"],
  gemini: ["gemini-3.5-flash", "gemini-flash-latest"],
  "hermes-agent": ["hermes-agent"],
};

const EMPTY_MODEL_CATALOG: ModelCatalogResponse["providers"] = {
  openai: { models: [], loadedAt: null, error: null },
  anthropic: { models: [], loadedAt: null, error: null },
  xai: { models: [], loadedAt: null, error: null },
  gemini: { models: [], loadedAt: null, error: null },
};

const BASE_PROVIDERS: Provider[] = ["openai", "anthropic", "xai", "gemini"];
const ALL_PROVIDERS: Provider[] = [...BASE_PROVIDERS, "hermes-agent"];

const MODEL_PREFERENCES_STORAGE_KEY = "sybil:modelPreferencesByProvider";
const QUICK_QUESTION_MODEL_SELECTION_STORAGE_KEY = "sybil:quickQuestionModelSelection";
const STREAM_RESUME_RETRY_DELAYS_MS = [0, 250, 750, 1500];
const MOBILE_LAYOUT_MEDIA_QUERY = "(max-width: 767px)";
const MOBILE_SWIPE_ACTIVATION_DISTANCE = 18;
const MOBILE_SWIPE_DIRECTION_DOMINANCE = 1.22;
const MOBILE_SWIPE_VELOCITY_PROJECTION_SECONDS = 0.18;
const MOBILE_SWIPE_COMPLETION_VELOCITY = 620;

type MobileWorkspaceSwipe = {
  touchIdentifier: number;
  startX: number;
  startY: number;
  lastX: number;
  lastTimestamp: number;
  velocityX: number;
  direction: -1 | 1 | null;
};

type ProviderModelPreferences = Record<Provider, string | null>;

type QuickQuestionModelSelection = {
  provider: Provider;
  modelPreferences: ProviderModelPreferences;
};

const EMPTY_MODEL_PREFERENCES: ProviderModelPreferences = {
  openai: null,
  anthropic: null,
  xai: null,
  gemini: null,
  "hermes-agent": null,
};
const EMPTY_ACTIVE_RUNS: ActiveRunsState = {
  chats: {},
  searches: {},
};

function isMobileLayout() {
  return typeof window !== "undefined" && window.matchMedia(MOBILE_LAYOUT_MEDIA_QUERY).matches;
}

function getMobileSwipeLatchDistance() {
  if (typeof window === "undefined") return 112;
  return Math.min(Math.max(window.innerWidth * 0.28, 112), 152);
}

function findTouch(touches: TouchList, identifier: number) {
  for (let index = 0; index < touches.length; index += 1) {
    const touch = touches.item(index);
    if (touch?.identifier === identifier) return touch;
  }
  return null;
}

function waitForAppForeground() {
  if (typeof document === "undefined" || typeof window === "undefined") return Promise.resolve();
  if (document.visibilityState !== "hidden") return Promise.resolve();

  return new Promise<void>((resolve) => {
    const finishIfReady = () => {
      if (document.visibilityState === "hidden") return;
      document.removeEventListener("visibilitychange", finishIfReady);
      window.removeEventListener("pageshow", finishIfReady);
      resolve();
    };

    document.addEventListener("visibilitychange", finishIfReady);
    window.addEventListener("pageshow", finishIfReady);
  });
}

function waitForRetry(delayMs: number) {
  if (!delayMs) return Promise.resolve();
  return new Promise<void>((resolve) => window.setTimeout(resolve, delayMs));
}

async function retryAfterAppResume<T>(operation: () => Promise<T>) {
  await waitForAppForeground();
  let lastError: unknown = new Error("Unable to reconnect");

  for (const delayMs of STREAM_RESUME_RETRY_DELAYS_MS) {
    await waitForRetry(delayMs);
    try {
      return await operation();
    } catch (err) {
      lastError = err;
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) throw err;
    }
  }

  throw lastError;
}

function getActiveRunsAfterResume() {
  return retryAfterAppResume(getActiveRuns);
}

function createClientRequestId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    return crypto.randomUUID();
  }
  return `web-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}

function isRecoverableStreamDisconnect(error: unknown) {
  if (error instanceof TypeError) return true;
  const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
  return (
    message.includes("failed to fetch") ||
    message.includes("load failed") ||
    message.includes("network error") ||
    message.includes("networkerror") ||
    message.includes("no response stream") ||
    message.includes("stream disconnected before completion")
  );
}

function hasNewPersistedUserMessage(chat: ChatDetail, previousMessageIds: Set<string>, content: string, attachments: ChatAttachment[]) {
  return chat.messages.some((message) => {
    if (previousMessageIds.has(message.id) || message.role !== "user" || message.content !== content) return false;
    const persistedAttachmentIds = new Set(getMessageAttachments(message.metadata).map((attachment) => attachment.id));
    return attachments.length === persistedAttachmentIds.size && attachments.every((attachment) => persistedAttachmentIds.has(attachment.id));
  });
}

const TRANSCRIPT_BOTTOM_GAP = 20;
const REPLY_SCROLL_BUFFER_MIN = 288;
const REPLY_SCROLL_BUFFER_MAX = 576;
const REPLY_SCROLL_BUFFER_VIEWPORT_RATIO = 0.52;
const MAX_CHAT_ATTACHMENTS = 8;
const MAX_IMAGE_ATTACHMENT_BYTES = 6 * 1024 * 1024;
const MAX_TEXT_ATTACHMENT_BYTES = 8 * 1024 * 1024;
const MAX_TEXT_ATTACHMENT_CHARS = 200_000;
const CHAT_FILE_ACCEPT =
  ".png,.jpg,.jpeg,.txt,.md,.markdown,.csv,.tsv,.json,.jsonl,.xml,.yaml,.yml,.html,.htm,.css,.js,.jsx,.ts,.tsx,.py,.rb,.java,.c,.cc,.cpp,.h,.hpp,.go,.rs,.sh,.sql,.log,.toml,.ini,.cfg,.conf,.swift,.kt,.m,.mm";
const TEXT_ATTACHMENT_EXTENSIONS = new Set([
  ".txt",
  ".md",
  ".markdown",
  ".csv",
  ".tsv",
  ".json",
  ".jsonl",
  ".xml",
  ".yaml",
  ".yml",
  ".html",
  ".htm",
  ".css",
  ".js",
  ".jsx",
  ".ts",
  ".tsx",
  ".py",
  ".rb",
  ".java",
  ".c",
  ".cc",
  ".cpp",
  ".h",
  ".hpp",
  ".go",
  ".rs",
  ".sh",
  ".sql",
  ".log",
  ".toml",
  ".ini",
  ".cfg",
  ".conf",
  ".swift",
  ".kt",
  ".m",
  ".mm",
]);
const TEXT_ATTACHMENT_MIME_TYPES = new Set([
  "application/json",
  "application/ld+json",
  "application/sql",
  "application/toml",
  "application/x-httpd-php",
  "application/x-javascript",
  "application/x-sh",
  "application/xml",
  "application/yaml",
  "application/x-yaml",
  "image/svg+xml",
]);

function getModelOptions(catalog: ModelCatalogResponse["providers"], provider: Provider) {
  const providerModels = catalog[provider]?.models ?? [];
  if (providerModels.length) return providerModels;
  return PROVIDER_FALLBACK_MODELS[provider];
}

function getVisibleProviders(catalog: ModelCatalogResponse["providers"]) {
  return ALL_PROVIDERS.filter((provider) => provider !== "hermes-agent" || catalog[provider] !== undefined);
}

function getReplyScrollBufferHeight() {
  if (typeof window === "undefined") return REPLY_SCROLL_BUFFER_MIN;
  return Math.min(
    REPLY_SCROLL_BUFFER_MAX,
    Math.max(REPLY_SCROLL_BUFFER_MIN, Math.round(window.innerHeight * REPLY_SCROLL_BUFFER_VIEWPORT_RATIO))
  );
}

function getFileExtension(filename: string) {
  const index = filename.lastIndexOf(".");
  return index >= 0 ? filename.slice(index).toLowerCase() : "";
}

function createAttachmentId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    return crypto.randomUUID();
  }
  return `att-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}

function inferImageMimeType(file: File) {
  if (file.type === "image/png" || file.type === "image/jpeg") return file.type;
  const extension = getFileExtension(file.name);
  if (extension === ".png") return "image/png";
  if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
  return null;
}

function isTextLikeFile(file: File) {
  const mimeType = file.type.toLowerCase();
  if (mimeType.startsWith("text/")) return true;
  if (TEXT_ATTACHMENT_MIME_TYPES.has(mimeType)) return true;
  return TEXT_ATTACHMENT_EXTENSIONS.has(getFileExtension(file.name));
}

function arrayBufferToBase64(buffer: ArrayBuffer) {
  const bytes = new Uint8Array(buffer);
  const chunkSize = 0x8000;
  let binary = "";
  for (let index = 0; index < bytes.length; index += chunkSize) {
    const chunk = bytes.subarray(index, index + chunkSize);
    binary += String.fromCharCode(...chunk);
  }
  return btoa(binary);
}

async function buildChatAttachment(file: File): Promise<ChatAttachment> {
  const imageMimeType = inferImageMimeType(file);
  if (imageMimeType) {
    if (file.size > MAX_IMAGE_ATTACHMENT_BYTES) {
      throw new Error(`Image '${file.name}' exceeds the 6 MB upload limit.`);
    }
    const base64 = arrayBufferToBase64(await file.arrayBuffer());
    return {
      kind: "image",
      id: createAttachmentId(),
      filename: file.name,
      mimeType: imageMimeType,
      sizeBytes: file.size,
      dataUrl: `data:${imageMimeType};base64,${base64}`,
    };
  }

  if (!isTextLikeFile(file)) {
    throw new Error(`Unsupported file type for '${file.name}'. Use PNG/JPEG images or text-based files.`);
  }

  if (file.size > MAX_TEXT_ATTACHMENT_BYTES) {
    throw new Error(`Text file '${file.name}' exceeds the 8 MB upload limit.`);
  }

  const normalizedText = (await file.text()).replace(/\r\n/g, "\n").replace(/\u0000/g, "");
  const truncated = normalizedText.length > MAX_TEXT_ATTACHMENT_CHARS;
  return {
    kind: "text",
    id: createAttachmentId(),
    filename: file.name,
    mimeType: file.type || "text/plain",
    sizeBytes: file.size,
    text: truncated ? normalizedText.slice(0, MAX_TEXT_ATTACHMENT_CHARS) : normalizedText,
    truncated,
  };
}

function buildAttachmentSummary(attachments: ChatAttachment[]) {
  if (!attachments.length) return "";
  const filenames = attachments.map((attachment) => attachment.filename).join(", ");
  return attachments.length === 1 ? filenames : `Attached: ${filenames}`;
}

function getFilesFromDataTransfer(dataTransfer: DataTransfer | null) {
  if (!dataTransfer) return [];
  const fromItems = Array.from(dataTransfer.items ?? [])
    .filter((item) => item.kind === "file")
    .map((item) => item.getAsFile())
    .filter((file): file is File => file instanceof File);
  if (fromItems.length) return fromItems;
  return Array.from(dataTransfer.files ?? []);
}

function hasFileTransfer(dataTransfer: DataTransfer | null) {
  if (!dataTransfer) return false;
  return Array.from(dataTransfer.types ?? []).includes("Files") || getFilesFromDataTransfer(dataTransfer).length > 0;
}

function loadStoredModelPreferences() {
  if (typeof window === "undefined") return EMPTY_MODEL_PREFERENCES;
  try {
    const raw = window.localStorage.getItem(MODEL_PREFERENCES_STORAGE_KEY);
    if (!raw) return EMPTY_MODEL_PREFERENCES;
    const parsed = JSON.parse(raw) as Partial<Record<Provider, string>>;
    return {
      openai: typeof parsed.openai === "string" && parsed.openai.trim() ? parsed.openai.trim() : null,
      anthropic: typeof parsed.anthropic === "string" && parsed.anthropic.trim() ? parsed.anthropic.trim() : null,
      xai: typeof parsed.xai === "string" && parsed.xai.trim() ? parsed.xai.trim() : null,
      gemini: typeof parsed.gemini === "string" && parsed.gemini.trim() ? parsed.gemini.trim() : null,
      "hermes-agent":
        typeof parsed["hermes-agent"] === "string" && parsed["hermes-agent"].trim() ? parsed["hermes-agent"].trim() : null,
    };
  } catch {
    return EMPTY_MODEL_PREFERENCES;
  }
}

function normalizeStoredProvider(value: unknown): Provider {
  return value === "anthropic" || value === "xai" || value === "gemini" || value === "openai" || value === "hermes-agent"
    ? value
    : "openai";
}

function normalizeStoredModelPreferences(value: unknown): ProviderModelPreferences {
  if (!value || typeof value !== "object" || Array.isArray(value)) return EMPTY_MODEL_PREFERENCES;
  const parsed = value as Partial<Record<Provider, unknown>>;
    return {
      openai: typeof parsed.openai === "string" && parsed.openai.trim() ? parsed.openai.trim() : null,
      anthropic: typeof parsed.anthropic === "string" && parsed.anthropic.trim() ? parsed.anthropic.trim() : null,
      xai: typeof parsed.xai === "string" && parsed.xai.trim() ? parsed.xai.trim() : null,
      gemini: typeof parsed.gemini === "string" && parsed.gemini.trim() ? parsed.gemini.trim() : null,
      "hermes-agent":
        typeof parsed["hermes-agent"] === "string" && parsed["hermes-agent"].trim() ? parsed["hermes-agent"].trim() : null,
    };
}

function loadStoredQuickQuestionModelSelection(): QuickQuestionModelSelection {
  if (typeof window === "undefined") {
    return { provider: "openai", modelPreferences: EMPTY_MODEL_PREFERENCES };
  }
  try {
    const raw = window.localStorage.getItem(QUICK_QUESTION_MODEL_SELECTION_STORAGE_KEY);
    if (!raw) return { provider: "openai", modelPreferences: EMPTY_MODEL_PREFERENCES };
    const parsed = JSON.parse(raw) as { provider?: unknown; modelPreferences?: unknown };
    return {
      provider: normalizeStoredProvider(parsed.provider),
      modelPreferences: normalizeStoredModelPreferences(parsed.modelPreferences),
    };
  } catch {
    return { provider: "openai", modelPreferences: EMPTY_MODEL_PREFERENCES };
  }
}

function pickProviderModel(options: string[], preferred: string | null) {
  if (preferred?.trim()) return preferred.trim();
  return options[0] ?? "";
}

function getProviderLabel(provider: Provider | null | undefined) {
  if (provider === "openai") return "OpenAI";
  if (provider === "anthropic") return "Anthropic";
  if (provider === "xai") return "xAI";
  if (provider === "gemini") return "Gemini";
  if (provider === "hermes-agent") return "Hermes Agent";
  return "";
}

function getToolLabel(name: string) {
  if (name === "web_search") return "Web search";
  if (name === "fetch_url") return "Fetch URL";
  if (name === "codex_exec") return "Codex";
  if (name === "shell_exec") return "Shell";
  return name
    .split("_")
    .filter(Boolean)
    .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
    .join(" ");
}

function getDefaultEnabledTools(availableTools: ChatToolInfo[]) {
  return availableTools.map((tool) => tool.name);
}

function normalizeEnabledTools(value: unknown, availableTools: ChatToolInfo[]) {
  const available = new Set(availableTools.map((tool) => tool.name));
  if (!Array.isArray(value)) return getDefaultEnabledTools(availableTools);
  return [...new Set(value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean))].filter((name) =>
    available.has(name)
  );
}

type ToolLogMetadata = {
  kind: "tool_call";
  toolCallId?: string;
  toolName?: string;
  status?: "initiated" | "completed" | "failed";
  summary?: string;
  args?: Record<string, unknown>;
  startedAt?: string;
  completedAt?: string;
  durationMs?: number;
  error?: string | null;
  resultPreview?: string | null;
};

function asToolLogMetadata(value: unknown): ToolLogMetadata | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  const record = value as Record<string, unknown>;
  if (record.kind !== "tool_call") return null;
  return record as ToolLogMetadata;
}

function isToolCallLogMessage(message: Message) {
  return asToolLogMetadata(message.metadata) !== null;
}

function isDisplayableMessage(message: Message) {
  return message.role !== "system";
}

function buildOptimisticToolMessage(event: ToolCallEvent): Message {
  const metadata: ToolLogMetadata = {
    kind: "tool_call",
    toolCallId: event.toolCallId,
    toolName: event.name,
    status: event.status,
    summary: event.summary,
    args: event.args,
    startedAt: event.startedAt,
    error: event.error ?? null,
    resultPreview: event.resultPreview ?? null,
  };

  if (event.completedAt) metadata.completedAt = event.completedAt;
  if (typeof event.durationMs === "number") metadata.durationMs = event.durationMs;

  return {
    id: `temp-tool-${event.toolCallId}`,
    createdAt: event.completedAt ?? event.startedAt ?? new Date().toISOString(),
    role: "tool",
    content: event.summary,
    name: event.name,
    metadata,
  };
}

function upsertOptimisticToolMessage(messages: Message[], event: ToolCallEvent, assistantMessagePrefix: string) {
  const toolMessage = buildOptimisticToolMessage(event);
  const existingIndex = messages.findIndex(
    (message) => asToolLogMetadata(message.metadata)?.toolCallId === event.toolCallId || message.id === `temp-tool-${event.toolCallId}`
  );

  if (existingIndex >= 0) {
    return messages.map((message, index) => (index === existingIndex ? { ...toolMessage, id: message.id } : message));
  }

  const assistantIndex = messages.findIndex(
    (message, index, all) => index === all.length - 1 && message.id.startsWith(assistantMessagePrefix)
  );
  if (assistantIndex < 0) return messages.concat(toolMessage);
  return [...messages.slice(0, assistantIndex), toolMessage, ...messages.slice(assistantIndex)];
}

type ModelComboboxProps = {
  options: string[];
  value: string;
  onChange: (value: string) => void;
  disabled?: boolean;
};

function ModelCombobox({ options, value, onChange, disabled = false }: ModelComboboxProps) {
  const [open, setOpen] = useState(false);
  const [draftValue, setDraftValue] = useState(value);
  const rootRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  const normalizedDraftValue = draftValue.trim();
  const filteredOptions = useMemo(() => {
    const needle = normalizedDraftValue.toLowerCase();
    if (!needle) return options;
    return options.filter((option) => option.toLowerCase().includes(needle));
  }, [normalizedDraftValue, options]);
  const hasExactOption = options.includes(normalizedDraftValue);

  useEffect(() => {
    if (open) return;
    setDraftValue(value);
  }, [open, value]);

  useEffect(() => {
    if (!open) return;
    inputRef.current?.focus();
  }, [open]);

  const commitDraftValue = () => {
    onChange(normalizedDraftValue);
    setDraftValue(normalizedDraftValue);
    setOpen(false);
  };

  useEffect(() => {
    if (!open) return;

    const handlePointerDown = (event: PointerEvent) => {
      if (rootRef.current?.contains(event.target as Node)) return;
      commitDraftValue();
    };

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "Escape") return;
      setOpen(false);
      setDraftValue(value);
    };

    window.addEventListener("pointerdown", handlePointerDown);
    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("pointerdown", handlePointerDown);
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [commitDraftValue, open, value]);

  return (
    <div className="relative" ref={rootRef}>
      <div className="flex h-10 min-w-56 items-center rounded-lg border border-violet-300/22 bg-background/72 px-3 text-sm shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.06)]">
        <input
          ref={inputRef}
          value={draftValue}
          onFocus={() => {
            if (disabled) return;
            setDraftValue(value);
            setOpen(true);
          }}
          onInput={(event) => {
            setDraftValue(event.currentTarget.value);
            setOpen(true);
          }}
          onKeyDown={(event) => {
            if (event.key !== "Enter") return;
            event.preventDefault();
            commitDraftValue();
          }}
          className="h-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
          placeholder="Select or type model"
          disabled={disabled}
        />
        <button
          type="button"
          className="ml-2 shrink-0 text-muted-foreground disabled:opacity-50"
          onClick={() => {
            if (disabled) return;
            if (open) {
              commitDraftValue();
              return;
            }
            setDraftValue(value);
            setOpen(true);
          }}
          disabled={disabled}
          aria-label="Toggle model options"
        >
          <ChevronDown className="h-4 w-4" />
        </button>
      </div>
      {open ? (
        <div className="absolute right-0 z-50 mt-2 w-full rounded-lg border border-violet-300/20 bg-[hsl(238_48%_7%)] p-1 shadow-2xl shadow-black/45">
          <div className="max-h-64 overflow-y-auto">
            {normalizedDraftValue && !hasExactOption ? (
              <button
                type="button"
                className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-violet-400/12"
                onClick={commitDraftValue}
              >
                <Check className={cn("h-4 w-4", normalizedDraftValue === value ? "opacity-100" : "opacity-0")} />
                <span className="truncate">Use "{normalizedDraftValue}"</span>
              </button>
            ) : null}
            {filteredOptions.length ? (
              filteredOptions.map((option) => (
                <button
                  key={option}
                  type="button"
                  className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-violet-400/12"
                  onClick={() => {
                    onChange(option);
                    setOpen(false);
                    setDraftValue(option);
                  }}
                >
                  <Check className={cn("h-4 w-4", option === value ? "opacity-100" : "opacity-0")} />
                  <span className="truncate">{option}</span>
                </button>
              ))
            ) : !normalizedDraftValue ? (
              <p className="px-2 py-2 text-sm text-muted-foreground">No models found</p>
            ) : null}
          </div>
        </div>
      ) : null}
    </div>
  );
}

function getChatTitle(chat: Pick<ChatSummary, "title">, messages?: ChatDetail["messages"]) {
  if (chat.title?.trim()) return chat.title.trim();
  const firstUserMessage = messages?.find((message) => message.role === "user");
  const firstUserText = firstUserMessage?.content.trim();
  if (firstUserText) return firstUserText.slice(0, 48);
  const firstUserAttachments = firstUserMessage ? getMessageAttachments(firstUserMessage.metadata) : [];
  const attachmentSummary = buildAttachmentSummary(firstUserAttachments);
  if (attachmentSummary) return attachmentSummary.slice(0, 48);
  return "New chat";
}

function getSearchTitle(search: Pick<SearchSummary, "title" | "query">) {
  if (search.title?.trim()) return search.title.trim();
  if (search.query?.trim()) return search.query.trim().slice(0, 64);
  return "New search";
}

function chatWorkspaceItem(chat: ChatSummary): WorkspaceItem {
  return { type: "chat", ...chat };
}

function searchWorkspaceItem(search: SearchSummary): WorkspaceItem {
  return { type: "search", ...search };
}

function splitWorkspaceItems(items: WorkspaceItem[]) {
  const chats: ChatSummary[] = [];
  const searches: SearchSummary[] = [];
  for (const item of items) {
    if (item.type === "chat") {
      const { type: _type, ...chat } = item;
      chats.push(chat);
    } else {
      const { type: _type, ...search } = item;
      searches.push(search);
    }
  }
  return { chats, searches };
}

function buildSidebarItems(items: WorkspaceItem[]): SidebarItem[] {
  return items.map((item) => {
    if (item.type === "chat") {
      const chat = item;
      return {
        kind: "chat" as const,
        id: chat.id,
        title: getChatTitle(chat),
        parentChatId: chat.parentChatId,
        updatedAt: chat.updatedAt,
        createdAt: chat.createdAt,
        starred: chat.starred,
        starredAt: chat.starredAt,
        initiatedProvider: chat.initiatedProvider,
        initiatedModel: chat.initiatedModel,
        lastUsedProvider: chat.lastUsedProvider,
        lastUsedModel: chat.lastUsedModel,
      };
    }

    const search = item;
    return {
      kind: "search" as const,
      id: search.id,
      title: getSearchTitle(search),
      parentChatId: null,
      updatedAt: search.updatedAt,
      createdAt: search.createdAt,
      starred: search.starred,
      starredAt: search.starredAt,
      initiatedProvider: null,
      initiatedModel: null,
      lastUsedProvider: null,
      lastUsedModel: null,
    };
  });
}

function upsertWorkspaceItem(items: WorkspaceItem[], item: WorkspaceItem, moveToFront = true) {
  const withoutExisting = items.filter((existing) => existing.type !== item.type || existing.id !== item.id);
  if (moveToFront) {
    return [item, ...withoutExisting];
  }

  const existingIndex = items.findIndex((existing) => existing.type === item.type && existing.id === item.id);
  if (existingIndex < 0) return [item, ...items];
  const next = [...items];
  next[existingIndex] = item;
  return next;
}

function buildActiveRunsState(activeRuns: ActiveRunsResponse): ActiveRunsState {
  return {
    chats: Object.fromEntries(activeRuns.chats.map((id) => [id, true])),
    searches: Object.fromEntries(activeRuns.searches.map((id) => [id, true])),
  };
}

function formatDate(value: string) {
  return new Intl.DateTimeFormat(undefined, {
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  }).format(new Date(value));
}

function getSidebarSectionLabel(value: string) {
  const date = new Date(value);
  const now = new Date();
  const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
  const startOfItemDay = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
  const dayMs = 24 * 60 * 60 * 1000;
  const dayDelta = Math.floor((startOfToday - startOfItemDay) / dayMs);

  if (dayDelta <= 0) return "TODAY";
  if (dayDelta < 7) return "LAST 7 DAYS";
  return "EARLIER";
}

function buildSidebarSections(items: SidebarItem[], allItems = items) {
  const groups = groupForkedChatItems(items).map((groupItems) => {
    const presentation = getForkGroupPresentation(groupItems, allItems);
    return {
      items: groupItems,
      updatedAt: presentation?.updatedAt ?? groupItems[0].updatedAt,
      starred: presentation?.starred ?? false,
      starredAt: presentation?.starredAt ?? null,
    };
  });
  const starred = groups
    .filter((group) => group.starred)
    .sort((a, b) => new Date(b.starredAt ?? b.updatedAt).getTime() - new Date(a.starredAt ?? a.updatedAt).getTime());
  const unstarred = groups
    .filter((group) => !group.starred)
    .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());

  const sections: Array<{ label: string; items: SidebarItem[] }> = starred.length
    ? [{ label: "STARRED", items: starred.flatMap((group) => group.items) }]
    : [];
  return unstarred.reduce<Array<{ label: string; items: SidebarItem[] }>>((sections, group) => {
    const label = getSidebarSectionLabel(group.updatedAt);
    const section = sections.find((candidate) => candidate.label === label);
    if (section) {
      section.items.push(...group.items);
    } else {
      sections.push({ label, items: [...group.items] });
    }
    return sections;
  }, sections);
}

export default function App() {
  const {
    authTokenInput,
    setAuthTokenInput,
    isCheckingSession,
    isSigningIn,
    isAuthenticated,
    authMode,
    authError,
    handleAuthFailure: baseHandleAuthFailure,
    handleSignIn,
    logout,
  } = useSessionAuth();

  const [chats, setChats] = useState<ChatSummary[]>([]);
  const [searches, setSearches] = useState<SearchSummary[]>([]);
  const [workspaceItems, setWorkspaceItems] = useState<WorkspaceItem[]>([]);
  const [selectedItem, setSelectedItem] = useState<SidebarSelection | null>(null);
  const [selectedChat, setSelectedChat] = useState<ChatDetail | null>(null);
  const [selectedSearch, setSelectedSearch] = useState<SearchDetail | null>(null);
  const [draftKind, setDraftKind] = useState<DraftSelectionKind | null>(null);
  const [isLoadingCollections, setIsLoadingCollections] = useState(false);
  const [isLoadingSelection, setIsLoadingSelection] = useState(false);
  const [isStartingSearchChat, setIsStartingSearchChat] = useState(false);
  const [isForkingChat, setIsForkingChat] = useState(false);
  const [pendingChatStates, setPendingChatStates] = useState<Record<string, PendingChatState>>({});
  const [runningSearchStates, setRunningSearchStates] = useState<Record<string, SearchDetail>>({});
  const [activeRuns, setActiveRuns] = useState<ActiveRunsState>(EMPTY_ACTIVE_RUNS);
  const [composerDraftRevision, setComposerDraftRevision] = useState(0);
  const [pendingAttachments, setPendingAttachments] = useState<ChatAttachment[]>([]);
  const [isComposerDropActive, setIsComposerDropActive] = useState(false);
  const [provider, setProvider] = useState<Provider>("openai");
  const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse["providers"]>(EMPTY_MODEL_CATALOG);
  const [availableChatTools, setAvailableChatTools] = useState<ChatToolInfo[]>([]);
  const [providerModelPreferences, setProviderModelPreferences] = useState<ProviderModelPreferences>(() => loadStoredModelPreferences());
  const [model, setModel] = useState(() => {
    const stored = loadStoredModelPreferences();
    return stored.openai ?? PROVIDER_FALLBACK_MODELS.openai[0];
  });
  const [quickProvider, setQuickProvider] = useState<Provider>(() => loadStoredQuickQuestionModelSelection().provider);
  const [quickProviderModelPreferences, setQuickProviderModelPreferences] = useState<ProviderModelPreferences>(
    () => loadStoredQuickQuestionModelSelection().modelPreferences
  );
  const [quickModel, setQuickModel] = useState(() => {
    const stored = loadStoredQuickQuestionModelSelection();
    return stored.modelPreferences[stored.provider] ?? PROVIDER_FALLBACK_MODELS[stored.provider][0];
  });
  const [isQuickQuestionOpen, setIsQuickQuestionOpen] = useState(false);
  const [quickPrompt, setQuickPrompt] = useState("");
  const [quickSubmittedPrompt, setQuickSubmittedPrompt] = useState<string | null>(null);
  const [quickSubmittedModelSelection, setQuickSubmittedModelSelection] = useState<{ provider: Provider; model: string } | null>(null);
  const [quickQuestionMessages, setQuickQuestionMessages] = useState<Message[]>([]);
  const [isQuickQuestionSending, setIsQuickQuestionSending] = useState(false);
  const [isConvertingQuickQuestion, setIsConvertingQuickQuestion] = useState(false);
  const [quickQuestionError, setQuickQuestionError] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [renameChatDialog, setRenameChatDialog] = useState<RenameChatDialogState | null>(null);
  const [renameChatDraft, setRenameChatDraft] = useState("");
  const [renameChatError, setRenameChatError] = useState<string | null>(null);
  const [isRenamingChat, setIsRenamingChat] = useState(false);
  const [isChatSettingsOpen, setIsChatSettingsOpen] = useState(false);
  const [isSavingChatSettings, setIsSavingChatSettings] = useState(false);
  const [isTogglingChatSettingsStar, setIsTogglingChatSettingsStar] = useState(false);
  const [chatSettingsError, setChatSettingsError] = useState<string | null>(null);
  const [draftChatTitle, setDraftChatTitle] = useState("");
  const [chatSettingsTitleDraft, setChatSettingsTitleDraft] = useState("");
  const [chatSettingsProviderDraft, setChatSettingsProviderDraft] = useState<Provider>("openai");
  const [chatSettingsModelDraft, setChatSettingsModelDraft] = useState("");
  const [chatSettingsPromptDraft, setChatSettingsPromptDraft] = useState("");
  const [chatSettingsEnabledToolsDraft, setChatSettingsEnabledToolsDraft] = useState<string[]>([]);
  const [additionalSystemPrompt, setAdditionalSystemPrompt] = useState("");
  const [enabledTools, setEnabledTools] = useState<string[]>([]);
  const [transcriptTailSpacerHeight, setTranscriptTailSpacerHeight] = useState(TRANSCRIPT_BOTTOM_GAP);
  const transcriptContainerRef = useRef<HTMLDivElement>(null);
  const transcriptEndRef = useRef<HTMLDivElement>(null);
  const contextMenuRef = useRef<HTMLDivElement>(null);
  const renameChatInputRef = useRef<HTMLInputElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const dragDepthRef = useRef(0);
  const pendingAttachmentsRef = useRef<ChatAttachment[]>([]);
  const composerDraftRef = useRef("");
  const selectedItemRef = useRef<SidebarSelection | null>(null);
  const draftKindRef = useRef<DraftSelectionKind | null>(null);
  const collectionLoadRequestRef = useRef(0);
  const activeCollectionLoadRef = useRef<{ requestId: number; options: RefreshCollectionsOptions } | null>(null);
  const selectionLoadRequestRef = useRef(0);
  const forkRequestSequenceRef = useRef(0);
  const activeForkRequestRef = useRef<number | null>(null);
  const pendingTitleGenerationRef = useRef<Set<string>>(new Set());
  const chatStreamAbortRefs = useRef<Map<string, AbortController>>(new Map());
  const backgroundSuspendedChatStreamsRef = useRef<Set<string>>(new Set());
  const searchRunAbortRefs = useRef<Map<string, AbortController>>(new Map());
  const quickQuestionAbortRef = useRef<AbortController | null>(null);
  const searchRunCountersRef = useRef<Map<string, number>>(new Map());
  const shouldAutoScrollRef = useRef(true);
  const wasSendingRef = useRef(false);
  const suppressNextMobileComposerFocusRef = useRef(false);
  const mobileWorkspaceSwipeRef = useRef<MobileWorkspaceSwipe | null>(null);
  const pendingReplyScrollRef = useRef(false);
  const transcriptTailSpacerHeightRef = useRef(TRANSCRIPT_BOTTOM_GAP);
  const transcriptTailSpacerSettleFrameRef = useRef<number | null>(null);
  const transcriptViewKeyRef = useRef<string | null>(null);
  const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
  const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
  const [sidebarQuery, setSidebarQuery] = useState("");
  const initialRouteSelectionRef = useRef<SidebarSelection | null>(readSidebarSelectionFromUrl());
  const hasSyncedSelectionHistoryRef = useRef(false);

  const setTranscriptTailSpacer = (height: number) => {
    const nextHeight = Math.max(TRANSCRIPT_BOTTOM_GAP, Math.ceil(height));
    transcriptTailSpacerHeightRef.current = nextHeight;
    setTranscriptTailSpacerHeight(nextHeight);
  };

  const expandTranscriptTailSpacer = (height: number) => {
    const targetHeight = Math.max(TRANSCRIPT_BOTTOM_GAP, Math.ceil(height));
    setTranscriptTailSpacerHeight((currentHeight) => {
      const nextHeight = Math.max(currentHeight, targetHeight);
      transcriptTailSpacerHeightRef.current = nextHeight;
      return nextHeight;
    });
  };

  const settleTranscriptTailSpacer = () => {
    const container = transcriptContainerRef.current;
    const currentSpacerHeight = transcriptTailSpacerHeightRef.current;
    if (currentSpacerHeight <= TRANSCRIPT_BOTTOM_GAP) return;
    if (!container) {
      setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
      return;
    }

    const scrollHeightWithoutSpacer = container.scrollHeight - currentSpacerHeight;
    const requiredSpacerHeight = container.scrollTop + container.clientHeight - scrollHeightWithoutSpacer;
    setTranscriptTailSpacer(Math.min(currentSpacerHeight, requiredSpacerHeight));
  };

  const requestSettleTranscriptTailSpacer = () => {
    if (transcriptTailSpacerHeightRef.current <= TRANSCRIPT_BOTTOM_GAP) return;
    if (typeof window === "undefined") {
      settleTranscriptTailSpacer();
      return;
    }
    if (transcriptTailSpacerSettleFrameRef.current !== null) {
      window.cancelAnimationFrame(transcriptTailSpacerSettleFrameRef.current);
    }
    transcriptTailSpacerSettleFrameRef.current = window.requestAnimationFrame(() => {
      transcriptTailSpacerSettleFrameRef.current = null;
      settleTranscriptTailSpacer();
    });
  };

  const focusComposer = () => {
    if (typeof window === "undefined") return;
    window.requestAnimationFrame(() => {
      const textarea = document.getElementById("composer-input") as HTMLTextAreaElement | null;
      textarea?.focus();
    });
  };

  const replaceComposerDraft = (value: string) => {
    composerDraftRef.current = value;
    setComposerDraftRevision((current) => current + 1);
  };

  useEffect(() => {
    pendingAttachmentsRef.current = pendingAttachments;
  }, [pendingAttachments]);

  const sidebarItems = useMemo(() => buildSidebarItems(workspaceItems), [workspaceItems]);
  const filteredSidebarItems = useMemo(() => {
    const query = sidebarQuery.trim().toLowerCase();
    if (!query) return groupForkedChatItems(sidebarItems).flat();
    return filterSidebarItemsWithForkGroups(sidebarItems, (item) => {
      const providerLabel = getProviderLabel(item.lastUsedProvider || item.initiatedProvider).toLowerCase();
      return [item.title, item.initiatedModel, item.lastUsedModel, providerLabel]
        .filter(Boolean)
        .some((value) => String(value).toLowerCase().includes(query));
    });
  }, [sidebarItems, sidebarQuery]);
  const sidebarSections = useMemo(
    () => buildSidebarSections(filteredSidebarItems, sidebarItems),
    [filteredSidebarItems, sidebarItems]
  );
  const orderedSidebarItems = useMemo(() => sidebarSections.flatMap((section) => section.items), [sidebarSections]);

  const cancelCollectionLoad = () => {
    collectionLoadRequestRef.current += 1;
    activeCollectionLoadRef.current = null;
    setIsLoadingCollections(false);
  };

  const resetWorkspaceState = () => {
    cancelCollectionLoad();
    selectionLoadRequestRef.current += 1;
    forkRequestSequenceRef.current += 1;
    activeForkRequestRef.current = null;
    draftKindRef.current = null;
    setChats([]);
    setSearches([]);
    setWorkspaceItems([]);
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
    setDraftKind(null);
    setPendingChatStates({});
    setRunningSearchStates({});
    setActiveRuns(EMPTY_ACTIVE_RUNS);
    for (const controller of chatStreamAbortRefs.current.values()) {
      controller.abort();
    }
    chatStreamAbortRefs.current.clear();
    backgroundSuspendedChatStreamsRef.current.clear();
    for (const controller of searchRunAbortRefs.current.values()) {
      controller.abort();
    }
    searchRunAbortRefs.current.clear();
    searchRunCountersRef.current.clear();
    replaceComposerDraft("");
    setPendingAttachments([]);
    setIsChatSettingsOpen(false);
    setIsSavingChatSettings(false);
    setIsTogglingChatSettingsStar(false);
    setChatSettingsError(null);
    setDraftChatTitle("");
    setChatSettingsTitleDraft("");
    setChatSettingsProviderDraft("openai");
    setChatSettingsModelDraft("");
    setChatSettingsPromptDraft("");
    setChatSettingsEnabledToolsDraft([]);
    setAdditionalSystemPrompt("");
    setEnabledTools([]);
    setIsQuickQuestionOpen(false);
    setQuickPrompt("");
    setQuickSubmittedPrompt(null);
    setQuickSubmittedModelSelection(null);
    setQuickQuestionMessages([]);
    setQuickQuestionError(null);
    setContextMenu(null);
    setRenameChatDialog(null);
    setRenameChatDraft("");
    setRenameChatError(null);
    setIsRenamingChat(false);
    setIsForkingChat(false);
    setError(null);
  };

  const handleAuthFailure = (message: string) => {
    baseHandleAuthFailure(message);
    resetWorkspaceState();
  };

  const refreshCollections = async ({
    initialSelection,
    reportTransientError = true,
    selectFallback = false,
  }: RefreshCollectionsOptions = {}) => {
    const requestId = ++collectionLoadRequestRef.current;
    activeCollectionLoadRef.current = {
      requestId,
      options: { initialSelection, reportTransientError, selectFallback },
    };
    setIsLoadingCollections(true);
    try {
      const nextWorkspaceItems = await listWorkspaceItems();
      if (requestId !== collectionLoadRequestRef.current) return;
      const { chats: nextChats, searches: nextSearches } = splitWorkspaceItems(nextWorkspaceItems);
      setWorkspaceItems(nextWorkspaceItems);
      setChats(nextChats);
      setSearches(nextSearches);

      setSelectedItem((current) => {
        const nextSelection = resolveSidebarSelectionAfterRefresh(current, nextWorkspaceItems, {
          initialSelection,
          selectFallback,
        });
        selectedItemRef.current = nextSelection;
        return nextSelection;
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else if (
        requestId === collectionLoadRequestRef.current &&
        (reportTransientError || !isRecoverableStreamDisconnect(err))
      ) {
        setError(message);
      }
    } finally {
      if (requestId === collectionLoadRequestRef.current) {
        activeCollectionLoadRef.current = null;
        setIsLoadingCollections(false);
      }
    }
  };

  const supersedeCollectionLoad = () => {
    const replacementOptions = activeCollectionLoadRef.current?.options;
    cancelCollectionLoad();
    if (replacementOptions) void refreshCollections(replacementOptions);
  };

  const refreshModels = async () => {
    try {
      const data = await listModels();
      setModelCatalog(data.providers);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  const refreshChatTools = async () => {
    try {
      const tools = await listChatTools();
      setAvailableChatTools(tools);
      setEnabledTools((current) => normalizeEnabledTools(current.length ? current : null, tools));
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  const refreshActiveRuns = async () => {
    try {
      const data = await getActiveRuns();
      setActiveRuns(buildActiveRunsState(data));
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      }
    }
  };

  const refreshChat = async (chatId: string) => {
    const requestId = ++selectionLoadRequestRef.current;
    setIsLoadingSelection(true);
    try {
      const chat = await getChat(chatId);
      const current = selectedItemRef.current;
      if (requestId !== selectionLoadRequestRef.current || current?.kind !== "chat" || current.id !== chatId) return;
      setSelectedChat(chat);
      setSelectedSearch(null);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else if (requestId === selectionLoadRequestRef.current && !isRecoverableStreamDisconnect(err)) {
        setError(message);
      }
    } finally {
      if (requestId === selectionLoadRequestRef.current) setIsLoadingSelection(false);
    }
  };

  const refreshSearch = async (searchId: string) => {
    const requestId = ++selectionLoadRequestRef.current;
    setIsLoadingSelection(true);
    try {
      const search = await getSearch(searchId);
      const current = selectedItemRef.current;
      if (requestId !== selectionLoadRequestRef.current || current?.kind !== "search" || current.id !== searchId) return;
      setSelectedSearch(search);
      setSelectedChat(null);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else if (requestId === selectionLoadRequestRef.current && !isRecoverableStreamDisconnect(err)) {
        setError(message);
      }
    } finally {
      if (requestId === selectionLoadRequestRef.current) setIsLoadingSelection(false);
    }
  };

  useEffect(() => {
    if (!isAuthenticated) return;
    const initialSelection = initialRouteSelectionRef.current;
    initialRouteSelectionRef.current = null;
    void Promise.all([
      refreshCollections({ initialSelection: initialSelection ?? undefined, selectFallback: true }),
      refreshModels(),
      refreshChatTools(),
      refreshActiveRuns(),
    ]);
  }, [isAuthenticated]);

  useEffect(() => {
    if (!isAuthenticated) return;
    const interval = window.setInterval(() => {
      void refreshActiveRuns();
    }, 3000);
    return () => window.clearInterval(interval);
  }, [isAuthenticated]);

  useEffect(() => {
    if (!isAuthenticated) return;

    const suspendChatStreams = () => {
      for (const [chatId, controller] of chatStreamAbortRefs.current) {
        backgroundSuspendedChatStreamsRef.current.add(chatId);
        controller.abort();
      }
    };
    const handleVisibilityChange = () => {
      if (document.visibilityState === "hidden") {
        suspendChatStreams();
        return;
      }
      setError((current) => (current && isRecoverableStreamDisconnect(new Error(current)) ? null : current));
      void refreshActiveRuns();
      const currentSelection = selectedItemRef.current;
      void refreshCollections({ reportTransientError: false });
      if (currentSelection?.kind === "chat") {
        void refreshChat(currentSelection.id);
      } else if (currentSelection?.kind === "search") {
        void refreshSearch(currentSelection.id);
      }
    };
    const handlePageShow = () => {
      void refreshActiveRuns();
    };

    document.addEventListener("visibilitychange", handleVisibilityChange);
    window.addEventListener("pagehide", suspendChatStreams);
    window.addEventListener("pageshow", handlePageShow);
    return () => {
      document.removeEventListener("visibilitychange", handleVisibilityChange);
      window.removeEventListener("pagehide", suspendChatStreams);
      window.removeEventListener("pageshow", handlePageShow);
    };
  }, [isAuthenticated]);

  useEffect(() => {
    const onPopState = () => {
      setContextMenu(null);
      draftKindRef.current = null;
      setDraftKind(null);
      const target = readSidebarSelectionFromUrl();
      selectedItemRef.current = target;
      setSelectedItem(target);
      setIsMobileSidebarOpen(false);
    };
    window.addEventListener("popstate", onPopState);
    return () => window.removeEventListener("popstate", onPopState);
  }, []);

  useEffect(() => {
    if (!isAuthenticated) {
      hasSyncedSelectionHistoryRef.current = false;
      return;
    }
    const current = `${window.location.pathname}${window.location.search}`;
    const next = buildWorkspaceUrl(selectedItem);

    if (!hasSyncedSelectionHistoryRef.current) {
      hasSyncedSelectionHistoryRef.current = true;
      if (current !== next) {
        window.history.replaceState({}, "", next);
      }
      return;
    }

    if (current !== next) {
      window.history.pushState({}, "", next);
    }
  }, [isAuthenticated, selectedItem]);

  const providerModelOptions = useMemo(() => getModelOptions(modelCatalog, provider), [modelCatalog, provider]);
  const quickProviderModelOptions = useMemo(() => getModelOptions(modelCatalog, quickProvider), [modelCatalog, quickProvider]);
  const chatSettingsProviderModelOptions = useMemo(
    () => getModelOptions(modelCatalog, chatSettingsProviderDraft),
    [chatSettingsProviderDraft, modelCatalog]
  );
  const providerOptions = useMemo(() => getVisibleProviders(modelCatalog), [modelCatalog]);

  useEffect(() => {
    if (model.trim()) return;
    setModel((current) => {
      return current.trim() || pickProviderModel(providerModelOptions, providerModelPreferences[provider]);
    });
  }, [model, provider, providerModelOptions, providerModelPreferences]);

  useEffect(() => {
    if (typeof window === "undefined") return;
    window.localStorage.setItem(MODEL_PREFERENCES_STORAGE_KEY, JSON.stringify(providerModelPreferences));
  }, [providerModelPreferences]);

  useEffect(() => {
    if (quickModel.trim()) return;
    setQuickModel((current) => {
      return current.trim() || pickProviderModel(quickProviderModelOptions, quickProviderModelPreferences[quickProvider]);
    });
  }, [quickModel, quickProvider, quickProviderModelOptions, quickProviderModelPreferences]);

  useEffect(() => {
    if (typeof window === "undefined") return;
    window.localStorage.setItem(
      QUICK_QUESTION_MODEL_SELECTION_STORAGE_KEY,
      JSON.stringify({
        provider: quickProvider,
        modelPreferences: quickProviderModelPreferences,
      } satisfies QuickQuestionModelSelection)
    );
  }, [quickProvider, quickProviderModelPreferences]);

  useEffect(() => {
    if (!isQuickQuestionOpen || typeof window === "undefined") return;
    window.requestAnimationFrame(() => {
      const textarea = document.getElementById("quick-question-input") as HTMLTextAreaElement | null;
      if (!textarea) return;
      textarea.focus();
      textarea.style.height = "0px";
      textarea.style.height = `${textarea.scrollHeight}px`;
      if (textarea.value.length > 0) {
        textarea.select();
      }
    });
  }, [isQuickQuestionOpen]);

  useEffect(() => {
    if (typeof document === "undefined") return;
    const textarea = document.getElementById("quick-question-input") as HTMLTextAreaElement | null;
    if (!textarea) return;
    textarea.style.height = "0px";
    textarea.style.height = `${textarea.scrollHeight}px`;
  }, [quickPrompt, isQuickQuestionOpen]);

  const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null;
  const transcriptViewKey = draftKind ? `draft:${draftKind}` : selectedKey ?? "empty";
  const selectedChatPendingState =
    draftKind === null && selectedItem?.kind === "chat" ? pendingChatStates[selectedItem.id] ?? null : null;
  const selectedSearchRunState =
    draftKind === null && selectedItem?.kind === "search" ? runningSearchStates[selectedItem.id] ?? null : null;
  const selectedChatIsActive =
    draftKind === null &&
    selectedItem?.kind === "chat" &&
    (!!selectedChatPendingState || !!activeRuns.chats[selectedItem.id]);
  const selectedSearchIsActive =
    draftKind === null &&
    selectedItem?.kind === "search" &&
    (!!selectedSearchRunState || !!activeRuns.searches[selectedItem.id]);
  const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search";
  const isSearchRunning = !!selectedSearchIsActive;
  const isSendingActiveChat = draftKind !== "search" && !!selectedChatIsActive;
  const isActiveSelectionSending = isSearchMode ? isSearchRunning : isSendingActiveChat;
  const selectedSearchForView =
    selectedItem?.kind === "search"
      ? selectedSearchRunState ?? (selectedSearch?.id === selectedItem.id ? selectedSearch : null)
      : selectedSearch;

  const isItemRunning = (item: SidebarSelection) =>
    item.kind === "chat" ? !!pendingChatStates[item.id] || !!activeRuns.chats[item.id] : !!runningSearchStates[item.id] || !!activeRuns.searches[item.id];
  const getChatFamilyItems = (chatId: string) => {
    const chat = sidebarItems.find((item) => item.kind === "chat" && item.id === chatId);
    if (!chat) return [];
    const rootId = chat.parentChatId ?? chat.id;
    return sidebarItems.filter(
      (item) => item.kind === "chat" && (item.id === rootId || item.parentChatId === rootId)
    );
  };
  const isRootChat = (item: SidebarSelection) => {
    if (item.kind !== "chat") return false;
    const chat = sidebarItems.find((candidate) => candidate.kind === "chat" && candidate.id === item.id);
    return chat?.parentChatId === null;
  };
  const isDeleteBlocked = (item: SidebarSelection) => {
    if (!isRootChat(item)) return isItemRunning(item);
    return getChatFamilyItems(item.id).some((familyItem) => isItemRunning(familyItem));
  };
  const isSidebarTargetStarred = (item: SidebarSelection) => {
    if (item.kind === "chat") {
      return getChatFamilyItems(item.id).find((familyItem) => familyItem.parentChatId === null)?.starred ?? false;
    }
    return sidebarItems.find((candidate) => candidate.kind === "search" && candidate.id === item.id)?.starred ?? false;
  };
  const isCurrentSelection = (item: SidebarSelection) => {
    const current = selectedItemRef.current;
    return current?.kind === item.kind && current.id === item.id;
  };

  useEffect(() => {
    const didViewChange = transcriptViewKeyRef.current !== transcriptViewKey;
    transcriptViewKeyRef.current = transcriptViewKey;
    shouldAutoScrollRef.current = true;
    if (didViewChange && !pendingReplyScrollRef.current) {
      setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
    }
  }, [transcriptViewKey]);

  useEffect(() => {
    selectedItemRef.current = selectedItem;
  }, [selectedItem]);

  useEffect(() => {
    draftKindRef.current = draftKind;
  }, [draftKind]);

  useEffect(() => {
    if (!isAuthenticated) {
      setSelectedChat(null);
      setSelectedSearch(null);
      return;
    }
    if (!selectedItem) {
      setSelectedChat(null);
      setSelectedSearch(null);
      return;
    }

    if (selectedItem.kind === "chat") {
      void refreshChat(selectedItem.id);
      return;
    }
    void refreshSearch(selectedItem.id);
  }, [isAuthenticated, selectedKey]);

  useEffect(() => {
    if (draftKind === "search" || selectedItem?.kind === "search") return;
    const wasSending = wasSendingRef.current;
    wasSendingRef.current = isSendingActiveChat;
    if (isSendingActiveChat) return;
    if (wasSending) {
      suppressNextMobileComposerFocusRef.current = isMobileLayout();
      shouldAutoScrollRef.current = false;
      return;
    }
    if (!shouldAutoScrollRef.current) return;
    transcriptEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
  }, [draftKind, selectedChat?.messages.length, isSendingActiveChat, selectedItem?.kind, selectedKey]);

  useEffect(() => {
    if (!isSendingActiveChat || !pendingReplyScrollRef.current) return;
    pendingReplyScrollRef.current = false;
    shouldAutoScrollRef.current = true;

    window.requestAnimationFrame(() => {
      const container = transcriptContainerRef.current;
      if (!container) return;
      container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
    });
  }, [isSendingActiveChat, selectedKey]);

  useEffect(() => {
    if (isActiveSelectionSending) return;
    const hasWorkspaceSelection = Boolean(selectedItem) || draftKind !== null;
    if (!hasWorkspaceSelection) return;
    const mobileLayout = isMobileLayout();
    if (mobileLayout && (Boolean(selectedItem) || suppressNextMobileComposerFocusRef.current)) {
      suppressNextMobileComposerFocusRef.current = false;
      return;
    }
    suppressNextMobileComposerFocusRef.current = false;
    focusComposer();
  }, [draftKind, isActiveSelectionSending, selectedKey]);

  useEffect(() => {
    return () => {
      if (transcriptTailSpacerSettleFrameRef.current !== null) {
        window.cancelAnimationFrame(transcriptTailSpacerSettleFrameRef.current);
        transcriptTailSpacerSettleFrameRef.current = null;
      }
      for (const controller of chatStreamAbortRefs.current.values()) {
        controller.abort();
      }
      chatStreamAbortRefs.current.clear();
      for (const controller of searchRunAbortRefs.current.values()) {
        controller.abort();
      }
      searchRunAbortRefs.current.clear();
      quickQuestionAbortRef.current?.abort();
      quickQuestionAbortRef.current = null;
    };
  }, []);

  const messages =
    draftKind === null && selectedItem?.kind === "chat" && selectedChat?.id === selectedItem.id
      ? selectedChat.messages
      : [];

  useEffect(() => {
    if (isSearchMode && pendingAttachments.length) {
      setPendingAttachments([]);
    }
    if (isSearchMode) {
      dragDepthRef.current = 0;
      setIsComposerDropActive(false);
    }
  }, [isSearchMode, pendingAttachments.length]);
  const displayMessages = useMemo(() => {
    if (selectedChatPendingState) return selectedChatPendingState.messages.filter(isDisplayableMessage);
    return messages.filter(isDisplayableMessage);
  }, [messages, selectedChatPendingState]);
  const displayMessagesLayoutKey = useMemo(
    () => displayMessages.map((message) => `${message.id}:${message.content.length}`).join("|"),
    [displayMessages]
  );

  useEffect(() => {
    if (isSearchMode || isSendingActiveChat) return;
    requestSettleTranscriptTailSpacer();
  }, [displayMessagesLayoutKey, isSearchMode, isSendingActiveChat, selectedKey]);

  const quickAnswerText = useMemo(() => {
    for (let index = quickQuestionMessages.length - 1; index >= 0; index -= 1) {
      const message = quickQuestionMessages[index];
      if (message.role === "assistant") return message.content;
    }
    return "";
  }, [quickQuestionMessages]);
  const canConvertQuickQuestion =
    Boolean(quickSubmittedPrompt?.trim()) &&
    Boolean(quickSubmittedModelSelection?.model.trim()) &&
    Boolean(quickAnswerText.trim()) &&
    !isQuickQuestionSending;

  const selectedChatSummary = useMemo(() => {
    if (!selectedItem || selectedItem.kind !== "chat") return null;
    return chats.find((chat) => chat.id === selectedItem.id) ?? null;
  }, [chats, selectedItem]);

  const selectedSearchSummary = useMemo(() => {
    if (!selectedItem || selectedItem.kind !== "search") return null;
    return searches.find((search) => search.id === selectedItem.id) ?? null;
  }, [searches, selectedItem]);

  const selectedChatModelSelection = useMemo(() => {
    if (draftKind || selectedItem?.kind !== "chat") return null;
    const detailSelection = selectedChat?.id === selectedItem.id ? getChatModelSelection(selectedChat) : null;
    const summarySelection = getChatModelSelection(selectedChatSummary);
    return detailSelection ?? summarySelection;
  }, [
    draftKind,
    selectedChat?.id,
    selectedChat?.lastUsedModel,
    selectedChat?.lastUsedProvider,
    selectedChatSummary?.id,
    selectedChatSummary?.lastUsedModel,
    selectedChatSummary?.lastUsedProvider,
    selectedItem?.id,
    selectedItem?.kind,
  ]);
  const selectedChatModelSelectionSyncKey = getChatModelSelectionSyncKey(
    selectedItem?.kind === "chat" ? selectedItem.id : null,
    selectedChatModelSelection
  );

  useEffect(() => {
    if (!selectedChatModelSelection) return;
    setProvider(selectedChatModelSelection.provider);
    setModel(selectedChatModelSelection.model);
  }, [selectedChatModelSelectionSyncKey]);

  useEffect(() => {
    if (draftKind === "chat") return;
    if (selectedItem?.kind !== "chat") return;
    const chat = selectedChat?.id === selectedItem.id ? selectedChat : selectedChatSummary;
    if (!chat) return;
    setAdditionalSystemPrompt(chat.additionalSystemPrompt ?? "");
    setEnabledTools(normalizeEnabledTools(chat.enabledTools, availableChatTools));
  }, [availableChatTools, draftKind, selectedChat, selectedChatSummary, selectedItem]);

  const selectedTitle = useMemo(() => {
    if (draftKind === "chat") return draftChatTitle.trim() || "New chat";
    if (draftKind === "search") return "New search";
    if (!selectedItem) return "Sybil";
    if (selectedItem.kind === "chat") {
      if (selectedChat?.id === selectedItem.id) return getChatTitle(selectedChat, selectedChat.messages);
      if (selectedChatSummary) return getChatTitle(selectedChatSummary);
      return "New chat";
    }
    if (selectedSearchForView) return getSearchTitle(selectedSearchForView);
    if (selectedSearchSummary) return getSearchTitle(selectedSearchSummary);
    return "New search";
  }, [draftChatTitle, draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearchForView, selectedSearchSummary]);

  const pageTitle = useMemo(() => {
    if (draftKind || !selectedItem) return "Sybil";
    if (selectedItem.kind === "chat") {
      if (selectedChat?.id === selectedItem.id) return `${getChatTitle(selectedChat, selectedChat.messages)} — Sybil`;
      if (selectedChatSummary) return `${getChatTitle(selectedChatSummary)} — Sybil`;
      return "Sybil";
    }
    const searchQuery = selectedSearchForView?.query?.trim() || selectedSearchSummary?.query?.trim();
    if (searchQuery) return `${searchQuery} — Sybil`;
    if (selectedSearchForView) return `${getSearchTitle(selectedSearchForView)} — Sybil`;
    if (selectedSearchSummary) return `${getSearchTitle(selectedSearchSummary)} — Sybil`;
    return "Sybil";
  }, [draftKind, selectedChat, selectedChatSummary, selectedItem, selectedSearchForView, selectedSearchSummary]);
  const primaryShortcutModifier = useMemo(() => {
    if (typeof navigator === "undefined") return "Ctrl";
    return /Mac|iPhone|iPad|iPod/i.test(navigator.platform) ? "Cmd" : "Ctrl";
  }, []);

  useEffect(() => {
    document.title = pageTitle;
  }, [pageTitle]);

  const handleCreateChat = () => {
    setError(null);
    setContextMenu(null);
    draftKindRef.current = "chat";
    setDraftKind("chat");
    selectedItemRef.current = null;
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
    setPendingAttachments([]);
    setDraftChatTitle("");
    setAdditionalSystemPrompt("");
    setEnabledTools(getDefaultEnabledTools(availableChatTools));
    setIsChatSettingsOpen(false);
    setChatSettingsError(null);
    setIsMobileSidebarOpen(false);
  };

  const resetMobileWorkspaceSwipe = () => {
    mobileWorkspaceSwipeRef.current = null;
  };

  const handleMobileWorkspaceTouchStart = (event: TargetedTouchEvent<HTMLElement>) => {
    if (
      event.touches.length !== 1 ||
      !isMobileLayout() ||
      isMobileSidebarOpen ||
      isQuickQuestionOpen ||
      isChatSettingsOpen ||
      renameChatDialog !== null
    ) {
      return;
    }

    const target = event.target;
    if (
      target instanceof Element &&
      target.closest("input, textarea, select, button, a, [role='dialog'], [data-mobile-swipe-ignore]")
    ) {
      return;
    }

    const touch = event.touches.item(0);
    if (!touch) return;
    mobileWorkspaceSwipeRef.current = {
      touchIdentifier: touch.identifier,
      startX: touch.clientX,
      startY: touch.clientY,
      lastX: touch.clientX,
      lastTimestamp: event.timeStamp,
      velocityX: 0,
      direction: null,
    };
  };

  const handleMobileWorkspaceTouchMove = (event: TargetedTouchEvent<HTMLElement>) => {
    const swipe = mobileWorkspaceSwipeRef.current;
    if (!swipe) return;
    const touch = findTouch(event.touches, swipe.touchIdentifier);
    if (!touch) return;

    const deltaX = touch.clientX - swipe.startX;
    const deltaY = touch.clientY - swipe.startY;
    const horizontalTravel = Math.abs(deltaX);
    const verticalTravel = Math.abs(deltaY);

    if (swipe.direction === null) {
      if (
        verticalTravel >= MOBILE_SWIPE_ACTIVATION_DISTANCE &&
        verticalTravel > horizontalTravel * MOBILE_SWIPE_DIRECTION_DOMINANCE
      ) {
        resetMobileWorkspaceSwipe();
        return;
      }
      if (
        horizontalTravel < MOBILE_SWIPE_ACTIVATION_DISTANCE ||
        horizontalTravel < verticalTravel * MOBILE_SWIPE_DIRECTION_DOMINANCE
      ) {
        return;
      }
      swipe.direction = deltaX > 0 ? 1 : -1;
    }

    const elapsedMs = event.timeStamp - swipe.lastTimestamp;
    if (elapsedMs > 0 && touch.clientX !== swipe.lastX) {
      swipe.velocityX = ((touch.clientX - swipe.lastX) / elapsedMs) * 1000;
    }
    swipe.lastX = touch.clientX;
    swipe.lastTimestamp = event.timeStamp;
    if (event.cancelable) event.preventDefault();
  };

  const handleMobileWorkspaceTouchEnd = (event: TargetedTouchEvent<HTMLElement>) => {
    const swipe = mobileWorkspaceSwipeRef.current;
    if (!swipe) return;
    const touch = findTouch(event.changedTouches, swipe.touchIdentifier);
    if (!touch) return;

    const direction = swipe.direction;
    const deltaX = touch.clientX - swipe.startX;
    const elapsedMs = event.timeStamp - swipe.lastTimestamp;
    if (elapsedMs > 0 && touch.clientX !== swipe.lastX) {
      swipe.velocityX = ((touch.clientX - swipe.lastX) / elapsedMs) * 1000;
    }
    resetMobileWorkspaceSwipe();
    if (direction === null) return;

    const directionalDistance = deltaX * direction;
    const directionalVelocity = swipe.velocityX * direction;
    const projectedDistance =
      directionalDistance + directionalVelocity * MOBILE_SWIPE_VELOCITY_PROJECTION_SECONDS;
    const shouldComplete =
      directionalVelocity <= -MOBILE_SWIPE_COMPLETION_VELOCITY
        ? false
        : directionalVelocity >= MOBILE_SWIPE_COMPLETION_VELOCITY ||
          directionalDistance >= getMobileSwipeLatchDistance() ||
          projectedDistance >= getMobileSwipeLatchDistance();
    if (!shouldComplete) return;

    if (direction > 0) {
      setIsMobileSidebarOpen(true);
    } else {
      handleCreateChat();
    }
  };

  const handleMobileWorkspaceTouchCancel = (event: TargetedTouchEvent<HTMLElement>) => {
    const swipe = mobileWorkspaceSwipeRef.current;
    if (!swipe || !findTouch(event.changedTouches, swipe.touchIdentifier)) return;
    resetMobileWorkspaceSwipe();
  };

  const handleOpenQuickQuestion = () => {
    setQuickQuestionError(null);
    setIsQuickQuestionOpen(true);
    setIsMobileSidebarOpen(false);
  };

  const handleCreateSearch = () => {
    setError(null);
    setContextMenu(null);
    draftKindRef.current = "search";
    setDraftKind("search");
    selectedItemRef.current = null;
    setSelectedItem(null);
    setSelectedChat(null);
    setSelectedSearch(null);
    setPendingAttachments([]);
    setIsChatSettingsOpen(false);
    setChatSettingsError(null);
    setIsMobileSidebarOpen(false);
  };

  const selectAdjacentSidebarItem = (direction: -1 | 1) => {
    if (!orderedSidebarItems.length) return;

    setError(null);
    setContextMenu(null);
    draftKindRef.current = null;
    setDraftKind(null);
    setIsMobileSidebarOpen(false);
    const current = selectedItemRef.current;
    const currentIndex = current
      ? orderedSidebarItems.findIndex((item) => item.kind === current.kind && item.id === current.id)
      : -1;
    const fallbackIndex = direction > 0 ? 0 : orderedSidebarItems.length - 1;
    const nextIndex =
      currentIndex < 0
        ? fallbackIndex
        : Math.min(orderedSidebarItems.length - 1, Math.max(0, currentIndex + direction));
    const nextItem = orderedSidebarItems[nextIndex];
    const target: SidebarSelection = { kind: nextItem.kind, id: nextItem.id };
    selectedItemRef.current = target;
    setSelectedItem(target);
  };

  useEffect(() => {
    if (!isAuthenticated) return;

    const handleKeyDown = (event: KeyboardEvent) => {
      const hasPrimaryModifier = event.metaKey || event.ctrlKey;
      if (!hasPrimaryModifier || event.altKey) return;

      const key = event.key.toLowerCase();
      if (key === "i" && !event.shiftKey) {
        event.preventDefault();
        setQuickQuestionError(null);
        setIsQuickQuestionOpen((current) => !current);
        return;
      }

      if (isQuickQuestionOpen) return;

      if (key === "j") {
        event.preventDefault();
        if (event.shiftKey) {
          handleCreateSearch();
        } else {
          handleCreateChat();
        }
        focusComposer();
        return;
      }

      if (event.key === "ArrowUp" || event.key === "ArrowDown") {
        event.preventDefault();
        selectAdjacentSidebarItem(event.key === "ArrowUp" ? -1 : 1);
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [isAuthenticated, isQuickQuestionOpen, orderedSidebarItems]);

  const getRenameSeedTitle = (chatId: string) => {
    if (selectedChat?.id === chatId) return getChatTitle(selectedChat, selectedChat.messages);
    const summary = chats.find((chat) => chat.id === chatId);
    if (summary) return getChatTitle(summary);
    const sidebarItem = sidebarItems.find((item) => item.kind === "chat" && item.id === chatId);
    return sidebarItem?.title ?? "New chat";
  };

  const applyChatSummary = (updatedChat: ChatSummary, moveToFront = true) => {
    supersedeCollectionLoad();
    setChats((current) => {
      const withoutExisting = current.filter((chat) => chat.id !== updatedChat.id);
      if (moveToFront) return [updatedChat, ...withoutExisting];
      const existingIndex = current.findIndex((chat) => chat.id === updatedChat.id);
      if (existingIndex < 0) return [updatedChat, ...current];
      const next = [...current];
      next[existingIndex] = updatedChat;
      return next;
    });
    setWorkspaceItems((current) => upsertWorkspaceItem(current, chatWorkspaceItem(updatedChat), moveToFront));
    setSelectedChat((current) => {
      if (!current || current.id !== updatedChat.id) return current;
      return {
        ...current,
        title: updatedChat.title,
        parentChatId: updatedChat.parentChatId,
        titleGenerationPending: updatedChat.titleGenerationPending,
        updatedAt: updatedChat.updatedAt,
        starred: updatedChat.starred,
        starredAt: updatedChat.starredAt,
        initiatedProvider: updatedChat.initiatedProvider,
        initiatedModel: updatedChat.initiatedModel,
        lastUsedProvider: updatedChat.lastUsedProvider,
        lastUsedModel: updatedChat.lastUsedModel,
        additionalSystemPrompt: updatedChat.additionalSystemPrompt,
        enabledTools: updatedChat.enabledTools,
      };
    });
  };

  const applySearchSummary = (updatedSearch: SearchSummary, moveToFront = true) => {
    supersedeCollectionLoad();
    setSearches((current) => {
      const withoutExisting = current.filter((search) => search.id !== updatedSearch.id);
      if (moveToFront) return [updatedSearch, ...withoutExisting];
      const existingIndex = current.findIndex((search) => search.id === updatedSearch.id);
      if (existingIndex < 0) return [updatedSearch, ...current];
      const next = [...current];
      next[existingIndex] = updatedSearch;
      return next;
    });
    setWorkspaceItems((current) => upsertWorkspaceItem(current, searchWorkspaceItem(updatedSearch), moveToFront));
    setSelectedSearch((current) => {
      if (!current || current.id !== updatedSearch.id) return current;
      return {
        ...current,
        title: updatedSearch.title,
        query: updatedSearch.query,
        updatedAt: updatedSearch.updatedAt,
        starred: updatedSearch.starred,
        starredAt: updatedSearch.starredAt,
      };
    });
  };

  const openRenameChatDialog = (chatId: string) => {
    setContextMenu(null);
    setRenameChatDraft(getRenameSeedTitle(chatId));
    setRenameChatError(null);
    setRenameChatDialog({ chatId });
  };

  const getChatSettingsSeedTitle = () => {
    if (draftKind === "chat") return draftChatTitle;
    if (selectedItem?.kind === "chat") {
      if (selectedChat?.id === selectedItem.id) return getChatTitle(selectedChat, selectedChat.messages);
      if (selectedChatSummary) return getChatTitle(selectedChatSummary);
    }
    return draftChatTitle;
  };

  const openChatSettings = () => {
    if (isSearchMode) return;
    setContextMenu(null);
    setRenameChatDialog(null);
    setChatSettingsError(null);
    setChatSettingsTitleDraft(getChatSettingsSeedTitle());
    setChatSettingsProviderDraft(provider);
    setChatSettingsModelDraft(model);
    setChatSettingsPromptDraft(additionalSystemPrompt);
    setChatSettingsEnabledToolsDraft(normalizeEnabledTools(enabledTools, availableChatTools));
    setIsChatSettingsOpen(true);
  };

  const toggleChatSettingsTool = (toolName: string) => {
    setChatSettingsEnabledToolsDraft((current) => {
      if (current.includes(toolName)) return current.filter((name) => name !== toolName);
      return current.concat(toolName);
    });
  };

  const commitLocalChatSettings = (nextProvider: Provider, nextModel: string, nextPrompt: string, nextTools: string[], nextTitle: string) => {
    setProvider(nextProvider);
    setModel(nextModel);
    setProviderModelPreferences((current) => ({
      ...current,
      [nextProvider]: nextModel || null,
    }));
    setAdditionalSystemPrompt(nextPrompt);
    setEnabledTools(nextTools);
    setDraftChatTitle(nextTitle);
  };

  const handleChatSettingsSubmit = async (event?: Event) => {
    event?.preventDefault();
    if (isSavingChatSettings) return;

    const nextModel = chatSettingsModelDraft.trim();
    if (!nextModel) {
      setChatSettingsError("Enter a model.");
      return;
    }

    const existingChatId = draftKind === null && selectedItem?.kind === "chat" ? selectedItem.id : null;
    const isExistingChat = existingChatId !== null;
    const nextTitle = chatSettingsTitleDraft.trim();
    if (isExistingChat && !nextTitle) {
      setChatSettingsError("Enter a chat title.");
      return;
    }

    const nextPrompt = chatSettingsPromptDraft.trim();
    const nextTools = availableChatTools.length
      ? normalizeEnabledTools(chatSettingsEnabledToolsDraft, availableChatTools)
      : chatSettingsEnabledToolsDraft;

    setIsSavingChatSettings(true);
    setChatSettingsError(null);
    setError(null);
    try {
      if (isExistingChat) {
        const updatedChat = await updateChatSettings(existingChatId, {
          title: nextTitle,
          additionalSystemPrompt: nextPrompt || null,
          ...(availableChatTools.length ? { enabledTools: nextTools } : {}),
        });
        applyChatSummary(updatedChat);
      } else if (!selectedItem && draftKind !== "chat") {
        draftKindRef.current = "chat";
        setDraftKind("chat");
      }

      commitLocalChatSettings(chatSettingsProviderDraft, nextModel, nextPrompt, nextTools, nextTitle);
      setIsChatSettingsOpen(false);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setChatSettingsError(message);
      }
    } finally {
      setIsSavingChatSettings(false);
    }
  };

  const openContextMenu = (event: MouseEvent, item: SidebarSelection) => {
    event.preventDefault();
    const menuWidth = 176;
    const menuHeight = item.kind === "chat" ? 152 : 80;
    const padding = 8;
    const x = Math.min(event.clientX, window.innerWidth - menuWidth - padding);
    const y = Math.min(event.clientY, window.innerHeight - menuHeight - padding);
    setContextMenu({ kind: "sidebar", item, x: Math.max(padding, x), y: Math.max(padding, y) });
  };

  const openMessageContextMenu = (event: MouseEvent, messageId: string) => {
    if (selectedItem?.kind !== "chat" || messageId.startsWith("temp-")) return;
    event.preventDefault();
    const menuWidth = 176;
    const menuHeight = 48;
    const padding = 8;
    const x = Math.min(event.clientX, window.innerWidth - menuWidth - padding);
    const y = Math.min(event.clientY, window.innerHeight - menuHeight - padding);
    setContextMenu({
      kind: "message",
      chatId: selectedItem.id,
      messageId,
      x: Math.max(padding, x),
      y: Math.max(padding, y),
    });
  };

  const handleRenameChatSubmit = async (event?: Event) => {
    event?.preventDefault();
    if (!renameChatDialog || isRenamingChat) return;

    const title = renameChatDraft.trim();
    if (!title) {
      setRenameChatError("Enter a chat title.");
      return;
    }

    setIsRenamingChat(true);
    setRenameChatError(null);
    setError(null);
    try {
      const updatedChat = await updateChatTitle(renameChatDialog.chatId, title);
      applyChatSummary(updatedChat);
      setRenameChatDialog(null);
      setRenameChatDraft("");
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setRenameChatError(message);
      }
    } finally {
      setIsRenamingChat(false);
    }
  };

  const toggleChatFamilyStar = async (chatId: string) => {
    const family = getChatFamilyItems(chatId);
    const root = family.find((item) => item.parentChatId === null) ?? family[0];
    if (!root) return;
    applyChatSummary(await updateChatStar(root.id, !root.starred), false);
  };

  const handleToggleStar = async (target: SidebarSelection) => {
    setContextMenu(null);
    setError(null);

    try {
      if (target.kind === "chat") {
        await toggleChatFamilyStar(target.id);
      } else {
        const current = sidebarItems.find((item) => item.kind === "search" && item.id === target.id);
        const updatedSearch = await updateSearchStar(target.id, !current?.starred);
        applySearchSummary(updatedSearch, false);
      }
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  const handleForkChat = async (chatId: string, messageId?: string) => {
    if (activeForkRequestRef.current !== null) return;
    const requestId = ++forkRequestSequenceRef.current;
    const selectionAtStart = selectedItemRef.current;
    const draftKindAtStart = draftKindRef.current;
    activeForkRequestRef.current = requestId;
    setIsForkingChat(true);
    setContextMenu(null);
    setError(null);
    try {
      const forkedChat = await forkChat(chatId, messageId);
      if (activeForkRequestRef.current !== requestId) return;
      const target: SidebarSelection = { kind: "chat", id: forkedChat.id };
      applyChatSummary(forkedChat);
      const currentSelection = selectedItemRef.current;
      const selectionUnchanged = selectionAtStart
        ? currentSelection?.kind === selectionAtStart.kind && currentSelection.id === selectionAtStart.id
        : currentSelection === null;
      if (!selectionUnchanged || draftKindRef.current !== draftKindAtStart) return;

      draftKindRef.current = null;
      setDraftKind(null);
      selectedItemRef.current = target;
      setSelectedItem(target);
      setSelectedChat(null);
      setSelectedSearch(null);
      setIsMobileSidebarOpen(false);
    } catch (err) {
      if (activeForkRequestRef.current !== requestId) return;
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      if (activeForkRequestRef.current === requestId) {
        activeForkRequestRef.current = null;
        setIsForkingChat(false);
      }
    }
  };

  const handleToggleChatSettingsStar = async () => {
    if (draftKind !== null || selectedItem?.kind !== "chat" || isTogglingChatSettingsStar) return;
    setIsTogglingChatSettingsStar(true);
    setChatSettingsError(null);
    setError(null);

    try {
      await toggleChatFamilyStar(selectedItem.id);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setChatSettingsError(message);
      }
    } finally {
      setIsTogglingChatSettingsStar(false);
    }
  };

  const handleDeleteFromContextMenu = async () => {
    if (!contextMenu || contextMenu.kind !== "sidebar" || isDeleteBlocked(contextMenu.item)) return;
    const target = contextMenu.item;
    setContextMenu(null);
    if (isRootChat(target)) {
      const confirmed = window.confirm("Delete this chat and all of its forks? This cannot be undone.");
      if (!confirmed) return;
    }
    setError(null);
    try {
      if (target.kind === "chat") {
        await deleteChat(target.id);
      } else {
        await deleteSearch(target.id);
      }
      await refreshCollections({ selectFallback: true });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    }
  };

  useEffect(() => {
    if (!contextMenu) return;
    const handlePointerDown = (event: PointerEvent) => {
      if (contextMenuRef.current?.contains(event.target as Node)) return;
      setContextMenu(null);
    };
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") setContextMenu(null);
    };
    window.addEventListener("pointerdown", handlePointerDown);
    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("pointerdown", handlePointerDown);
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [contextMenu]);

  useEffect(() => {
    if (!renameChatDialog) return;
    const timer = window.setTimeout(() => {
      renameChatInputRef.current?.focus();
      renameChatInputRef.current?.select();
    }, 0);
    return () => window.clearTimeout(timer);
  }, [renameChatDialog]);

  useEffect(() => {
    if (!isChatSettingsOpen) return;
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "Escape" || isSavingChatSettings) return;
      event.preventDefault();
      setIsChatSettingsOpen(false);
    };
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [isChatSettingsOpen, isSavingChatSettings]);

  useEffect(() => {
    if (!isQuickQuestionOpen) return;
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "Escape") return;
      event.preventDefault();
      setIsQuickQuestionOpen(false);
    };
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [isQuickQuestionOpen]);

  const handleOpenAttachmentPicker = () => {
    fileInputRef.current?.click();
  };

  const handleRemovePendingAttachment = (attachmentId: string) => {
    setPendingAttachments((current) => current.filter((attachment) => attachment.id !== attachmentId));
  };

  const appendPendingAttachments = async (files: File[]) => {
    if (!files.length) return;
    if (isSearchMode) {
      setError("Attachments are only available in chat mode.");
      return;
    }

    setError(null);

    try {
      const attachments = await Promise.all(files.map((file) => buildChatAttachment(file)));
      if (pendingAttachmentsRef.current.length + attachments.length > MAX_CHAT_ATTACHMENTS) {
        throw new Error(`You can attach up to ${MAX_CHAT_ATTACHMENTS} files per message.`);
      }
      setPendingAttachments((current) => current.concat(attachments));
      focusComposer();
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      setError(message);
    }
  };

  const handleFileSelection = async (event: Event) => {
    const input = event.currentTarget as HTMLInputElement;
    const files = Array.from(input.files ?? []);
    input.value = "";
    await appendPendingAttachments(files);
  };

  const handleComposerPaste = async (event: ClipboardEvent) => {
    const files = getFilesFromDataTransfer(event.clipboardData);
    if (!files.length) return;
    event.preventDefault();
    await appendPendingAttachments(files);
  };

  const handleComposerDragEnter = (event: DragEvent) => {
    if (!hasFileTransfer(event.dataTransfer)) return;
    event.preventDefault();
    if (isSearchMode) return;
    dragDepthRef.current += 1;
    setIsComposerDropActive(true);
  };

  const handleComposerDragOver = (event: DragEvent) => {
    if (!hasFileTransfer(event.dataTransfer)) return;
    event.preventDefault();
    if (event.dataTransfer) {
      event.dataTransfer.dropEffect = isSearchMode ? "none" : "copy";
    }
    if (!isSearchMode) {
      setIsComposerDropActive(true);
    }
  };

  const handleComposerDragLeave = (event: DragEvent) => {
    if (!hasFileTransfer(event.dataTransfer)) return;
    event.preventDefault();
    if (isSearchMode) return;
    dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
    if (dragDepthRef.current === 0) {
      setIsComposerDropActive(false);
    }
  };

  const handleComposerDrop = async (event: DragEvent) => {
    if (!hasFileTransfer(event.dataTransfer)) return;
    event.preventDefault();
    dragDepthRef.current = 0;
    setIsComposerDropActive(false);
    await appendPendingAttachments(getFilesFromDataTransfer(event.dataTransfer));
  };

  const removePendingChatState = (chatId: string) => {
    setPendingChatStates((current) => {
      if (!current[chatId]) return current;
      const next = { ...current };
      delete next[chatId];
      return next;
    });
  };

  const removeActiveRun = (kind: SidebarSelection["kind"], id: string) => {
    setActiveRuns((current) => {
      const bucket = current[kind === "chat" ? "chats" : "searches"];
      if (!bucket[id]) return current;
      const nextBucket = { ...bucket };
      delete nextBucket[id];
      return kind === "chat" ? { ...current, chats: nextBucket } : { ...current, searches: nextBucket };
    });
  };

  const addActiveRun = (kind: SidebarSelection["kind"], id: string) => {
    setActiveRuns((current) => {
      const bucket = current[kind === "chat" ? "chats" : "searches"];
      if (bucket[id]) return current;
      return kind === "chat"
        ? { ...current, chats: { ...current.chats, [id]: true } }
        : { ...current, searches: { ...current.searches, [id]: true } };
    });
  };

  const removeRunningSearchState = (searchId: string) => {
    setRunningSearchStates((current) => {
      if (!current[searchId]) return current;
      const next = { ...current };
      delete next[searchId];
      return next;
    });
  };

  const handleSendChat = async (content: string, attachments: ChatAttachment[]): Promise<SidebarSelection> => {
    const selectedModel = model.trim();
    if (!selectedModel) {
      throw new Error("No model available for selected provider");
    }

    pendingReplyScrollRef.current = true;
    expandTranscriptTailSpacer(getReplyScrollBufferHeight());

    const optimisticUserMessage: Message = {
      id: `temp-user-${Date.now()}`,
      createdAt: new Date().toISOString(),
      role: "user",
      content,
      name: null,
      metadata: attachments.length ? { attachments } : null,
    };

    const optimisticAssistantMessage: Message = {
      id: `temp-assistant-${Date.now()}`,
      createdAt: new Date().toISOString(),
      role: "assistant",
      content: "",
      name: null,
      metadata: null,
    };

    let chatId = draftKind === "chat" ? null : selectedItem?.kind === "chat" ? selectedItem.id : null;

    if (!chatId) {
      const initialEnabledTools = availableChatTools.length ? normalizeEnabledTools(enabledTools, availableChatTools) : undefined;
      const chat = await createChat({
        ...(draftChatTitle.trim() ? { title: draftChatTitle.trim() } : {}),
        provider,
        model: selectedModel,
        ...(additionalSystemPrompt.trim() ? { additionalSystemPrompt: additionalSystemPrompt.trim() } : {}),
        ...(initialEnabledTools !== undefined ? { enabledTools: initialEnabledTools } : {}),
      });
      chatId = chat.id;
      supersedeCollectionLoad();
      draftKindRef.current = null;
      setDraftKind(null);
      setDraftChatTitle("");
      setChats((current) => {
        const withoutExisting = current.filter((existing) => existing.id !== chat.id);
        return [chat, ...withoutExisting];
      });
      setWorkspaceItems((current) => upsertWorkspaceItem(current, chatWorkspaceItem(chat)));
      const createdChatSelection: SidebarSelection = { kind: "chat", id: chatId };
      selectedItemRef.current = createdChatSelection;
      setSelectedItem(createdChatSelection);
      setSelectedChat({
        id: chat.id,
        title: chat.title,
        parentChatId: chat.parentChatId,
        titleGenerationPending: chat.titleGenerationPending,
        createdAt: chat.createdAt,
        updatedAt: chat.updatedAt,
        starred: chat.starred,
        starredAt: chat.starredAt,
        initiatedProvider: chat.initiatedProvider,
        initiatedModel: chat.initiatedModel,
        lastUsedProvider: chat.lastUsedProvider,
        lastUsedModel: chat.lastUsedModel,
        additionalSystemPrompt: chat.additionalSystemPrompt,
        enabledTools: chat.enabledTools,
        messages: [],
      });
      setSelectedSearch(null);
    }

    if (!chatId) {
      throw new Error("Unable to initialize chat");
    }

    if (pendingChatStates[chatId]) {
      throw new Error("This chat is already generating a response.");
    }

    let baseChat = selectedChat;
    if (!baseChat || baseChat.id !== chatId) {
      baseChat = await getChat(chatId);
    }
    const previousMessageIds = new Set(baseChat.messages.map((message) => message.id));

    setPendingChatStates((current) => ({
      ...current,
      [chatId]: {
        messages: baseChat.messages.concat(optimisticUserMessage, optimisticAssistantMessage),
      },
    }));
    addActiveRun("chat", chatId);

    const requestMessages: CompletionRequestMessage[] = [
      ...baseChat.messages
        .filter((message) => !isToolCallLogMessage(message))
        .map((message) => ({
          role: message.role,
          content: message.content,
          ...(message.name ? { name: message.name } : {}),
          ...(getMessageAttachments(message.metadata).length ? { attachments: getMessageAttachments(message.metadata) } : {}),
        })),
      {
        role: "user",
        content,
        ...(attachments.length ? { attachments } : {}),
      },
    ];

    if (shouldRequestChatTitle(baseChat, pendingTitleGenerationRef.current.has(chatId))) {
      pendingTitleGenerationRef.current.add(chatId);
      const titleSeed = content || buildAttachmentSummary(attachments) || "Uploaded files";
      void suggestChatTitle({ chatId, content: titleSeed })
        .then((updatedChat) => {
          applyChatSummary(updatedChat, false);
        })
        .catch(() => {
          // ignore title suggestion errors so chat flow is not interrupted
        })
        .finally(() => {
          pendingTitleGenerationRef.current.delete(chatId);
        });
    }

    const target: SidebarSelection = { kind: "chat", id: chatId };
    const clientRequestId = createClientRequestId();

    while (true) {
      let streamErrorMessage: string | null = null;
      let replayedAssistantText = "";
      const abortController = new AbortController();
      chatStreamAbortRefs.current.set(chatId, abortController);

      try {
        await runCompletionStream(
          {
            chatId,
            clientRequestId,
            provider,
            model: selectedModel,
            messages: requestMessages,
          },
          {
            onMeta: (payload) => {
              if (payload.chatId !== chatId) return;
            },
            onToolCall: (payload) => {
              setPendingChatStates((current) => {
                const pendingState = current[chatId];
                if (!pendingState) return current;
                return {
                  ...current,
                  [chatId]: {
                    messages: upsertOptimisticToolMessage(pendingState.messages, payload, "temp-assistant-"),
                  },
                };
              });
            },
            onDelta: (payload) => {
              if (!payload.text) return;
              replayedAssistantText += payload.text;
              setPendingChatStates((current) => {
                const pendingState = current[chatId];
                if (!pendingState) return current;
                let updated = false;
                const nextMessages = pendingState.messages.map((message, index, all) => {
                  const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-");
                  if (!isTarget) return message;
                  updated = true;
                  return { ...message, content: replayedAssistantText };
                });
                return updated ? { ...current, [chatId]: { messages: nextMessages } } : current;
              });
            },
            onDone: (payload) => {
              setPendingChatStates((current) => {
                const pendingState = current[chatId];
                if (!pendingState) return current;
                let updated = false;
                const nextMessages = pendingState.messages.map((message, index, all) => {
                  const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-");
                  if (!isTarget) return message;
                  updated = true;
                  return { ...message, content: payload.text };
                });
                return updated ? { ...current, [chatId]: { messages: nextMessages } } : current;
              });
            },
            onError: (payload) => {
              streamErrorMessage = payload.message;
            },
          },
          { signal: abortController.signal }
        );

        if (streamErrorMessage) {
          throw new Error(streamErrorMessage);
        }

        backgroundSuspendedChatStreamsRef.current.delete(chatId);
        const persistedChat = await retryAfterAppResume(() => getChat(chatId));
        await refreshCollections({ reportTransientError: false });
        if (isCurrentSelection(target)) {
          setSelectedChat(persistedChat);
          setSelectedSearch(null);
        }
        removePendingChatState(chatId);
        removeActiveRun("chat", chatId);
        if (isCurrentSelection(target)) {
          requestSettleTranscriptTailSpacer();
        }
        return target;
      } catch (caughtError) {
        let err: unknown = caughtError;
        const wasBackgroundSuspended =
          abortController.signal.aborted && backgroundSuspendedChatStreamsRef.current.delete(chatId);
        const shouldResumeStream =
          wasBackgroundSuspended ||
          (!abortController.signal.aborted && streamErrorMessage === null && isRecoverableStreamDisconnect(err));

        if (chatStreamAbortRefs.current.get(chatId) === abortController) {
          chatStreamAbortRefs.current.delete(chatId);
        }

        if (shouldResumeStream) {
          try {
            const persistedChat = await retryAfterAppResume(() => getChat(chatId));
            setError(null);
            if (isCurrentSelection(target)) {
              setSelectedChat(persistedChat);
              setSelectedSearch(null);
            }

            // Retrying the same idempotent request either starts an unaccepted
            // submission or replays the existing backend-owned stream. Keep
            // the current optimistic transcript visible until replay begins.
            continue;
          } catch (resumeError) {
            if (isRecoverableStreamDisconnect(resumeError)) {
              setError(null);
              await waitForAppForeground();
              await waitForRetry(1000);
              continue;
            }
            err = resumeError;
          }
        } else if (streamErrorMessage) {
          try {
            const persistedChat = await retryAfterAppResume(() => getChat(chatId));
            if (hasNewPersistedUserMessage(persistedChat, previousMessageIds, content, attachments)) {
              if (isCurrentSelection(target)) {
                setSelectedChat(persistedChat);
                setSelectedSearch(null);
                setError(streamErrorMessage);
              }
              removePendingChatState(chatId);
              removeActiveRun("chat", chatId);
              requestSettleTranscriptTailSpacer();
              return target;
            }
          } catch (reconcileError) {
            err = reconcileError;
          }
        }

        removePendingChatState(chatId);
        removeActiveRun("chat", chatId);
        pendingReplyScrollRef.current = false;
        setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
        throw err;
      } finally {
        if (chatStreamAbortRefs.current.get(chatId) === abortController) {
          chatStreamAbortRefs.current.delete(chatId);
        }
      }
    }
  };

  const handleSendSearch = async (query: string): Promise<SidebarSelection> => {
    let searchId = draftKind === "search" ? null : selectedItem?.kind === "search" ? selectedItem.id : null;

    if (!searchId) {
      const search = await createSearch({
        query,
        title: query.slice(0, 80),
      });
      searchId = search.id;
      supersedeCollectionLoad();
      draftKindRef.current = null;
      setDraftKind(null);
      const createdSearchSelection: SidebarSelection = { kind: "search", id: searchId };
      selectedItemRef.current = createdSearchSelection;
      setSelectedItem(createdSearchSelection);
      setSearches((current) => {
        const withoutExisting = current.filter((existing) => existing.id !== search.id);
        return [search, ...withoutExisting];
      });
      setWorkspaceItems((current) => upsertWorkspaceItem(current, searchWorkspaceItem(search)));
    }

    if (!searchId) {
      throw new Error("Unable to initialize search");
    }

    const targetSearchId = searchId;
    const target: SidebarSelection = { kind: "search", id: targetSearchId };
    const runId = (searchRunCountersRef.current.get(targetSearchId) ?? 0) + 1;
    searchRunCountersRef.current.set(targetSearchId, runId);
    searchRunAbortRefs.current.get(targetSearchId)?.abort();
    const abortController = new AbortController();
    searchRunAbortRefs.current.set(targetSearchId, abortController);
    const isCurrentRun = () => searchRunCountersRef.current.get(targetSearchId) === runId;

    const nowIso = new Date().toISOString();
    const currentSearch = selectedSearchForView?.id === targetSearchId ? selectedSearchForView : null;
    const optimisticSearch: SearchDetail = {
      id: targetSearchId,
      title: query.slice(0, 80),
      query,
      createdAt: currentSearch?.createdAt ?? nowIso,
      updatedAt: nowIso,
      starred: currentSearch?.starred ?? false,
      starredAt: currentSearch?.starredAt ?? null,
      requestId: null,
      latencyMs: null,
      error: null,
      answerText: null,
      answerRequestId: null,
      answerCitations: null,
      answerError: null,
      results: [],
    };
    setRunningSearchStates((current) => ({ ...current, [targetSearchId]: optimisticSearch }));
    addActiveRun("search", targetSearchId);
    if (isCurrentSelection(target)) {
      setSelectedSearch(optimisticSearch);
      setSelectedChat(null);
    }

    const updateRunningSearch = (updater: (search: SearchDetail) => SearchDetail) => {
      setRunningSearchStates((current) => {
        const search = current[targetSearchId];
        if (!search) return current;
        return { ...current, [targetSearchId]: updater(search) };
      });
    };

    try {
      await runSearchStream(
        targetSearchId,
        {
          query,
          title: query.slice(0, 80),
          type: "auto",
          numResults: 10,
        },
        {
          onSearchResults: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => {
              return {
                ...current,
                requestId: payload.requestId ?? current.requestId,
                error: null,
                results: payload.results,
              };
            });
          },
          onSearchError: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => ({ ...current, error: payload.error }));
          },
          onAnswer: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => {
              return {
                ...current,
                answerText: payload.answerText,
                answerRequestId: payload.answerRequestId,
                answerCitations: payload.answerCitations,
                answerError: null,
              };
            });
          },
          onAnswerError: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => ({ ...current, answerError: payload.error }));
          },
          onDone: (payload) => {
            if (!isCurrentRun()) return;
            setRunningSearchStates((current) => ({ ...current, [targetSearchId]: payload.search }));
            if (isCurrentSelection(target)) {
              setSelectedSearch(payload.search);
              setSelectedChat(null);
            }
          },
          onError: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => ({ ...current, error: payload.message }));
            if (isCurrentSelection(target)) {
              setSelectedSearch((current) => (current?.id === targetSearchId ? { ...current, error: payload.message } : current));
              setError(payload.message);
            }
          },
        },
        { signal: abortController.signal }
      );
    } catch (err) {
      if (abortController.signal.aborted) return target;
      throw err;
    } finally {
      if (isCurrentRun()) {
        searchRunAbortRefs.current.delete(targetSearchId);
        searchRunCountersRef.current.delete(targetSearchId);
        removeRunningSearchState(targetSearchId);
        removeActiveRun("search", targetSearchId);
      }
    }

    await refreshCollections();
    return target;
  };

  async function attachToActiveChatStream(chatId: string, resetFromServer = false) {
    if (chatStreamAbortRefs.current.has(chatId)) return;
    const target: SidebarSelection = { kind: "chat", id: chatId };
    addActiveRun("chat", chatId);
    let shouldResetFromServer = resetFromServer;

    try {
      while (true) {
        const abortController = new AbortController();
        chatStreamAbortRefs.current.set(chatId, abortController);
        let streamErrorMessage: string | null = null;

        try {
          const baseChat = await getChat(chatId);
          const resetPendingState = shouldResetFromServer;
          shouldResetFromServer = false;
          setPendingChatStates((current) => {
            if (!resetPendingState && current[chatId]) return current;
            return {
              ...current,
              [chatId]: {
                messages: baseChat.messages.concat({
                  id: `temp-assistant-attach-${chatId}-${Date.now()}`,
                  createdAt: new Date().toISOString(),
                  role: "assistant",
                  content: "",
                  name: null,
                  metadata: null,
                }),
              },
            };
          });

          await attachCompletionStream(
            chatId,
            {
              onToolCall: (payload) => {
                setPendingChatStates((current) => {
                  const pendingState = current[chatId];
                  if (!pendingState) return current;
                  return {
                    ...current,
                    [chatId]: {
                      messages: upsertOptimisticToolMessage(pendingState.messages, payload, "temp-assistant-"),
                    },
                  };
                });
              },
              onDelta: (payload) => {
                if (!payload.text) return;
                setPendingChatStates((current) => {
                  const pendingState = current[chatId];
                  if (!pendingState) return current;
                  let updated = false;
                  const nextMessages = pendingState.messages.map((message, index, all) => {
                    const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-");
                    if (!isTarget) return message;
                    updated = true;
                    return { ...message, content: message.content + payload.text };
                  });
                  return updated ? { ...current, [chatId]: { messages: nextMessages } } : current;
                });
              },
              onDone: (payload) => {
                setPendingChatStates((current) => {
                  const pendingState = current[chatId];
                  if (!pendingState) return current;
                  let updated = false;
                  const nextMessages = pendingState.messages.map((message, index, all) => {
                    const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-");
                    if (!isTarget) return message;
                    updated = true;
                    return { ...message, content: payload.text };
                  });
                  return updated ? { ...current, [chatId]: { messages: nextMessages } } : current;
                });
              },
              onError: (payload) => {
                streamErrorMessage = payload.message;
              },
            },
            { signal: abortController.signal }
          );

          if (streamErrorMessage) {
            throw new Error(streamErrorMessage);
          }

          backgroundSuspendedChatStreamsRef.current.delete(chatId);
          await refreshCollections();
          if (isCurrentSelection(target)) {
            await refreshChat(chatId);
          }
          return;
        } catch (err) {
          const wasBackgroundSuspended =
            abortController.signal.aborted && backgroundSuspendedChatStreamsRef.current.delete(chatId);
          const shouldResumeStream =
            wasBackgroundSuspended ||
            (!abortController.signal.aborted && streamErrorMessage === null && isRecoverableStreamDisconnect(err));

          if (shouldResumeStream) {
            shouldResetFromServer = true;
            try {
              const resumedRuns = await getActiveRunsAfterResume();
              setActiveRuns(buildActiveRunsState(resumedRuns));
              if (resumedRuns.chats.includes(chatId)) {
                continue;
              }

              const persistedChat = await retryAfterAppResume(() => getChat(chatId));
              if (isCurrentSelection(target)) {
                setSelectedChat(persistedChat);
                setSelectedSearch(null);
              }
              void refreshCollections();
              return;
            } catch (resumeError) {
              err = resumeError;
            }
          }
          if (abortController.signal.aborted && !shouldResumeStream) return;

          const message = err instanceof Error ? err.message : String(err);
          if (message.includes("active chat stream not found")) {
            await refreshActiveRuns();
            if (isCurrentSelection(target)) await refreshChat(chatId);
          } else if (message.includes("bearer token")) {
            handleAuthFailure(message);
          } else if (isCurrentSelection(target)) {
            setError(message);
          }
          return;
        } finally {
          if (chatStreamAbortRefs.current.get(chatId) === abortController) {
            chatStreamAbortRefs.current.delete(chatId);
          }
        }
      }
    } finally {
      backgroundSuspendedChatStreamsRef.current.delete(chatId);
      removePendingChatState(chatId);
      removeActiveRun("chat", chatId);
      if (isCurrentSelection(target)) {
        requestSettleTranscriptTailSpacer();
      }
    }
  }

  const attachToActiveSearchStream = async (searchId: string) => {
    if (searchRunAbortRefs.current.has(searchId)) return;
    const target: SidebarSelection = { kind: "search", id: searchId };
    const runId = (searchRunCountersRef.current.get(searchId) ?? 0) + 1;
    searchRunCountersRef.current.set(searchId, runId);
    const abortController = new AbortController();
    searchRunAbortRefs.current.set(searchId, abortController);
    addActiveRun("search", searchId);
    const isCurrentRun = () => searchRunCountersRef.current.get(searchId) === runId;

    try {
      const baseSearch = selectedSearch?.id === searchId ? selectedSearch : await getSearch(searchId);
      setRunningSearchStates((current) => ({ ...current, [searchId]: baseSearch }));

      const updateRunningSearch = (updater: (search: SearchDetail) => SearchDetail) => {
        setRunningSearchStates((current) => {
          const search = current[searchId];
          if (!search) return current;
          return { ...current, [searchId]: updater(search) };
        });
      };

      await attachSearchStream(
        searchId,
        {
          onSearchResults: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => ({
              ...current,
              requestId: payload.requestId ?? current.requestId,
              error: null,
              results: payload.results,
            }));
          },
          onSearchError: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => ({ ...current, error: payload.error }));
          },
          onAnswer: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => ({
              ...current,
              answerText: payload.answerText,
              answerRequestId: payload.answerRequestId,
              answerCitations: payload.answerCitations,
              answerError: null,
            }));
          },
          onAnswerError: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => ({ ...current, answerError: payload.error }));
          },
          onDone: (payload) => {
            if (!isCurrentRun()) return;
            setRunningSearchStates((current) => ({ ...current, [searchId]: payload.search }));
            if (isCurrentSelection(target)) {
              setSelectedSearch(payload.search);
              setSelectedChat(null);
            }
          },
          onError: (payload) => {
            if (!isCurrentRun()) return;
            updateRunningSearch((current) => ({ ...current, error: payload.message }));
            if (isCurrentSelection(target)) {
              setSelectedSearch((current) => (current?.id === searchId ? { ...current, error: payload.message } : current));
              setError(payload.message);
            }
          },
        },
        { signal: abortController.signal }
      );

      await refreshCollections();
      if (isCurrentSelection(target)) {
        await refreshSearch(searchId);
      }
    } catch (err) {
      if (abortController.signal.aborted) return;
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("active search stream not found")) {
        await refreshActiveRuns();
        if (isCurrentSelection(target)) await refreshSearch(searchId);
      } else if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else if (isCurrentSelection(target)) {
        setError(message);
      }
    } finally {
      if (isCurrentRun()) {
        searchRunAbortRefs.current.delete(searchId);
        searchRunCountersRef.current.delete(searchId);
        removeRunningSearchState(searchId);
        removeActiveRun("search", searchId);
      }
    }
  };

  useEffect(() => {
    if (!isAuthenticated || !selectedItem || draftKind) return;
    if (selectedItem.kind === "chat" && activeRuns.chats[selectedItem.id] && !pendingChatStates[selectedItem.id]) {
      void attachToActiveChatStream(selectedItem.id);
    }
    if (selectedItem.kind === "search" && activeRuns.searches[selectedItem.id] && !runningSearchStates[selectedItem.id]) {
      void attachToActiveSearchStream(selectedItem.id);
    }
  }, [activeRuns, draftKind, isAuthenticated, pendingChatStates, runningSearchStates, selectedKey]);

  const handleStartChatFromSearch = async () => {
    const sourceSearch = selectedSearchForView;
    if (!sourceSearch || isStartingSearchChat || isSearchRunning) return;

    setError(null);
    setIsStartingSearchChat(true);
    try {
      const chat = await createChatFromSearch(sourceSearch.id);
      supersedeCollectionLoad();
      draftKindRef.current = null;
      setDraftKind(null);
      replaceComposerDraft("");
      setPendingAttachments([]);
      setChats((current) => {
        const withoutExisting = current.filter((existing) => existing.id !== chat.id);
        return [chat, ...withoutExisting];
      });
      setWorkspaceItems((current) => upsertWorkspaceItem(current, chatWorkspaceItem(chat)));
      const createdChatSelection: SidebarSelection = { kind: "chat", id: chat.id };
      selectedItemRef.current = createdChatSelection;
      setSelectedItem(createdChatSelection);
      setSelectedChat({
        id: chat.id,
        title: chat.title,
        parentChatId: chat.parentChatId,
        titleGenerationPending: chat.titleGenerationPending,
        createdAt: chat.createdAt,
        updatedAt: chat.updatedAt,
        starred: chat.starred,
        starredAt: chat.starredAt,
        initiatedProvider: chat.initiatedProvider,
        initiatedModel: chat.initiatedModel,
        lastUsedProvider: chat.lastUsedProvider,
        lastUsedModel: chat.lastUsedModel,
        additionalSystemPrompt: chat.additionalSystemPrompt,
        enabledTools: chat.enabledTools,
        messages: [],
      });
      setSelectedSearch(null);
      await refreshCollections();
      await refreshChat(chat.id);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setError(message);
      }
    } finally {
      setIsStartingSearchChat(false);
    }
  };

  const handleSendQuickQuestion = async () => {
    const content = quickPrompt.trim();
    if (!content || isQuickQuestionSending || isConvertingQuickQuestion) return;

    const selectedModel = quickModel.trim();
    if (!selectedModel) {
      setQuickQuestionError("No model available for selected provider");
      return;
    }

    const now = new Date().toISOString();
    const optimisticAssistantMessage: Message = {
      id: `temp-assistant-quick-${Date.now()}`,
      createdAt: now,
      role: "assistant",
      content: "",
      name: null,
      metadata: null,
    };

    quickQuestionAbortRef.current?.abort();
    const abortController = new AbortController();
    quickQuestionAbortRef.current = abortController;

    setQuickQuestionError(null);
    setQuickSubmittedPrompt(content);
    setQuickSubmittedModelSelection({ provider: quickProvider, model: selectedModel });
    setQuickQuestionMessages([optimisticAssistantMessage]);
    setIsQuickQuestionSending(true);

    let streamErrorMessage: string | null = null;

    try {
      await runCompletionStream(
        {
          persist: false,
          provider: quickProvider,
          model: selectedModel,
          messages: [{ role: "user", content }],
        },
        {
          onToolCall: (payload) => {
            setQuickQuestionMessages((current) => {
              return upsertOptimisticToolMessage(current, payload, "temp-assistant-quick-");
            });
          },
          onDelta: (payload) => {
            if (!payload.text) return;
            setQuickQuestionMessages((current) => {
              let updated = false;
              const nextMessages = current.map((message, index, all) => {
                const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-quick-");
                if (!isTarget) return message;
                updated = true;
                return { ...message, content: message.content + payload.text };
              });
              return updated ? nextMessages : current;
            });
          },
          onDone: (payload) => {
            setQuickQuestionMessages((current) => {
              let updated = false;
              const nextMessages = current.map((message, index, all) => {
                const isTarget = index === all.length - 1 && message.id.startsWith("temp-assistant-quick-");
                if (!isTarget) return message;
                updated = true;
                return { ...message, content: payload.text };
              });
              return updated ? nextMessages : current;
            });
          },
          onError: (payload) => {
            streamErrorMessage = payload.message;
          },
        },
        { signal: abortController.signal }
      );

      if (streamErrorMessage) {
        throw new Error(streamErrorMessage);
      }
    } catch (err) {
      if (abortController.signal.aborted) return;
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setQuickQuestionError(message);
      }
    } finally {
      if (quickQuestionAbortRef.current === abortController) {
        quickQuestionAbortRef.current = null;
      }
      if (!abortController.signal.aborted) {
        setIsQuickQuestionSending(false);
      }
    }
  };

  const handleConvertQuickQuestionToChat = async () => {
    const question = quickSubmittedPrompt?.trim();
    const answer = quickAnswerText.trim();
    const selection = quickSubmittedModelSelection;
    if (!question || !answer || !selection || isQuickQuestionSending || isConvertingQuickQuestion) return;

    setQuickQuestionError(null);
    setIsConvertingQuickQuestion(true);

    try {
      const title = question.split(/\r?\n/)[0]?.trim().slice(0, 48) || "Quick question";
      const chat = await createChat({
        title,
        provider: selection.provider,
        model: selection.model,
        messages: [
          { role: "user", content: question },
          { role: "assistant", content: answer },
        ],
      });

      supersedeCollectionLoad();
      draftKindRef.current = null;
      setDraftKind(null);
      replaceComposerDraft("");
      setPendingAttachments([]);
      setIsQuickQuestionOpen(false);
      setProvider(selection.provider);
      setModel(selection.model);
      setChats((current) => {
        const withoutExisting = current.filter((existing) => existing.id !== chat.id);
        return [chat, ...withoutExisting];
      });
      setWorkspaceItems((current) => upsertWorkspaceItem(current, chatWorkspaceItem(chat)));
      const createdChatSelection: SidebarSelection = { kind: "chat", id: chat.id };
      selectedItemRef.current = createdChatSelection;
      setSelectedItem(createdChatSelection);
      setSelectedChat({
        id: chat.id,
        title: chat.title,
        parentChatId: chat.parentChatId,
        titleGenerationPending: chat.titleGenerationPending,
        createdAt: chat.createdAt,
        updatedAt: chat.updatedAt,
        starred: chat.starred,
        starredAt: chat.starredAt,
        initiatedProvider: chat.initiatedProvider,
        initiatedModel: chat.initiatedModel,
        lastUsedProvider: chat.lastUsedProvider,
        lastUsedModel: chat.lastUsedModel,
        additionalSystemPrompt: chat.additionalSystemPrompt,
        enabledTools: chat.enabledTools,
        messages: [],
      });
      setSelectedSearch(null);
      await refreshCollections();
      await refreshChat(chat.id);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else {
        setQuickQuestionError(message);
      }
    } finally {
      setIsConvertingQuickQuestion(false);
    }
  };

  const handleSend = async (draft: string) => {
    const content = draft.trim();
    const attachments = pendingAttachments;
    if ((!content && !attachments.length) || isActiveSelectionSending) return;
    if (isSearchMode && attachments.length) {
      setError("Attachments are only available in chat mode.");
      return;
    }

    replaceComposerDraft("");
    setPendingAttachments([]);
    setError(null);

    let sentTarget: SidebarSelection | null = null;
    const sentAsSearch = isSearchMode;
    try {
      if (isSearchMode) {
        sentTarget = await handleSendSearch(content);
      } else {
        sentTarget = await handleSendChat(content, attachments);
      }
    } catch (err) {
      if (!sentAsSearch) {
        pendingReplyScrollRef.current = false;
        setTranscriptTailSpacer(TRANSCRIPT_BOTTOM_GAP);
      }
      const message = err instanceof Error ? err.message : String(err);
      if (message.includes("bearer token")) {
        handleAuthFailure(message);
      } else if (!sentTarget || isCurrentSelection(sentTarget)) {
        setError(message);
      }

      if (!sentAsSearch && (!sentTarget || isCurrentSelection(sentTarget))) {
        replaceComposerDraft(content);
        setPendingAttachments(attachments);
      }

      if (sentTarget && isCurrentSelection(sentTarget) && sentTarget.kind === "chat") {
        await refreshChat(sentTarget.id);
      }
      if (sentTarget && isCurrentSelection(sentTarget) && sentTarget.kind === "search") {
        await refreshSearch(sentTarget.id);
      }
    } finally {
      const shouldSuppressMobileChatFocus = sentTarget?.kind === "chat" && isMobileLayout();
      if ((!sentTarget || isCurrentSelection(sentTarget)) && !shouldSuppressMobileChatFocus) {
        focusComposer();
      }
    }
  };

  const chatSettingsChatId = draftKind === null && selectedItem?.kind === "chat" ? selectedItem.id : null;
  const chatSettingsStarred = chatSettingsChatId ? isSidebarTargetStarred({ kind: "chat", id: chatSettingsChatId }) : false;

  if (isCheckingSession) {
    return (
      <div className="flex h-full items-center justify-center">
        <p className="text-sm text-muted-foreground">Checking session...</p>
      </div>
    );
  }

  if (!isAuthenticated) {
    return (
      <AuthScreen
        authTokenInput={authTokenInput}
        setAuthTokenInput={setAuthTokenInput}
        isSigningIn={isSigningIn}
        authError={authError}
        onSignIn={handleSignIn}
      />
    );
  }

  return (
    <div className="app-grid-surface app-safe-frame h-full">
      <div className="workspace-shell flex h-full w-full overflow-hidden bg-transparent md:gap-2">
        {isMobileSidebarOpen ? (
          <button
            type="button"
            className="workspace-sidebar-backdrop fixed inset-0 z-30 bg-black/70 backdrop-blur-sm md:hidden"
            onClick={() => setIsMobileSidebarOpen(false)}
            aria-label="Close sidebar"
          />
        ) : null}

        <aside
          className={cn(
            "workspace-sidebar glass-panel fixed inset-y-0 left-0 z-40 flex w-[86vw] max-w-80 shrink-0 flex-col border-r border-violet-300/18 transition-transform md:static md:z-auto md:w-80 md:max-w-none md:rounded-2xl md:border",
            isMobileSidebarOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
          )}
        >
          <div className="relative min-h-24 px-4 pb-4 pt-5">
            <div className="pr-24">
              <div className="sybil-wordmark bg-[linear-gradient(90deg,#ff8df8,#9a6dff_54%,#67dfff)] bg-clip-text text-3xl text-transparent">
                SYBIL
              </div>
              <p className="mt-2 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
                <span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
                Sybil Web{authMode ? ` (${authMode === "open" ? "open mode" : "token mode"})` : ""}
              </p>
            </div>
            <SybilCharacter
              className="absolute right-4 top-3 h-[calc(100%-1.5rem)]"
              isBusy={isActiveSelectionSending || isQuickQuestionSending}
            />
          </div>

          <div className="space-y-3 px-3 pb-3">
            <div className="flex gap-2">
              <Button className="h-11 min-w-0 flex-1 justify-start gap-3 text-[15px]" onClick={handleCreateChat}>
                <Plus className="h-4 w-4" />
                New chat
                <span className="ml-auto rounded-md border border-violet-100/12 bg-white/5 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-100/52">
                  {primaryShortcutModifier} J
                </span>
              </Button>
              <div className="group relative">
                <Button
                  className="h-11 w-11 rounded-lg"
                  onClick={handleOpenQuickQuestion}
                  size="icon"
                  variant="secondary"
                  title={`${primaryShortcutModifier}+i`}
                  aria-label="Quick question"
                >
                  <Rabbit className="h-4 w-4" />
                </Button>
                <span className="pointer-events-none absolute left-1/2 top-full z-50 mt-2 -translate-x-1/2 whitespace-nowrap rounded-md border border-violet-300/22 bg-[hsl(238_48%_7%)] px-2 py-1 text-xs font-semibold text-violet-100/90 opacity-0 shadow-xl shadow-black/35 transition group-hover:opacity-100 group-focus-within:opacity-100">
                  {primaryShortcutModifier}+i
                </span>
              </div>
            </div>
            <Button className="h-10 w-full justify-start gap-3" variant="secondary" onClick={handleCreateSearch}>
              <Search className="h-4 w-4" />
              New search
              <span className="ml-auto rounded-md border border-violet-100/10 bg-white/[0.035] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-violet-100/44">
                Shift {primaryShortcutModifier} J
              </span>
            </Button>
            <div className="relative">
              <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-violet-200/58" />
              <input
                value={sidebarQuery}
                onInput={(event) => setSidebarQuery(event.currentTarget.value)}
                placeholder="Search chats"
                className="h-10 w-full rounded-lg border border-violet-300/18 bg-background/66 pl-9 pr-3 text-sm text-violet-50 outline-none shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.05)] placeholder:text-muted-foreground focus:border-violet-300/45 focus:ring-1 focus:ring-ring/70"
              />
            </div>
          </div>
          <Separator className="bg-violet-300/10" />
          <div className="flex-1 overflow-y-auto px-2 py-3">
            {isLoadingCollections && sidebarItems.length === 0 ? <p className="px-2 py-3 text-sm text-muted-foreground">Loading conversations...</p> : null}
            {!isLoadingCollections && sidebarItems.length === 0 ? (
              <div className="flex h-full flex-col items-center justify-center gap-2 p-5 text-center text-sm text-muted-foreground">
                <MessageSquare className="h-5 w-5" />
                Start a chat or run your first search.
              </div>
            ) : null}
            {!isLoadingCollections && sidebarItems.length > 0 && filteredSidebarItems.length === 0 ? (
              <p className="px-2 py-3 text-sm text-muted-foreground">No chats found.</p>
            ) : null}
            {sidebarSections.map((section) => (
              <div key={section.label} className="mb-4">
                <p className="px-3 pb-2 text-[11px] font-semibold text-violet-200/48">{section.label}</p>
                {section.items.map((item) => {
                  const active = selectedItem?.kind === item.kind && selectedItem.id === item.id;
                  const isForkChild = item.kind === "chat" && item.parentChatId !== null;
                  const familyItems = item.kind === "chat" ? getChatFamilyItems(item.id) : [item];
                  const itemIsRunning = familyItems.some((familyItem) => isItemRunning(familyItem));
                  const itemIsStarred =
                    familyItems.find((familyItem) => familyItem.parentChatId === null)?.starred ?? false;
                  const itemUpdatedAt = familyItems.reduce(
                    (newest, familyItem) =>
                      new Date(familyItem.updatedAt).getTime() > new Date(newest).getTime()
                        ? familyItem.updatedAt
                        : newest,
                    item.updatedAt
                  );
                  const initiatedLabel = item.kind === "chat" && item.initiatedModel
                    ? `${getProviderLabel(item.initiatedProvider)}${item.initiatedProvider ? " · " : ""}${item.initiatedModel}`
                    : null;
                  return (
                    <button
                      key={`${item.kind}-${item.id}`}
                      className={cn(
                        "mb-1 rounded-lg border text-left transition",
                        isForkChild ? "ml-7 w-[calc(100%-1.75rem)] px-3 py-1.5" : "w-full px-3 py-2.5",
                        active
                          ? "border-violet-300/45 bg-[linear-gradient(135deg,hsl(258_86%_52%_/_0.58),hsl(277_78%_28%_/_0.55))] text-violet-50"
                          : "border-transparent text-violet-100/78 hover:border-violet-300/18 hover:bg-violet-400/10"
                      )}
                      onClick={() => {
                        setContextMenu(null);
                        draftKindRef.current = null;
                        setDraftKind(null);
                        const target = { kind: item.kind, id: item.id } as SidebarSelection;
                        selectedItemRef.current = target;
                        setSelectedItem(target);
                        setIsMobileSidebarOpen(false);
                      }}
                      onContextMenu={(event) => openContextMenu(event, { kind: item.kind, id: item.id })}
                      type="button"
                      title={isForkChild ? item.title : undefined}
                    >
                      {isForkChild ? (
                        <span className="block truncate text-xs font-medium leading-5">{item.title}</span>
                      ) : (
                        <div className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-1">
                          <span
                            className={cn(
                              "flex h-5 w-5 shrink-0 items-center justify-center rounded-md border",
                              active ? "border-cyan-200/35 bg-cyan-300/12 text-cyan-100" : "border-violet-300/18 text-violet-200/70"
                            )}
                          >
                            {item.kind === "chat" ? <MessageSquare className="h-3.5 w-3.5" /> : <Search className="h-3.5 w-3.5" />}
                          </span>
                          <span className="flex min-w-0 flex-1 items-center gap-1.5">
                            <span className="truncate text-sm font-semibold">{item.title}</span>
                            {itemIsStarred ? (
                              <Star
                                className={cn("h-3.5 w-3.5 shrink-0 fill-amber-300", active ? "text-amber-200" : "text-amber-300/90")}
                                aria-label="Starred"
                              />
                            ) : null}
                            {itemIsRunning ? (
                              <LoaderCircle
                                className={cn("h-3.5 w-3.5 shrink-0 animate-spin", active ? "text-cyan-100" : "text-cyan-300/90")}
                                aria-label="Generating"
                              />
                            ) : null}
                          </span>
                          <span className="col-start-2 flex min-w-0 items-center gap-2">
                            <span className="shrink-0 text-xs text-secondary-foreground/70">{formatDate(itemUpdatedAt)}</span>
                            {initiatedLabel ? (
                              <span className="ml-auto min-w-0 truncate text-right text-xs text-secondary-foreground/70">
                                {initiatedLabel}
                              </span>
                            ) : null}
                          </span>
                        </div>
                      )}
                    </button>
                  );
                })}
              </div>
            ))}
          </div>
        </aside>

        <main
          className="workspace-content glass-panel relative flex min-w-0 flex-1 touch-pan-y flex-col overflow-hidden border-violet-300/18 md:touch-auto md:rounded-2xl md:border"
          onTouchStart={handleMobileWorkspaceTouchStart}
          onTouchMove={handleMobileWorkspaceTouchMove}
          onTouchEnd={handleMobileWorkspaceTouchEnd}
          onTouchCancel={handleMobileWorkspaceTouchCancel}
        >
          <header className="flex items-center justify-between gap-2 border-b border-violet-300/12 bg-[linear-gradient(180deg,hsl(243_48%_10%_/_0.86),hsl(236_48%_6%_/_0.66))] px-4 py-3 md:gap-3 md:px-7">
            <div className="flex min-w-0 items-center gap-2">
              <Button
                type="button"
                size="icon"
                variant="ghost"
                className="workspace-sidebar-trigger -ml-1 h-8 w-8 md:hidden"
                onClick={() => setIsMobileSidebarOpen(true)}
                aria-label="Open sidebar"
              >
                <Menu className="h-4 w-4" />
              </Button>

              <div className="flex min-w-0 items-center gap-1.5">
                <h1 className="truncate text-sm font-semibold text-violet-50 md:text-base">{selectedTitle}</h1>
              </div>
            </div>
            <div className="flex shrink-0 items-center justify-end gap-2">
              {!isSearchMode ? (
                <Button
                  type="button"
                  variant="secondary"
                  className="h-10 max-w-[44vw] gap-2 rounded-lg px-3 md:max-w-full"
                  onClick={openChatSettings}
                  disabled={isActiveSelectionSending}
                  aria-label="Open chat settings"
                >
                  <Settings2 className="h-4 w-4 shrink-0" />
                  <span className="hidden shrink-0 sm:inline">Settings</span>
                  <span className="hidden min-w-0 max-w-[18rem] truncate text-xs font-medium text-violet-100/58 sm:inline">
                    {getProviderLabel(provider)} · {model || "No model"}
                  </span>
                </Button>
              ) : (
                <div className="flex h-10 items-center rounded-lg border border-cyan-300/22 bg-cyan-300/8 px-3 text-sm text-cyan-100">
                  <Globe2 className="mr-2 h-4 w-4" />
                  Search mode
                </div>
              )}
            </div>
          </header>

          <div
            ref={transcriptContainerRef}
            className="flex-1 overflow-y-auto px-4 pt-8 md:px-10 lg:px-14 pb-36 md:pb-44 [overflow-anchor:none]"
            onScroll={() => {
              const container = transcriptContainerRef.current;
              if (!container) return;
              const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
              shouldAutoScrollRef.current = distanceFromBottom < 96;
              if (!isSearchMode && !isSendingActiveChat && distanceFromBottom > 0) {
                settleTranscriptTailSpacer();
              }
            }}
          >
            {!isSearchMode ? (
              <ChatMessagesPanel
                messages={displayMessages}
                isLoading={isLoadingSelection}
                isSending={isSendingActiveChat}
                onMessageContextMenu={openMessageContextMenu}
              />
            ) : (
              <SearchResultsPanel
                search={selectedSearchForView}
                isLoading={isLoadingSelection}
                isRunning={isSearchRunning}
                isStartingChat={isStartingSearchChat}
                onStartChat={selectedSearchForView ? handleStartChatFromSearch : undefined}
              />
            )}
            <div
              className="mx-auto max-w-4xl"
              style={{ height: `${transcriptTailSpacerHeight}px` }}
              aria-hidden="true"
            />
            <div ref={transcriptEndRef} />
          </div>

          <footer
            className="pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-[linear-gradient(to_top,hsl(235_50%_4%)_0%,hsl(235_50%_4%_/_0.92)_58%,transparent)] p-3 pt-14 md:p-6 md:pt-20"
            data-mobile-swipe-ignore
          >
            <div
              className={cn(
                "pointer-events-auto mx-auto max-w-4xl rounded-2xl border bg-[linear-gradient(135deg,hsl(235_48%_7%_/_0.96),hsl(258_48%_11%_/_0.94))] p-2 shadow-lg shadow-black/20 transition",
                isComposerDropActive
                  ? "border-cyan-300/70 shadow-cyan-500/20"
                  : "border-violet-300/30"
              )}
              onDragEnter={handleComposerDragEnter}
              onDragOver={handleComposerDragOver}
              onDragLeave={handleComposerDragLeave}
              onDrop={(event) => {
                void handleComposerDrop(event);
              }}
            >
              <input
                ref={fileInputRef}
                type="file"
                multiple
                accept={CHAT_FILE_ACCEPT}
                className="hidden"
                onChange={(event) => {
                  void handleFileSelection(event);
                }}
              />
              {!isSearchMode && pendingAttachments.length ? (
                <div className="px-2 pb-2 pt-1">
                  <ChatAttachmentList attachments={pendingAttachments} onRemove={handleRemovePendingAttachment} />
                </div>
              ) : null}
              {!isSearchMode && isComposerDropActive ? (
                <div className="px-3 pb-2">
                  <div className="rounded-xl border border-dashed border-cyan-300/55 bg-cyan-300/8 px-4 py-3 text-sm text-cyan-100">
                    Drop files to attach them
                  </div>
                </div>
              ) : null}
              <ChatComposer
                draftRef={composerDraftRef}
                draftRevision={composerDraftRevision}
                error={error}
                isSearchMode={isSearchMode}
                isSending={isActiveSelectionSending}
                pendingAttachmentCount={pendingAttachments.length}
                attachmentButtonDisabled={
                  isActiveSelectionSending || pendingAttachments.length >= MAX_CHAT_ATTACHMENTS
                }
                onOpenAttachmentPicker={handleOpenAttachmentPicker}
                onPaste={(event) => {
                  void handleComposerPaste(event);
                }}
                onSend={handleSend}
              />
            </div>
          </footer>
        </main>
      </div>
      {contextMenu ? (
        <div
          ref={contextMenuRef}
          className="fixed z-50 min-w-40 rounded-lg border border-violet-300/20 bg-[hsl(238_48%_7%)] p-1 shadow-2xl shadow-black/45"
          style={{ left: contextMenu.x, top: contextMenu.y }}
          onContextMenu={(event) => event.preventDefault()}
        >
          {contextMenu.kind === "message" ? (
            <button
              type="button"
              className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-violet-100 transition hover:bg-violet-400/12 disabled:text-muted-foreground"
              onClick={() => void handleForkChat(contextMenu.chatId, contextMenu.messageId)}
              disabled={isForkingChat}
            >
              <GitFork className="h-3.5 w-3.5" />
              Fork
            </button>
          ) : (
            <>
              <button
                type="button"
                className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-violet-100 transition hover:bg-violet-400/12"
                onClick={() => void handleToggleStar(contextMenu.item)}
              >
                <Star
                  className={cn(
                    "h-3.5 w-3.5",
                    isSidebarTargetStarred(contextMenu.item)
                      ? "fill-amber-300 text-amber-300"
                      : ""
                  )}
                />
                {isSidebarTargetStarred(contextMenu.item) ? "Unstar" : "Star"}
              </button>
              {contextMenu.item.kind === "chat" ? (
                <>
                  <button
                    type="button"
                    className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-violet-100 transition hover:bg-violet-400/12 disabled:text-muted-foreground"
                    onClick={() => void handleForkChat(contextMenu.item.id)}
                    disabled={isForkingChat || isItemRunning(contextMenu.item)}
                  >
                    <GitFork className="h-3.5 w-3.5" />
                    Fork
                  </button>
                  <button
                    type="button"
                    className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-violet-100 transition hover:bg-violet-400/12"
                    onClick={() => openRenameChatDialog(contextMenu.item.id)}
                  >
                    <Pencil className="h-3.5 w-3.5" />
                    Rename
                  </button>
                </>
              ) : null}
              <button
                type="button"
                className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-rose-300 transition hover:bg-rose-500/12 disabled:text-muted-foreground"
                onClick={() => void handleDeleteFromContextMenu()}
                disabled={isDeleteBlocked(contextMenu.item)}
              >
                <Trash2 className="h-3.5 w-3.5" />
                {isRootChat(contextMenu.item) ? "Delete group" : "Delete"}
              </button>
            </>
          )}
        </div>
      ) : null}
      {isChatSettingsOpen ? (
        <div
          className="fixed inset-0 z-[60] flex items-center justify-center bg-black/72 p-3 backdrop-blur-md md:p-6"
          onMouseDown={(event) => {
            if (event.target === event.currentTarget && !isSavingChatSettings) setIsChatSettingsOpen(false);
          }}
        >
          <form
            role="dialog"
            aria-modal="true"
            aria-labelledby="chat-settings-title"
            className="glass-panel flex max-h-[88vh] w-full max-w-2xl flex-col rounded-2xl border border-violet-300/24 p-4 shadow-2xl shadow-black/45 md:p-5"
            onSubmit={(event) => void handleChatSettingsSubmit(event)}
          >
            <div className="mb-4 flex items-center justify-between gap-3">
              <div className="min-w-0">
                <h2 id="chat-settings-title" className="text-sm font-semibold text-violet-50">
                  Chat settings
                </h2>
                <p className="mt-1 truncate text-xs text-muted-foreground">{chatSettingsTitleDraft.trim() || "New chat"}</p>
              </div>
              <Button
                type="button"
                size="icon"
                variant="ghost"
                className="h-8 w-8"
                onClick={() => setIsChatSettingsOpen(false)}
                disabled={isSavingChatSettings}
                aria-label="Close chat settings"
              >
                <X className="h-4 w-4" />
              </Button>
            </div>

            <div className="min-h-0 flex-1 space-y-4 overflow-y-auto pr-1">
              <div>
                <span className="mb-1.5 block text-xs font-semibold text-violet-100/72">Chat title</span>
                <div className="flex items-center gap-2">
                  <input
                    value={chatSettingsTitleDraft}
                    onInput={(event) => {
                      setChatSettingsTitleDraft(event.currentTarget.value);
                      if (chatSettingsError) setChatSettingsError(null);
                    }}
                    maxLength={120}
                    placeholder={draftKind === null && selectedItem?.kind === "chat" ? "Chat title" : "Optional title"}
                    className="h-11 min-w-0 flex-1 rounded-lg border border-violet-300/22 bg-background/72 px-3 text-sm text-violet-50 outline-none shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.06)] placeholder:text-muted-foreground focus:border-violet-300/45 focus:ring-1 focus:ring-ring/70"
                    disabled={isSavingChatSettings}
                  />
                  {chatSettingsChatId ? (
                    <Button
                      type="button"
                      size="icon"
                      variant="secondary"
                      className="h-11 w-11 shrink-0 rounded-lg"
                      onClick={() => void handleToggleChatSettingsStar()}
                      disabled={isSavingChatSettings || isTogglingChatSettingsStar}
                      aria-label={chatSettingsStarred ? "Unstar chat" : "Star chat"}
                      title={chatSettingsStarred ? "Unstar chat" : "Star chat"}
                    >
                      {isTogglingChatSettingsStar ? (
                        <LoaderCircle className="h-4 w-4 animate-spin" />
                      ) : (
                        <Star className={cn("h-4 w-4", chatSettingsStarred ? "fill-amber-300 text-amber-300" : "")} />
                      )}
                    </Button>
                  ) : null}
                </div>
              </div>

              <div className="grid gap-3 md:grid-cols-[minmax(9rem,0.7fr)_minmax(14rem,1fr)]">
                <label className="block">
                  <span className="mb-1.5 block text-xs font-semibold text-violet-100/72">Provider</span>
                  <select
                    className="h-10 w-full rounded-lg border border-violet-300/22 bg-background/72 px-3 text-sm text-violet-50 outline-none shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.06)] focus:border-violet-300/45 focus:ring-1 focus:ring-ring/70"
                    value={chatSettingsProviderDraft}
                    onChange={(event) => {
                      const nextProvider = event.currentTarget.value as Provider;
                      setChatSettingsProviderDraft(nextProvider);
                      const options = getModelOptions(modelCatalog, nextProvider);
                      setChatSettingsModelDraft(pickProviderModel(options, providerModelPreferences[nextProvider]));
                      setChatSettingsError(null);
                    }}
                    disabled={isSavingChatSettings}
                  >
                    {providerOptions.map((candidate) => (
                      <option key={candidate} value={candidate}>
                        {getProviderLabel(candidate)}
                      </option>
                    ))}
                  </select>
                </label>

                <label className="block min-w-0">
                  <span className="mb-1.5 block text-xs font-semibold text-violet-100/72">Model</span>
                  <ModelCombobox
                    options={chatSettingsProviderModelOptions}
                    value={chatSettingsModelDraft}
                    disabled={isSavingChatSettings}
                    onChange={(nextModel) => {
                      setChatSettingsModelDraft(nextModel.trim());
                      setChatSettingsError(null);
                    }}
                  />
                </label>
              </div>

              <label className="block">
                <span className="mb-1.5 block text-xs font-semibold text-violet-100/72">Additional system prompt</span>
                <Textarea
                  rows={5}
                  value={chatSettingsPromptDraft}
                  onInput={(event) => {
                    setChatSettingsPromptDraft(event.currentTarget.value);
                    if (chatSettingsError) setChatSettingsError(null);
                  }}
                  placeholder="Add per-chat instructions"
                  className="min-h-32 resize-y border-violet-300/24 bg-background/72 text-sm text-violet-50 placeholder:text-violet-200/45"
                  disabled={isSavingChatSettings}
                />
              </label>

              <section>
                <div className="mb-2 flex items-center justify-between gap-3">
                  <h3 className="text-xs font-semibold text-violet-100/72">Tools</h3>
                  {availableChatTools.length ? (
                    <div className="flex items-center gap-2">
                      <Button
                        type="button"
                        size="sm"
                        variant="secondary"
                        onClick={() => setChatSettingsEnabledToolsDraft(getDefaultEnabledTools(availableChatTools))}
                        disabled={isSavingChatSettings}
                      >
                        <Check className="h-3.5 w-3.5" />
                        All
                      </Button>
                      <Button
                        type="button"
                        size="sm"
                        variant="secondary"
                        onClick={() => setChatSettingsEnabledToolsDraft([])}
                        disabled={isSavingChatSettings}
                      >
                        <X className="h-3.5 w-3.5" />
                        None
                      </Button>
                    </div>
                  ) : null}
                </div>
                <div className="space-y-2">
                  {availableChatTools.length ? (
                    availableChatTools.map((tool) => {
                      const checked = chatSettingsEnabledToolsDraft.includes(tool.name);
                      return (
                        <label
                          key={tool.name}
                          className="flex cursor-pointer items-start gap-3 rounded-lg border border-violet-300/18 bg-background/44 px-3 py-2.5 transition hover:border-violet-300/34 hover:bg-violet-400/8"
                        >
                          <input
                            type="checkbox"
                            checked={checked}
                            onChange={() => toggleChatSettingsTool(tool.name)}
                            className="mt-1 h-4 w-4 rounded border-violet-300/35 bg-background/80 accent-violet-400"
                            disabled={isSavingChatSettings}
                          />
                          <span className="min-w-0">
                            <span className="block text-sm font-medium text-violet-50">{getToolLabel(tool.name)}</span>
                            <span className="mt-0.5 block text-xs leading-5 text-muted-foreground">{tool.description}</span>
                          </span>
                        </label>
                      );
                    })
                  ) : (
                    <p className="rounded-lg border border-violet-300/18 bg-background/44 px-3 py-2.5 text-sm text-muted-foreground">
                      No chat tools are available.
                    </p>
                  )}
                </div>
              </section>
            </div>

            {chatSettingsError ? <p className="mt-3 text-sm text-rose-300">{chatSettingsError}</p> : null}
            <div className="mt-4 flex justify-end gap-2">
              <Button type="button" variant="secondary" onClick={() => setIsChatSettingsOpen(false)} disabled={isSavingChatSettings}>
                Cancel
              </Button>
              <Button type="submit" disabled={isSavingChatSettings}>
                {isSavingChatSettings ? <LoaderCircle className="h-4 w-4 animate-spin" /> : <Check className="h-4 w-4" />}
                Save
              </Button>
            </div>
          </form>
        </div>
      ) : null}
      {renameChatDialog ? (
        <div
          className="fixed inset-0 z-[60] flex items-center justify-center bg-black/72 p-3 backdrop-blur-md md:p-6"
          onMouseDown={(event) => {
            if (event.target === event.currentTarget && !isRenamingChat) setRenameChatDialog(null);
          }}
        >
          <form
            role="dialog"
            aria-modal="true"
            aria-labelledby="rename-chat-title"
            className="glass-panel w-full max-w-md rounded-2xl border border-violet-300/24 p-4 shadow-2xl shadow-black/45 md:p-5"
            onSubmit={(event) => void handleRenameChatSubmit(event)}
          >
            <div className="mb-4 flex items-center justify-between gap-3">
              <h2 id="rename-chat-title" className="text-sm font-semibold text-violet-50">
                Rename chat
              </h2>
              <Button
                type="button"
                size="icon"
                variant="ghost"
                className="h-8 w-8"
                onClick={() => setRenameChatDialog(null)}
                disabled={isRenamingChat}
                aria-label="Close rename dialog"
              >
                <X className="h-4 w-4" />
              </Button>
            </div>
            <input
              ref={renameChatInputRef}
              value={renameChatDraft}
              onInput={(event) => {
                setRenameChatDraft(event.currentTarget.value);
                if (renameChatError) setRenameChatError(null);
              }}
              maxLength={120}
              className="h-11 w-full rounded-lg border border-violet-300/22 bg-background/72 px-3 text-sm text-violet-50 outline-none shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.06)] placeholder:text-muted-foreground focus:border-violet-300/45 focus:ring-1 focus:ring-ring/70"
              aria-label="Chat title"
              disabled={isRenamingChat}
            />
            {renameChatError ? <p className="mt-2 text-sm text-rose-300">{renameChatError}</p> : null}
            <div className="mt-4 flex justify-end gap-2">
              <Button type="button" variant="secondary" onClick={() => setRenameChatDialog(null)} disabled={isRenamingChat}>
                Cancel
              </Button>
              <Button type="submit" disabled={isRenamingChat || !renameChatDraft.trim()}>
                {isRenamingChat ? <LoaderCircle className="h-4 w-4 animate-spin" /> : <Check className="h-4 w-4" />}
                Save
              </Button>
            </div>
          </form>
        </div>
      ) : null}
      {isQuickQuestionOpen ? (
        <div
          className="fixed inset-0 z-[60] flex items-center justify-center bg-black/72 p-3 backdrop-blur-md md:p-6"
          onMouseDown={(event) => {
            if (event.target === event.currentTarget) setIsQuickQuestionOpen(false);
          }}
        >
          <section
            role="dialog"
            aria-modal="true"
            aria-labelledby="quick-question-title"
            className="glass-panel flex max-h-[88vh] w-full max-w-3xl flex-col rounded-2xl border border-violet-300/24 p-4 shadow-2xl shadow-black/45 md:p-5"
          >
            <div className="mb-3 flex items-center justify-between gap-3">
              <h2 id="quick-question-title" className="text-sm font-semibold text-violet-50">
                Quick question
              </h2>
              <Button
                type="button"
                size="icon"
                variant="ghost"
                className="h-8 w-8"
                onClick={() => setIsQuickQuestionOpen(false)}
                aria-label="Close quick question"
              >
                <X className="h-4 w-4" />
              </Button>
            </div>

            <div className="min-h-0 flex-1 space-y-3">
              <Textarea
                id="quick-question-input"
                rows={2}
                value={quickPrompt}
                onInput={(event) => {
                  const textarea = event.currentTarget;
                  textarea.style.height = "0px";
                  textarea.style.height = `${textarea.scrollHeight}px`;
                  const nextPrompt = textarea.value;
                  if (nextPrompt !== quickPrompt) {
                    quickQuestionAbortRef.current?.abort();
                    quickQuestionAbortRef.current = null;
                    setIsQuickQuestionSending(false);
                    setQuickSubmittedPrompt(null);
                    setQuickSubmittedModelSelection(null);
                    setQuickQuestionMessages([]);
                    setQuickQuestionError(null);
                  }
                  setQuickPrompt(nextPrompt);
                }}
                onKeyDown={(event) => {
                  if (event.key === "Enter" && !event.shiftKey) {
                    event.preventDefault();
                    void handleSendQuickQuestion();
                  }
                }}
                placeholder="Ask Sybil..."
                className="max-h-36 min-h-[4.75rem] resize-none overflow-y-auto border-violet-300/24 bg-background/72 text-base text-violet-50 placeholder:text-violet-200/45"
                disabled={isQuickQuestionSending || isConvertingQuickQuestion}
              />

              <div className="h-[min(34vh,22rem)] overflow-y-auto rounded-xl border border-violet-300/16 bg-background/38 px-3 py-4">
                {quickQuestionMessages.length ? (
                  <ChatMessagesPanel messages={quickQuestionMessages} isLoading={false} isSending={isQuickQuestionSending} />
                ) : null}
                {quickQuestionError ? (
                  <p className="text-sm text-rose-300">{quickQuestionError}</p>
                ) : null}
              </div>
            </div>

            <div className="mt-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
              <div className="flex min-w-0 flex-1 flex-col gap-2 sm:flex-row sm:items-center">
                <select
                  className="h-10 min-w-32 rounded-lg border border-violet-300/22 bg-background/72 px-3 text-sm text-violet-50 outline-none shadow-[inset_0_1px_0_hsl(255_100%_92%_/_0.06)] focus:border-violet-300/45 focus:ring-1 focus:ring-ring/70"
                  value={quickProvider}
                  onChange={(event) => {
                    const nextProvider = event.currentTarget.value as Provider;
                    setQuickProvider(nextProvider);
                    const options = getModelOptions(modelCatalog, nextProvider);
                    setQuickModel(pickProviderModel(options, quickProviderModelPreferences[nextProvider]));
                  }}
                  disabled={isQuickQuestionSending || isConvertingQuickQuestion}
                  aria-label="Quick question provider"
                >
                  {providerOptions.map((candidate) => (
                    <option key={candidate} value={candidate}>
                      {getProviderLabel(candidate)}
                    </option>
                  ))}
                </select>
                <ModelCombobox
                  options={quickProviderModelOptions}
                  value={quickModel}
                  disabled={isQuickQuestionSending || isConvertingQuickQuestion}
                  onChange={(nextModel) => {
                    const normalizedModel = nextModel.trim();
                    setQuickModel(normalizedModel);
                    setQuickProviderModelPreferences((current) => ({
                      ...current,
                      [quickProvider]: normalizedModel || null,
                    }));
                  }}
                />
              </div>

              <div className="flex items-center justify-end gap-2">
                <Button
                  type="button"
                  variant="secondary"
                  className="gap-2"
                  onClick={() => void handleConvertQuickQuestionToChat()}
                  disabled={!canConvertQuickQuestion || isConvertingQuickQuestion}
                >
                  <MessageSquare className="h-4 w-4" />
                  Convert to chat
                </Button>
                <Button
                  type="button"
                  className="h-10 w-10 rounded-lg"
                  onClick={() => void handleSendQuickQuestion()}
                  size="icon"
                  disabled={isQuickQuestionSending || isConvertingQuickQuestion || !quickPrompt.trim()}
                  aria-label="Ask quick question"
                >
                  <SendHorizontal className="h-4 w-4" />
                </Button>
              </div>
            </div>
          </section>
        </div>
      ) : null}
    </div>
  );
}
