import blessed from "blessed";
import { SybilApiClient } from "./api.js";
import { config } from "./config.js";
import type {
  ChatDetail,
  ChatSummary,
  CompletionRequestMessage,
  Message,
  ModelCatalogResponse,
  Provider,
  SearchDetail,
  SearchSummary,
  ToolCallEvent,
  WorkspaceItem,
} from "./types.js";

type SidebarSelection = { kind: "chat" | "search"; id: string };
type DraftSelectionKind = "chat" | "search";
type SidebarItem = SidebarSelection & {
  title: string;
  updatedAt: string;
  createdAt: string;
  starred: boolean;
  starredAt: string | null;
  initiatedProvider: Provider | null;
  initiatedModel: string | null;
  lastUsedProvider: Provider | null;
  lastUsedModel: string | null;
};

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;
};

const BASE_PROVIDERS: Provider[] = ["openai", "anthropic", "xai", "gemini"];
const PROVIDERS: Provider[] = [...BASE_PROVIDERS, "hermes-agent"];
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 },
};

function escapeTags(value: string) {
  return value.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}");
}

function truncate(value: string, maxLength: number) {
  if (value.length <= maxLength) return value;
  return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
}

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

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 getChatTitle(chat: Pick<ChatSummary, "title">, messages?: ChatDetail["messages"]) {
  if (chat.title?.trim()) return chat.title.trim();
  const firstUserMessage = messages?.find((message) => message.role === "user")?.content.trim();
  if (firstUserMessage) return firstUserMessage.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 upsertWorkspaceItem(items: WorkspaceItem[], item: WorkspaceItem) {
  return [item, ...items.filter((existing) => existing.type !== item.type || existing.id !== item.id)];
}

function buildSidebarItems(items: WorkspaceItem[]): SidebarItem[] {
  const chatsById = new Map<string, ChatSummary>();
  for (const item of items) {
    if (item.type === "chat") chatsById.set(item.id, item);
  }

  return items.map((item) => {
    if (item.type === "chat") {
      const chat = item;
      const starOwner = chatsById.get(chat.parentChatId ?? chat.id) ?? chat;
      return {
        kind: "chat" as const,
        id: chat.id,
        title: getChatTitle(chat),
        updatedAt: chat.updatedAt,
        createdAt: chat.createdAt,
        starred: starOwner.starred,
        starredAt: starOwner.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),
      updatedAt: search.updatedAt,
      createdAt: search.createdAt,
      starred: search.starred,
      starredAt: search.starredAt,
      initiatedProvider: null,
      initiatedModel: null,
      lastUsedProvider: null,
      lastUsedModel: 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 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) {
  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("temp-assistant-")
  );
  if (assistantIndex < 0) return messages.concat(toolMessage);
  return [...messages.slice(0, assistantIndex), toolMessage, ...messages.slice(assistantIndex)];
}

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 PROVIDERS.filter((provider) => provider !== "hermes-agent" || catalog[provider] !== undefined);
}

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

function selectionKey(selection: SidebarSelection | null) {
  if (!selection) return null;
  return `${selection.kind}:${selection.id}`;
}

function formatHost(url: string) {
  try {
    return new URL(url).hostname.replace(/^www\./, "");
  } catch {
    return url;
  }
}

function isTextInputFocused(screen: blessed.Widgets.Screen, composer: blessed.Widgets.TextboxElement) {
  return screen.focused === composer;
}

async function main() {
  const api = new SybilApiClient(config.apiBaseUrl, config.adminToken);

  let authMode: "open" | "token" | null = null;
  let chats: ChatSummary[] = [];
  let searches: SearchSummary[] = [];
  let workspaceItems: WorkspaceItem[] = [];
  let selectedItem: SidebarSelection | null = null;
  let selectedChat: ChatDetail | null = null;
  let selectedSearch: SearchDetail | null = null;
  let draftKind: DraftSelectionKind | null = null;
  let isLoadingCollections = false;
  let isLoadingSelection = false;
  let isSending = false;
  let pendingChatState: { chatId: string | null; messages: Message[] } | null = null;
  let provider: Provider = config.defaultProvider;
  let modelCatalog: ModelCatalogResponse["providers"] = EMPTY_MODEL_CATALOG;
  const providerModelPreferences: Record<Provider, string | null> = {
    openai: null,
    anthropic: null,
    xai: null,
    gemini: null,
    "hermes-agent": null,
  };
  let model: string = config.defaultModel ?? pickProviderModel(getModelOptions(modelCatalog, provider), null);
  let errorMessage: string | null = null;
  let forceScrollToBottom = true;
  let searchRunController: AbortController | null = null;
  let searchRunCounter = 0;
  const pendingTitleGeneration = new Set<string>();
  let renderedSidebarSelectedIndex = -1;
  let highlightedSidebarKey: string | null = null;
  let renderedSidebarItems: SidebarItem[] = [];
  let renderedSidebarLines: string[] = [];
  let suppressedSidebarSelectEvents = 0;
  let isRenamePromptOpen = false;

  const screen = blessed.screen({
    smartCSR: true,
    fullUnicode: true,
    title: "Sybil TUI",
  });

  const sidebar = blessed.list({
    parent: screen,
    label: " Conversations ",
    border: "line",
    tags: true,
    top: 0,
    left: 0,
    bottom: 0,
    width: "32%",
    keys: true,
    vi: true,
    mouse: true,
    style: {
      border: { fg: "magenta" },
    },
    scrollbar: {
      ch: " ",
    },
    scrollable: true,
  });

  const sidebarStyle = sidebar.style as any;
  sidebarStyle.item = {
    fg: "white",
    bg: (item: unknown) => {
      const sidebarItem = getRenderedSidebarItemByElement(item);
      if (!sidebarItem) return undefined;
      return selectionKey(sidebarItem) === selectionKey(selectedItem) ? "brightblack" : undefined;
    },
  };
  sidebarStyle.selected = {
    fg: "white",
    bg: (item: unknown) => {
      const isSidebarFocused = screen.focused === sidebar;
      if (isSidebarFocused) return "blue";
      const sidebarItem = getRenderedSidebarItemByElement(item);
      if (!sidebarItem) return undefined;
      return selectionKey(sidebarItem) === selectionKey(selectedItem) ? "brightblack" : undefined;
    },
    bold: () => screen.focused === sidebar,
  };

  const header = blessed.box({
    parent: screen,
    label: " Workspace ",
    border: "line",
    tags: true,
    top: 0,
    left: "32%",
    width: "68%",
    height: 6,
    style: {
      border: { fg: "magenta" },
      label: { fg: "magenta" },
    },
  });

  const transcript = blessed.box({
    parent: screen,
    label: " Transcript ",
    border: "line",
    tags: true,
    top: 6,
    left: "32%",
    width: "68%",
    bottom: 3,
    keys: true,
    vi: true,
    mouse: true,
    scrollable: true,
    alwaysScroll: true,
    scrollbar: {
      ch: " ",
    },
    style: {
      border: { fg: "magenta" },
      label: { fg: "magenta" },
    },
  });

  const composer = blessed.textbox({
    parent: screen,
    label: " Message Sybil ",
    border: "line",
    tags: true,
    inputOnFocus: true,
    keys: true,
    mouse: true,
    top: undefined,
    bottom: 0,
    left: "32%",
    width: "68%",
    height: 3,
    style: {
      border: { fg: "magenta" },
      label: { fg: "magenta" },
      fg: "white",
    },
  });

  const renamePrompt = (blessed as any).prompt({
    parent: screen,
    label: " Rename chat ",
    border: "line",
    tags: true,
    keys: true,
    vi: true,
    mouse: true,
    top: "center",
    left: "center",
    width: "50%",
    height: "shrink",
    hidden: true,
    style: {
      border: { fg: "cyan" },
      label: { fg: "cyan" },
      fg: "white",
    },
  });

  const focusables = [sidebar, transcript, composer] as const;

  function getTranscriptViewportHeight() {
    const lpos = transcript.lpos;
    if (lpos) {
      return Math.max(1, lpos.yl - lpos.yi - Number(transcript.iheight ?? 0));
    }

    return Math.max(1, Number(transcript.height ?? 1) - Number(transcript.iheight ?? 0));
  }

  function isTranscriptNearBottom() {
    const viewportHeight = getTranscriptViewportHeight();
    const maxScroll = Math.max(0, transcript.getScrollHeight() - viewportHeight);
    if (maxScroll === 0) return true;
    return maxScroll - transcript.getScroll() <= 1;
  }

  function queueTranscriptScrollToBottomIfFollowing() {
    if (isTranscriptNearBottom()) {
      forceScrollToBottom = true;
    }
  }

  function scrollTranscriptByPage(direction: 1 | -1) {
    forceScrollToBottom = false;
    const pageSize = Math.max(1, getTranscriptViewportHeight() - 1);
    transcript.scroll(direction * pageSize);
    screen.render();
  }

  function withSuppressedSidebarSelectEvents(fn: () => void) {
    suppressedSidebarSelectEvents += 1;
    try {
      fn();
    } finally {
      suppressedSidebarSelectEvents -= 1;
    }
  }

  function getRenderedSidebarItemByElement(item: unknown) {
    const listItems = ((sidebar as any).items as unknown[] | undefined) ?? [];
    const index = listItems.indexOf(item);
    if (index < 0) return null;
    return renderedSidebarItems[index] ?? null;
  }

  function getSidebarItems() {
    return buildSidebarItems(workspaceItems);
  }

  function getSelectedChatSummary() {
    if (!selectedItem || selectedItem.kind !== "chat") return null;
    const selectedId = selectedItem.id;
    return chats.find((chat) => chat.id === selectedId) ?? null;
  }

  function getSelectedSearchSummary() {
    if (!selectedItem || selectedItem.kind !== "search") return null;
    const selectedId = selectedItem.id;
    return searches.find((search) => search.id === selectedId) ?? null;
  }

  function getIsSearchMode() {
    if (draftKind) return draftKind === "search";
    return selectedItem?.kind === "search";
  }

  function getDisplayMessages() {
    const canonicalMessages = selectedChat?.messages ?? [];
    if (!pendingChatState) return canonicalMessages;

    if (pendingChatState.chatId) {
      if (selectedItem?.kind === "chat" && selectedItem.id === pendingChatState.chatId) {
        return pendingChatState.messages;
      }
      return canonicalMessages;
    }

    if (getIsSearchMode()) return canonicalMessages;
    return pendingChatState.messages;
  }

  function syncModelForProvider() {
    const options = getModelOptions(modelCatalog, provider);
    model = pickProviderModel(options, providerModelPreferences[provider], model);
  }

  function updateProviderModelFromSelectedChat() {
    if (draftKind || selectedItem?.kind !== "chat") return;

    const detailSelection =
      selectedChat?.id === selectedItem.id && selectedChat.lastUsedProvider && selectedChat.lastUsedModel?.trim()
        ? {
            provider: selectedChat.lastUsedProvider,
            model: selectedChat.lastUsedModel.trim(),
          }
        : null;

    const summary = getSelectedChatSummary();
    const summarySelection =
      summary?.lastUsedProvider && summary.lastUsedModel?.trim()
        ? {
            provider: summary.lastUsedProvider,
            model: summary.lastUsedModel.trim(),
          }
        : null;

    const next = detailSelection ?? summarySelection;
    if (!next) return;
    provider = next.provider;
    model = next.model;
  }

  function getSelectedTitle() {
    if (draftKind === "chat") return "New chat";
    if (draftKind === "search") return "New search";
    if (!selectedItem) return "Sybil";

    if (selectedItem.kind === "chat") {
      if (selectedChat) return getChatTitle(selectedChat, selectedChat.messages);
      const summary = getSelectedChatSummary();
      return summary ? getChatTitle(summary) : "New chat";
    }

    if (selectedSearch) return getSearchTitle(selectedSearch);
    const summary = getSelectedSearchSummary();
    return summary ? getSearchTitle(summary) : "New search";
  }

  function renderSidebar() {
    const items = getSidebarItems();
    const showingEmptyState = items.length === 0 && !isLoadingCollections;
    renderedSidebarItems = showingEmptyState ? [] : items;

    const lines = showingEmptyState
      ? ["No chats/searches yet. Press n or /. "]
      : items.map((item) => {
          const kind = item.kind === "chat" ? "C" : "S";
          const star = item.starred ? "{yellow-fg}★{/yellow-fg} " : "  ";
          const title = truncate(item.title, 36);
          const initiatedLabel =
            item.kind === "chat" && item.initiatedModel
              ? ` | ${getProviderLabel(item.initiatedProvider)} ${truncate(item.initiatedModel, 16)}`
              : "";
          return `${star}${kind} ${title} {gray-fg}${formatDate(item.updatedAt)}${escapeTags(initiatedLabel)}{/gray-fg}`;
        });

    const linesChanged =
      lines.length !== renderedSidebarLines.length || lines.some((line, index) => line !== renderedSidebarLines[index]);
    if (linesChanged) {
      withSuppressedSidebarSelectEvents(() => {
        sidebar.setItems(lines);
      });
      renderedSidebarLines = lines.slice();
      renderedSidebarSelectedIndex = -1;
    }

    if (showingEmptyState) {
      highlightedSidebarKey = null;
      if (renderedSidebarSelectedIndex !== 0) {
        withSuppressedSidebarSelectEvents(() => {
          sidebar.select(0);
        });
        renderedSidebarSelectedIndex = 0;
      }
      sidebar.setLabel(isLoadingCollections ? " Conversations (loading...) " : " Conversations ");
      return;
    }

    const activeKey = selectionKey(selectedItem);
    const activeIndex = activeKey ? items.findIndex((item) => selectionKey(item) === activeKey) : -1;
    const highlightedIndex = highlightedSidebarKey ? items.findIndex((item) => selectionKey(item) === highlightedSidebarKey) : -1;
    const selectedIndex = highlightedIndex >= 0 ? highlightedIndex : activeIndex >= 0 ? activeIndex : items.length > 0 ? 0 : -1;

    if (selectedIndex >= 0 && renderedSidebarSelectedIndex !== selectedIndex) {
      withSuppressedSidebarSelectEvents(() => {
        sidebar.select(selectedIndex);
      });
      renderedSidebarSelectedIndex = selectedIndex;
    }
    if (selectedIndex >= 0) {
      const highlightedItem = items[selectedIndex];
      highlightedSidebarKey = highlightedItem ? selectionKey(highlightedItem) : null;
    } else {
      highlightedSidebarKey = null;
    }

    sidebar.setLabel(isLoadingCollections ? " Conversations (loading...) " : " Conversations ");
  }

  function buildChatTranscriptContent() {
    const isSendingActiveChat =
      isSending &&
      !getIsSearchMode() &&
      !!pendingChatState &&
      !!pendingChatState.chatId &&
      selectedItem?.kind === "chat" &&
      selectedItem.id === pendingChatState.chatId;

    const messages = getDisplayMessages();
    const parts: string[] = [];

    if (isLoadingSelection && messages.length === 0) {
      parts.push("{gray-fg}Loading messages...{/gray-fg}");
    }

    if (!isLoadingSelection && messages.length === 0) {
      parts.push("{gray-fg}No messages yet. Send a prompt to start.{/gray-fg}");
    }

    for (const message of messages) {
      const toolMeta = asToolLogMetadata(message.metadata);
      if (message.role === "tool" && toolMeta) {
        const prefix =
          toolMeta.status === "failed"
            ? "{red-fg}[tool failed]{/red-fg}"
            : toolMeta.status === "initiated"
              ? "{yellow-fg}[tool running]{/yellow-fg}"
              : "{cyan-fg}[tool]{/cyan-fg}";
        const summary = toolMeta.summary?.trim() || message.content.trim() || "Tool call executed.";
        parts.push(`${prefix} ${escapeTags(summary)}`);
        continue;
      }

      if (message.role === "user") {
        parts.push(`{bold}{magenta-fg}You{/magenta-fg}{/bold}\n${escapeTags(message.content)}`);
        continue;
      }

      if (message.role === "assistant") {
        const body = message.content.trim().length
          ? escapeTags(message.content)
          : isSendingActiveChat && message.id.startsWith("temp-assistant-")
            ? "{gray-fg}Sybil is typing...{/gray-fg}"
            : "";
        parts.push(`{bold}{cyan-fg}Sybil{/cyan-fg}{/bold}\n${body}`);
        continue;
      }

      parts.push(`{bold}${escapeTags(message.role)}{/bold}\n${escapeTags(message.content)}`);
    }

    if (isSendingActiveChat && !messages.some((message) => message.id.startsWith("temp-assistant-"))) {
      parts.push("{bold}{cyan-fg}Sybil{/cyan-fg}{/bold}\n{gray-fg}Sybil is typing...{/gray-fg}");
    }

    return parts.join("\n\n");
  }

  function buildSearchContent() {
    const parts: string[] = [];
    const search = selectedSearch;
    const isSearchRunning = isSending && getIsSearchMode();

    if (search?.query?.trim()) {
      parts.push(`{bold}Results for{/bold} ${escapeTags(search.query.trim())}`);
      parts.push(
        `{gray-fg}${search.results.length} result${search.results.length === 1 ? "" : "s"}${search.latencyMs ? ` • ${search.latencyMs}ms` : ""}{/gray-fg}`
      );
    }

    if (isSearchRunning || search?.answerText || search?.answerError) {
      parts.push("{bold}{magenta-fg}Answer{/magenta-fg}{/bold}");
      if (isSearchRunning && !search?.answerText) {
        parts.push("{gray-fg}Generating answer...{/gray-fg}");
      } else if (search?.answerText) {
        parts.push(escapeTags(search.answerText));
      }

      if (search?.answerError) {
        parts.push(`{red-fg}${escapeTags(search.answerError)}{/red-fg}`);
      }

      const citations = (search?.answerCitations ?? []).filter((citation) => citation.url || citation.id);
      if (citations.length > 0) {
        const rendered = citations.slice(0, 8).map((citation, index) => {
          const href = citation.url || citation.id || "";
          const label = citation.title?.trim() || formatHost(href);
          return `[${index + 1}] ${escapeTags(label)} - ${escapeTags(href)}`;
        });
        parts.push("{gray-fg}Citations{/gray-fg}\n" + rendered.join("\n"));
      }
    }

    if ((isLoadingSelection || isSearchRunning) && !search?.results.length) {
      parts.push(`{gray-fg}${isSearchRunning ? "Searching Exa..." : "Loading search..."}{/gray-fg}`);
    }

    if (!isLoadingSelection && !isSearchRunning && search?.query && search.results.length === 0) {
      parts.push("{gray-fg}No results found.{/gray-fg}");
    }

    if (search?.results.length) {
      parts.push("{bold}Results{/bold}");
      for (let index = 0; index < search.results.length; index += 1) {
        const result = search.results[index];
        if (!result) continue;
        parts.push(
          `${index + 1}. {cyan-fg}${escapeTags(result.title || result.url)}{/cyan-fg}\n` +
            `{gray-fg}${escapeTags(formatHost(result.url))}{/gray-fg}\n` +
            `${escapeTags(result.url)}`
        );
      }
    }

    if (search?.error) {
      parts.push(`{red-fg}${escapeTags(search.error)}{/red-fg}`);
    }

    if (parts.length === 0) {
      parts.push("{gray-fg}Run a search to see results and the answer panel.{/gray-fg}");
    }

    return parts.join("\n\n");
  }

  function renderHeader() {
    const isSearchMode = getIsSearchMode();
    const providerModelOptions = getModelOptions(modelCatalog, provider);
    const modeLabel = authMode ? ` (${authMode === "open" ? "open mode" : "token mode"})` : "";
    const sendState = isSending ? "{yellow-fg}Sending...{/yellow-fg}" : "{green-fg}Ready{/green-fg}";
    const top = `{bold}${escapeTags(getSelectedTitle())}{/bold} {gray-fg}- Sybil TUI${modeLabel}${isSearchMode ? " • Exa Search" : ""}{/gray-fg}`;

    let controls =
      "{gray-fg}Controls:{/gray-fg} [tab] focus  [esc] command mode  [↑/↓] highlight  [enter] send/select  [n] new chat  [/] new search  [s] star  [r] rename  [d] delete  [C-r] refresh  [q] quit";
    if (!isSearchMode) {
      controls += `\n{gray-fg}Model:{/gray-fg} provider {cyan-fg}${provider}{/cyan-fg} [p]  model {cyan-fg}${escapeTags(model)}{/cyan-fg} [m]`;
      controls += providerModelOptions.length === 0 ? " {red-fg}(no models){/red-fg}" : "";
    }

    const status = errorMessage ? `{red-fg}${escapeTags(errorMessage)}{/red-fg}` : sendState;
    header.setContent(`${top}\n${controls}\n${status}`);
  }

  function applyPaneFocusStyles() {
    const isSidebarFocused = screen.focused === sidebar;
    const isTranscriptFocused = screen.focused === transcript;
    const isComposerFocused = screen.focused === composer;
    const isWorkspaceFocused = isTranscriptFocused || isComposerFocused;

    (sidebar.style as any).border = { fg: isSidebarFocused ? "cyan" : "magenta" };
    (sidebar.style as any).label = { fg: isSidebarFocused ? "cyan" : "magenta" };
    (header.style as any).border = { fg: isWorkspaceFocused ? "cyan" : "magenta" };
    (header.style as any).label = { fg: isWorkspaceFocused ? "cyan" : "magenta" };
    (transcript.style as any).border = { fg: isTranscriptFocused ? "cyan" : "magenta" };
    (transcript.style as any).label = { fg: isTranscriptFocused ? "cyan" : "magenta" };
    (composer.style as any).border = { fg: isSending ? "yellow" : isComposerFocused ? "cyan" : "magenta" };
    (composer.style as any).label = { fg: isComposerFocused ? "cyan" : "magenta" };
  }

  function updateUI() {
    renderSidebar();
    renderHeader();

    const content = getIsSearchMode() ? buildSearchContent() : buildChatTranscriptContent();
    transcript.setContent(content);

    if (forceScrollToBottom) {
      transcript.setScrollPerc(100);
      forceScrollToBottom = false;
    }

    const composerLabel = getIsSearchMode() ? " Search the web " : " Message Sybil ";
    composer.setLabel(composerLabel);
    applyPaneFocusStyles();

    screen.render();
  }

  function setError(message: string | null) {
    errorMessage = message;
    updateUI();
  }

  function resetWorkspaceState() {
    chats = [];
    searches = [];
    workspaceItems = [];
    selectedItem = null;
    selectedChat = null;
    selectedSearch = null;
    draftKind = null;
    pendingChatState = null;
    isLoadingCollections = false;
    isLoadingSelection = false;
    isSending = false;
    forceScrollToBottom = true;
    renderedSidebarSelectedIndex = -1;
    highlightedSidebarKey = null;
    renderedSidebarItems = [];
    renderedSidebarLines = [];
  }

  function hasItem(items: SidebarItem[], selection: SidebarSelection | null) {
    if (!selection) return false;
    return items.some((item) => item.kind === selection.kind && item.id === selection.id);
  }

  async function loadSelection(selection: SidebarSelection | null, options?: { scrollToBottom?: boolean | undefined }) {
    if (!selection) {
      selectedChat = null;
      selectedSearch = null;
      updateUI();
      return;
    }

    const requestedSelectionKey = selectionKey(selection);
    isLoadingSelection = true;
    updateUI();

    try {
      if (selection.kind === "chat") {
        const chat = await api.getChat(selection.id);
        if (selectionKey(selectedItem) !== requestedSelectionKey) return;
        selectedChat = chat;
        selectedSearch = null;
      } else {
        const search = await api.getSearch(selection.id);
        if (selectionKey(selectedItem) !== requestedSelectionKey) return;
        selectedSearch = search;
        selectedChat = null;
      }

      updateProviderModelFromSelectedChat();
    } finally {
      if (selectionKey(selectedItem) === requestedSelectionKey) {
        isLoadingSelection = false;
      }
      if (options?.scrollToBottom) {
        forceScrollToBottom = true;
      }
      updateUI();
    }
  }

  async function refreshCollections(options?: {
    preferredSelection?: SidebarSelection;
    loadSelection?: boolean;
    scrollToBottomOnLoad?: boolean | undefined;
  }) {
    isLoadingCollections = true;
    updateUI();

    try {
      const nextWorkspaceItems = await api.listWorkspaceItems();
      const { chats: nextChats, searches: nextSearches } = splitWorkspaceItems(nextWorkspaceItems);
      workspaceItems = nextWorkspaceItems;
      chats = nextChats;
      searches = nextSearches;

      const nextItems = buildSidebarItems(nextWorkspaceItems);
      if (options?.preferredSelection && hasItem(nextItems, options.preferredSelection)) {
        selectedItem = options.preferredSelection;
        draftKind = null;
      } else if (selectedItem && !hasItem(nextItems, selectedItem)) {
        selectedItem = nextItems[0] ? { kind: nextItems[0].kind, id: nextItems[0].id } : null;
      } else if (!selectedItem && !draftKind) {
        selectedItem = nextItems[0] ? { kind: nextItems[0].kind, id: nextItems[0].id } : null;
      }

      if (!selectedItem && !draftKind) {
        selectedChat = null;
        selectedSearch = null;
      }
    } finally {
      isLoadingCollections = false;
      updateUI();
    }

    if (options?.loadSelection) {
      await loadSelection(selectedItem, { scrollToBottom: options?.scrollToBottomOnLoad });
    }
  }

  async function refreshModels() {
    const data = await api.listModels();
    modelCatalog = data.providers;
    syncModelForProvider();
    updateUI();
  }

  function focusComposer() {
    composer.focus();
    composer.readInput();
  }

  function shouldIgnoreGlobalShortcut() {
    return isRenamePromptOpen || isTextInputFocused(screen, composer);
  }

  function promptForChatTitle(currentTitle: string) {
    isRenamePromptOpen = true;
    updateUI();
    return new Promise<string | null>((resolve) => {
      renamePrompt.input("Title:", currentTitle, (err: Error | null, value: string | null) => {
        isRenamePromptOpen = false;
        renamePrompt.hide();
        screen.render();
        if (err || value === null || value === undefined) {
          resolve(null);
          return;
        }
        resolve(value);
      });
    });
  }

  function cycleFocus(step: 1 | -1) {
    const focused = screen.focused;
    const currentIndex = focusables.findIndex((node) => node === focused);
    const next = focusables[(currentIndex + step + focusables.length) % focusables.length] ?? focusables[0];
    next.focus();
    if (next === composer) {
      composer.readInput();
    }
    updateUI();
  }

  function enterCommandMode() {
    if (!isTextInputFocused(screen, composer)) return;
    sidebar.focus();
    updateUI();
  }

  async function selectSidebarIndex(index: number) {
    const item = getSidebarItems()[index];
    if (!item || item.id === "") return;
    highlightedSidebarKey = selectionKey(item) ?? null;
    renderedSidebarSelectedIndex = index;
    if (selectedItem?.kind === item.kind && selectedItem.id === item.id && !draftKind) {
      updateUI();
      return;
    }

    draftKind = null;
    selectedItem = { kind: item.kind, id: item.id };
    pendingChatState = null;
    errorMessage = null;
    forceScrollToBottom = true;
    updateUI();
    await loadSelection(selectedItem, { scrollToBottom: true });
  }

  function handleCreateChat() {
    draftKind = "chat";
    selectedItem = null;
    selectedChat = null;
    selectedSearch = null;
    pendingChatState = null;
    errorMessage = null;
    forceScrollToBottom = true;
    updateUI();
    focusComposer();
  }

  function handleCreateSearch() {
    draftKind = "search";
    selectedItem = null;
    selectedChat = null;
    selectedSearch = null;
    pendingChatState = null;
    errorMessage = null;
    forceScrollToBottom = true;
    updateUI();
    focusComposer();
  }

  async function maybeSuggestTitle(chat: ChatDetail, content: string) {
    const needsGeneratedTitle = chat.titleGenerationPending || !chat.title?.trim();
    if (!needsGeneratedTitle || pendingTitleGeneration.has(chat.id)) return;

    const chatId = chat.id;
    pendingTitleGeneration.add(chatId);
    try {
      const updated = await api.suggestChatTitle({ chatId, content });
      chats = chats.map((chat) => (chat.id === updated.id ? updated : chat));
      workspaceItems = workspaceItems.map((item) => (item.type === "chat" && item.id === updated.id ? chatWorkspaceItem(updated) : item));
      if (selectedChat?.id === updated.id) {
        selectedChat = {
          ...selectedChat,
          title: updated.title,
          parentChatId: updated.parentChatId,
          titleGenerationPending: updated.titleGenerationPending,
          updatedAt: updated.updatedAt,
          starred: updated.starred,
          starredAt: updated.starredAt,
          initiatedProvider: updated.initiatedProvider,
          initiatedModel: updated.initiatedModel,
          lastUsedProvider: updated.lastUsedProvider,
          lastUsedModel: updated.lastUsedModel,
        };
      }
      updateUI();
    } catch {
      // ignored intentionally so chat flow is not interrupted
    } finally {
      pendingTitleGeneration.delete(chatId);
    }
  }

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

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

    pendingChatState = {
      chatId: selectedItem?.kind === "chat" ? selectedItem.id : null,
      messages: (selectedChat?.messages ?? []).concat(optimisticUserMessage, optimisticAssistantMessage),
    };
    forceScrollToBottom = true;
    updateUI();

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

    if (!chatId) {
      const chat = await api.createChat();
      chatId = chat.id;
      draftKind = null;
      chats = [chat, ...chats.filter((existing) => existing.id !== chat.id)];
      workspaceItems = upsertWorkspaceItem(workspaceItems, chatWorkspaceItem(chat));
      selectedItem = { kind: "chat", id: chat.id };
      pendingChatState = pendingChatState ? { ...pendingChatState, chatId } : pendingChatState;
      selectedChat = {
        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,
        messages: [],
      };
      selectedSearch = null;
      forceScrollToBottom = true;
      updateUI();
    }

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

    let baseChat = selectedChat;
    if (!baseChat || baseChat.id !== chatId) {
      baseChat = await api.getChat(chatId);
    }

    void maybeSuggestTitle(baseChat, content);

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

    const selectedModel = model.trim();
    if (!selectedModel) {
      throw new Error("No model available for selected provider");
    }

    let streamErrorMessage: string | null = null;

    await api.runCompletionStream(
      {
        chatId,
        provider,
        model: selectedModel,
        messages: requestMessages,
      },
      {
        onMeta: (payload) => {
          if (payload.chatId !== chatId) return;
          pendingChatState = pendingChatState ? { ...pendingChatState, chatId: payload.chatId } : pendingChatState;
          updateUI();
        },
        onToolCall: (payload) => {
          if (!pendingChatState) return;
          pendingChatState = { ...pendingChatState, messages: upsertOptimisticToolMessage(pendingChatState.messages, payload) };

          queueTranscriptScrollToBottomIfFollowing();
          updateUI();
        },
        onDelta: (payload) => {
          if (!payload.text || !pendingChatState) return;

          let updated = false;
          const nextMessages = pendingChatState.messages.map((message, index, all) => {
            const target = index === all.length - 1 && message.id.startsWith("temp-assistant-");
            if (!target) return message;
            updated = true;
            return { ...message, content: message.content + payload.text };
          });

          if (!updated) return;
          pendingChatState = { ...pendingChatState, messages: nextMessages };
          queueTranscriptScrollToBottomIfFollowing();
          updateUI();
        },
        onDone: (payload) => {
          if (!pendingChatState) return;

          let updated = false;
          const nextMessages = pendingChatState.messages.map((message, index, all) => {
            const target = index === all.length - 1 && message.id.startsWith("temp-assistant-");
            if (!target) return message;
            updated = true;
            return { ...message, content: payload.text };
          });

          if (!updated) return;
          pendingChatState = { ...pendingChatState, messages: nextMessages };
          queueTranscriptScrollToBottomIfFollowing();
          updateUI();
        },
        onError: (payload) => {
          streamErrorMessage = payload.message;
        },
      }
    );

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

    const shouldFollowTranscript = isTranscriptNearBottom();

    await refreshCollections({ preferredSelection: { kind: "chat", id: chatId }, loadSelection: false });

    const currentSelection = selectedItem;
    if (currentSelection?.kind === "chat" && currentSelection.id === chatId) {
      await loadSelection(currentSelection, { scrollToBottom: shouldFollowTranscript });
    }

    pendingChatState = null;
    forceScrollToBottom = shouldFollowTranscript;
    updateUI();
  }

  async function handleSendSearch(query: string) {
    const runId = ++searchRunCounter;
    searchRunController?.abort();
    const abortController = new AbortController();
    searchRunController = abortController;

    let searchId = draftKind === "search" ? null : selectedItem?.kind === "search" ? selectedItem.id : null;

    if (!searchId) {
      const search = await api.createSearch({
        query,
        title: query.slice(0, 80),
      });
      searchId = search.id;
      draftKind = null;
      selectedItem = { kind: "search", id: searchId };
      searches = [search, ...searches.filter((existing) => existing.id !== search.id)];
      workspaceItems = upsertWorkspaceItem(workspaceItems, searchWorkspaceItem(search));
      selectedChat = null;
      forceScrollToBottom = true;
      updateUI();
    }

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

    const nowIso = new Date().toISOString();
    if (!selectedSearch || selectedSearch.id !== searchId) {
      selectedSearch = {
        id: searchId,
        title: query.slice(0, 80),
        query,
        createdAt: nowIso,
        updatedAt: nowIso,
        starred: false,
        starredAt: null,
        requestId: null,
        latencyMs: null,
        error: null,
        answerText: null,
        answerRequestId: null,
        answerCitations: null,
        answerError: null,
        results: [],
      };
    } else {
      selectedSearch = {
        ...selectedSearch,
        title: query.slice(0, 80),
        query,
        error: null,
        latencyMs: null,
        answerText: null,
        answerRequestId: null,
        answerCitations: null,
        answerError: null,
        results: [],
      };
    }

    forceScrollToBottom = true;
    updateUI();

    try {
      await api.runSearchStream(
        searchId,
        {
          query,
          title: query.slice(0, 80),
          type: "auto",
          numResults: config.searchNumResults,
        },
        {
          onSearchResults: (payload) => {
            if (runId !== searchRunCounter) return;
            if (!selectedSearch || selectedSearch.id !== searchId) return;
            selectedSearch = {
              ...selectedSearch,
              requestId: payload.requestId ?? selectedSearch.requestId,
              error: null,
              results: payload.results,
            };
            queueTranscriptScrollToBottomIfFollowing();
            updateUI();
          },
          onSearchError: (payload) => {
            if (runId !== searchRunCounter) return;
            if (!selectedSearch || selectedSearch.id !== searchId) return;
            selectedSearch = { ...selectedSearch, error: payload.error };
            updateUI();
          },
          onAnswer: (payload) => {
            if (runId !== searchRunCounter) return;
            if (!selectedSearch || selectedSearch.id !== searchId) return;
            selectedSearch = {
              ...selectedSearch,
              answerText: payload.answerText,
              answerRequestId: payload.answerRequestId,
              answerCitations: payload.answerCitations,
              answerError: null,
            };
            queueTranscriptScrollToBottomIfFollowing();
            updateUI();
          },
          onAnswerError: (payload) => {
            if (runId !== searchRunCounter) return;
            if (!selectedSearch || selectedSearch.id !== searchId) return;
            selectedSearch = { ...selectedSearch, answerError: payload.error };
            updateUI();
          },
          onDone: (payload) => {
            if (runId !== searchRunCounter) return;
            selectedSearch = payload.search;
            selectedChat = null;
            queueTranscriptScrollToBottomIfFollowing();
            updateUI();
          },
          onError: (payload) => {
            if (runId !== searchRunCounter) return;
            setError(payload.message);
          },
        },
        { signal: abortController.signal }
      );
    } catch (error) {
      if (abortController.signal.aborted) return;
      throw error;
    } finally {
      if (runId === searchRunCounter) {
        searchRunController = null;
      }
    }

    const shouldFollowTranscript = isTranscriptNearBottom();

    await refreshCollections({ preferredSelection: { kind: "search", id: searchId }, loadSelection: false });

    const currentSelection = selectedItem;
    if (currentSelection?.kind === "search" && currentSelection.id === searchId) {
      await loadSelection(currentSelection, { scrollToBottom: shouldFollowTranscript });
    }
  }

  async function handleSend(content: string) {
    const trimmed = content.trim();
    if (!trimmed || isSending) return;

    const isSearchMode = getIsSearchMode();
    setError(null);
    isSending = true;
    updateUI();

    try {
      if (isSearchMode) {
        await handleSendSearch(trimmed);
      } else {
        await handleSendChat(trimmed);
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      setError(message);

      if (!isSearchMode) {
        pendingChatState = null;
      }

      if (selectedItem) {
        await loadSelection(selectedItem, { scrollToBottom: true });
      }
    } finally {
      isSending = false;
      updateUI();
      focusComposer();
    }
  }

  async function handleDeleteSelection() {
    if (!selectedItem || isSending) return;

    const target = selectedItem;
    setError(null);

    if (target.kind === "chat") {
      await api.deleteChat(target.id);
    } else {
      await api.deleteSearch(target.id);
    }

    if (target.kind === "chat" && selectedChat?.id === target.id) {
      selectedChat = null;
    }
    if (target.kind === "search" && selectedSearch?.id === target.id) {
      selectedSearch = null;
    }

    await refreshCollections({ loadSelection: true, scrollToBottomOnLoad: true });
  }

  async function handleRenameSelection() {
    if (!selectedItem || selectedItem.kind !== "chat") return;

    const chatId = selectedItem.id;
    const summary = chats.find((chat) => chat.id === chatId);
    const currentTitle = selectedChat?.id === chatId ? getChatTitle(selectedChat, selectedChat.messages) : summary ? getChatTitle(summary) : "New chat";
    const value = await promptForChatTitle(currentTitle);
    const title = value?.trim();
    if (!title) {
      updateUI();
      return;
    }

    setError(null);
    const updated = await api.updateChatTitle(chatId, title);
    chats = [updated, ...chats.filter((chat) => chat.id !== updated.id)];
    workspaceItems = upsertWorkspaceItem(workspaceItems, chatWorkspaceItem(updated));
    if (selectedChat?.id === updated.id) {
      selectedChat = {
        ...selectedChat,
        title: updated.title,
        parentChatId: updated.parentChatId,
        titleGenerationPending: updated.titleGenerationPending,
        updatedAt: updated.updatedAt,
        initiatedProvider: updated.initiatedProvider,
        initiatedModel: updated.initiatedModel,
        lastUsedProvider: updated.lastUsedProvider,
        lastUsedModel: updated.lastUsedModel,
      };
    }
    updateUI();
  }

  async function handleToggleStarSelection() {
    if (!selectedItem) return;

    setError(null);

    if (selectedItem.kind === "chat") {
      const selectedSummary = chats.find((chat) => chat.id === selectedItem?.id);
      const selectedParentChatId = selectedChat?.id === selectedItem.id
        ? selectedChat.parentChatId
        : selectedSummary?.parentChatId;
      const rootChatId = selectedParentChatId ?? selectedItem.id;
      const rootSummary = chats.find((chat) => chat.id === rootChatId);
      const updated = await api.updateChatStar(rootChatId, !rootSummary?.starred);
      chats = chats.map((chat) => (chat.id === updated.id ? updated : chat));
      if (!chats.some((chat) => chat.id === updated.id)) chats = [updated, ...chats];
      workspaceItems = workspaceItems.map((item) => (item.type === "chat" && item.id === updated.id ? chatWorkspaceItem(updated) : item));
      if (!workspaceItems.some((item) => item.type === "chat" && item.id === updated.id)) {
        workspaceItems = [chatWorkspaceItem(updated), ...workspaceItems];
      }
      if (selectedChat?.id === updated.id) {
        selectedChat = {
          ...selectedChat,
          title: updated.title,
          parentChatId: updated.parentChatId,
          titleGenerationPending: updated.titleGenerationPending,
          updatedAt: updated.updatedAt,
          starred: updated.starred,
          starredAt: updated.starredAt,
          initiatedProvider: updated.initiatedProvider,
          initiatedModel: updated.initiatedModel,
          lastUsedProvider: updated.lastUsedProvider,
          lastUsedModel: updated.lastUsedModel,
        };
      }
    } else {
      const currentItem = getSidebarItems().find((item) => item.kind === "search" && item.id === selectedItem?.id);
      const updated = await api.updateSearchStar(selectedItem.id, !currentItem?.starred);
      searches = searches.map((search) => (search.id === updated.id ? updated : search));
      if (!searches.some((search) => search.id === updated.id)) searches = [updated, ...searches];
      workspaceItems = workspaceItems.map((item) => (item.type === "search" && item.id === updated.id ? searchWorkspaceItem(updated) : item));
      if (!workspaceItems.some((item) => item.type === "search" && item.id === updated.id)) {
        workspaceItems = [searchWorkspaceItem(updated), ...workspaceItems];
      }
      if (selectedSearch?.id === updated.id) {
        selectedSearch = {
          ...selectedSearch,
          title: updated.title,
          query: updated.query,
          updatedAt: updated.updatedAt,
          starred: updated.starred,
          starredAt: updated.starredAt,
        };
      }
    }

    updateUI();
  }

  function cycleProvider() {
    const visibleProviders = getVisibleProviders(modelCatalog);
    const cycleProviders = visibleProviders.length ? visibleProviders : BASE_PROVIDERS;
    const currentIndex = Math.max(0, cycleProviders.indexOf(provider));
    const nextProvider: Provider = cycleProviders[(currentIndex + 1) % cycleProviders.length] ?? "openai";
    provider = nextProvider;
    syncModelForProvider();
    updateUI();
  }

  function cycleModel() {
    const options = getModelOptions(modelCatalog, provider);
    if (!options.length) {
      setError("No models available for the selected provider");
      return;
    }

    const currentIndex = Math.max(0, options.indexOf(model));
    const nextModel = options[(currentIndex + 1) % options.length] ?? options[0];
    if (!nextModel) {
      setError("No models available for the selected provider");
      return;
    }
    model = nextModel;
    providerModelPreferences[provider] = nextModel;
    updateUI();
  }

  async function runAction(action: () => Promise<void> | void) {
    try {
      await action();
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      setError(message);
    }
  }

  sidebar.on("focus", () => {
    updateUI();
  });
  transcript.on("focus", () => {
    updateUI();
  });
  composer.on("focus", () => {
    updateUI();
  });

  transcript.key(["pageup"], () => {
    scrollTranscriptByPage(-1);
  });

  transcript.key(["pagedown"], () => {
    scrollTranscriptByPage(1);
  });

  sidebar.on("select item", (_item, index) => {
    if (suppressedSidebarSelectEvents > 0) return;
    const highlightedItem = getSidebarItems()[index];
    highlightedSidebarKey = highlightedItem ? selectionKey(highlightedItem) : null;
    renderedSidebarSelectedIndex = index;
    updateUI();
  });

  sidebar.on("action", (_item, index) => {
    if (typeof index !== "number") return;
    void runAction(async () => {
      await selectSidebarIndex(index);
    });
  });

  composer.on("submit", (value) => {
    const text = typeof value === "string" ? value : "";
    composer.clearValue();
    updateUI();
    void runAction(async () => {
      await handleSend(text);
    });
  });

  screen.key(["C-c"], () => {
    screen.destroy();
    process.exit(0);
  });

  screen.key(["q"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    screen.destroy();
    process.exit(0);
  });

  screen.key(["tab"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    cycleFocus(1);
  });

  screen.key(["S-tab", "backtab"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    cycleFocus(-1);
  });

  composer.key(["tab"], () => {
    cycleFocus(1);
  });

  composer.key(["S-tab", "backtab"], () => {
    cycleFocus(-1);
  });

  composer.key(["escape"], () => {
    enterCommandMode();
  });

  screen.key(["n"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    handleCreateChat();
  });

  screen.key(["/"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    handleCreateSearch();
  });

  screen.key(["d"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    void runAction(async () => {
      await handleDeleteSelection();
    });
  });

  screen.key(["s"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    void runAction(async () => {
      await handleToggleStarSelection();
    });
  });

  screen.key(["p"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    if (getIsSearchMode() || isSending) return;
    cycleProvider();
  });

  screen.key(["m"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    if (getIsSearchMode() || isSending) return;
    cycleModel();
  });

  screen.key(["r"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    void runAction(async () => {
      await handleRenameSelection();
    });
  });

  screen.key(["C-r"], () => {
    if (shouldIgnoreGlobalShortcut()) return;
    void runAction(async () => {
      await refreshCollections({ loadSelection: true });
      await refreshModels();
    });
  });

  process.on("SIGINT", () => {
    screen.destroy();
    process.exit(0);
  });

  process.on("SIGTERM", () => {
    screen.destroy();
    process.exit(0);
  });

  updateUI();

  try {
    const session = await api.verifySession();
    authMode = session.mode;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    resetWorkspaceState();
    updateUI();

    screen.destroy();

    if (message.includes("bearer token")) {
      const tokenHint =
        "Set SYBIL_TUI_ADMIN_TOKEN (or SYBIL_ADMIN_TOKEN) and rerun. Example: SYBIL_TUI_ADMIN_TOKEN=... npm run dev";
      console.error(`Authentication failed: ${message}\n${tokenHint}`);
    } else {
      console.error(`Failed to connect to Sybil API at ${config.apiBaseUrl}: ${message}`);
    }
    process.exit(1);
  }

  await runAction(async () => {
    await Promise.all([refreshCollections({ loadSelection: true, scrollToBottomOnLoad: true }), refreshModels()]);
  });

  focusComposer();
  updateUI();
}

void main().catch((error) => {
  const message = error instanceof Error ? error.message : String(error);
  console.error(message);
  process.exit(1);
});
