import { useState, useEffect, useRef, useCallback } from 'react';
import { useChatWebSocket } from 'hooks/useChatWebSocket';
import { useClient } from 'hooks/useClient';
import { useSettings } from 'state/useSettings';
import type { ChatMessage, ServerMessage, TaskInfo, Message } from '../apps/Chat/types';
import { spliceRunningTasks } from '../apps/Chat/running-tasks';
import { randomId } from 'helpers/random-id';

const SAVE_DEBOUNCE_MS = 1000;
const OLDER_PAGE_SIZE = 20; // messages fetched per scroll-up (matches the initial tail window)

type ResourceChatStorage = {
  load: () => Promise<{ sessionId: string | null; messages: ChatMessage[] }>;
  save: (sessionId: string, messages: ChatMessage[]) => Promise<void>;
};

type UsePiChatOptions = {
  replaceUrl?: boolean;
  storage?: ResourceChatStorage;
  resourceChatDir?: string;
  taskInfo?: TaskInfo;
  projectScoped?: boolean;
  context?: string;
  contextId?: string;
  resumeSummary?: string;
  resumeSessionId?: string;
  initialMessages?: ChatMessage[];
  // When set, `initialMessages` is only the tail of a long transcript; scroll-up pages older ones in.
  paginate?: { sessionId: string; total: number; initialOffset: number };
  onTurnComplete?: (hadToolCalls: boolean) => void;
};

/**
 * Point the address bar at Claude's transcript uuid — the only id `/chat/sessions/:id` can resolve, and
 * the only one that survives a refresh.
 *
 * The permalink is bare: no group, and `?cwd=` is stripped rather than carried. The transcript records
 * its own cwd and the server resolves it from the id, so naming the group again could only ever
 * contradict it — which is what a hand-edited or stale `?cwd=` used to do. Anything else in the query
 * string is left alone.
 */
function writePermalink(claudeSessionId: string): void {
  const params = new URLSearchParams(window.location.search);
  params.delete('cwd');
  const search = params.toString();
  window.history.replaceState(null, '', `/chat/${claudeSessionId}${search ? `?${search}` : ''}`);
}

export function useChat(initialSessionId?: string, initialModel?: string | null, options?: UsePiChatOptions) {
  const {
    replaceUrl = true,
    storage,
    resourceChatDir,
    taskInfo,
    projectScoped,
    context,
    contextId,
    resumeSummary: initialResumeSummary,
    resumeSessionId,
    initialMessages: preloadedMessages,
    paginate,
    onTurnComplete,
  } = options ?? {};
  const [messages, setMessages] = useState<ChatMessage[]>(preloadedMessages ?? []);
  /**
   * How many messages at the FRONT of the list came from the server rather than from this sitting.
   *
   * Folding used to be a pure derivation over the array, which is what made a reload render identically to
   * a live session. That property is deliberately given up here: a turn now folds because it is *history*,
   * not because a newer turn exists. Watching your own conversation collapse under you as you typed the
   * next message was the thing worth losing it for.
   *
   * A count rather than a set of ids because everything historical is contiguous and at the front: the
   * preload seeds it, paging older messages prepends to it, and everything live is appended past it. The
   * one case that replaces rather than appends is a resume, where the whole transcript is history.
   */
  const [historicalCount, setHistoricalCount] = useState(preloadedMessages?.length ?? 0);
  // Reverse pagination: absolute index of the oldest loaded message (0 = we have the whole transcript).
  const [oldestOffset, setOldestOffset] = useState(paginate?.initialOffset ?? 0);
  const [isLoadingOlder, setIsLoadingOlder] = useState(false);
  const loadingOlderRef = useRef(false);
  const [streamingText, setStreamingText] = useState('');
  const [isGenerating, setIsGenerating] = useState(false);
  const [sessionId, setSessionId] = useState<string | null>(initialSessionId ?? null);
  const [model, setModel] = useState<string | null>(null);
  const [selectedModel, setSelectedModel] = useState<string | null>(initialModel ?? null);
  const [cwd, setCwd] = useState<string | null>(null);
  const [thinkingLevel, setThinkingLevel] = useState<string | null>(null);

  // Resilient-transport cursor: the max durable-event `seq` seen. On (re)connect we send it as a
  // resume-cursor so the server replays anything we missed (e.g. a background task:notification that
  // landed while briefly disconnected). sendRef breaks the hook ↔ onOpen circular dependency.
  const cursorRef = useRef(0);
  const sendRef = useRef<(d: Record<string, unknown>) => void>(() => {});

  // Track if session has started (first message sent)
  const [hasStarted, setHasStarted] = useState(!!preloadedMessages?.length);

  // Get user settings for default model
  const { settings } = useSettings();

  // Set default model from settings when starting a new chat (no initialSessionId, no initialModel)
  useEffect(() => {
    if (!initialSessionId && !initialModel) {
      const defaultModel = (projectScoped && settings?.chat?.defaultProjectModel) || settings?.chat?.defaultModel;
      if (defaultModel) setSelectedModel(defaultModel);
    }
  }, [initialSessionId, initialModel, settings, projectScoped]);

  // Sync selectedModel when initialModel changes (e.g. resuming a session)
  useEffect(() => {
    if (initialModel) setSelectedModel(initialModel);
  }, [initialModel]);

  const streamingRef = useRef('');
  const rafRef = useRef<number | null>(null);
  const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
  // Mirrors of the session:init fields, for the reconnect handshake — `onOpen` is stable by design and
  // cannot close over the state.
  const modelRef = useRef<string | null>(null);
  const cwdRef = useRef<string | null>(null);
  const saveTimerRef = useRef<number | null>(null);
  const toolCallsInTurnRef = useRef(false);
  const onTurnCompleteRef = useRef(onTurnComplete);
  onTurnCompleteRef.current = onTurnComplete;
  const resumeSummaryRef = useRef<string | undefined>(initialResumeSummary);
  // Mirror for the reconnect handshake, which runs from a stable callback and cannot read state.
  const isGeneratingRef = useRef(false);
  isGeneratingRef.current = isGenerating;
  // Claude's transcript uuid — the reconnect handshake's fallback when officer's key is gone.
  //
  // Seeded from `resumeSessionId` because on a refresh that IS the URL: the page remounts with no memory
  // of officer's session, and this is the only identifier left to reconnect by. Kept current from
  // `session:claude` (start of turn) and `result` (end), so a chat started in this tab becomes
  // reattachable the moment the harness names its transcript rather than when the turn finishes.
  const claudeSessionIdRef = useRef<string | null>(resumeSessionId ?? null);

  const client = useClient();

  // Fetch the next older window and prepend it. The scroll container restores its position from the
  // height delta so the view stays put. Guarded against overlap and against running once fully paged in.
  const loadOlder = useCallback(async () => {
    if (!paginate || loadingOlderRef.current || oldestOffset <= 0) return;
    loadingOlderRef.current = true;
    setIsLoadingOlder(true);
    try {
      const res = await client.get<{ messages: ChatMessage[]; offset: number }>(
        `/chat/sessions/${paginate.sessionId}?before=${oldestOffset}&limit=${OLDER_PAGE_SIZE}`,
      );
      if (res.messages.length) {
        setMessages((prev) => [...res.messages, ...prev]);
        // Paged-in messages are older than everything on screen, so they join the historical block at the
        // front and the boundary between history and this sitting moves with them.
        setHistoricalCount((count) => count + res.messages.length);
        setOldestOffset(res.offset);
      } else {
        setOldestOffset(0);
      }
    } catch {
      // Leave the offset unchanged; scrolling near the top again will retry.
    } finally {
      loadingOlderRef.current = false;
      setIsLoadingOlder(false);
    }
  }, [client, paginate, oldestOffset]);

  const hasMoreOlder = !!paginate && oldestOffset > 0;

  const token = localStorage.getItem('BEARER_TOKEN');
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  const wsUrl = `${protocol}//${window.location.host}/api/chat/ws?token=${token}`;

  function flushStreaming() {
    if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
    rafRef.current = requestAnimationFrame(() => {
      setStreamingText(streamingRef.current);
      rafRef.current = null;
    });
  }

  function commitStreaming() {
    if (!streamingRef.current) return;
    const text = streamingRef.current;
    streamingRef.current = '';
    setStreamingText('');
    setMessages((prev) => [...prev, { role: 'assistant', id: randomId(), text }]);
  }

  /**
   * Apply `fn` to the children of the tool call `parentId`, wherever it sits in the transcript. A subagent
   * runs for minutes, so its parent Task row is rarely the last message by the time output arrives.
   */
  function withChildren(prev: ChatMessage[], parentId: string, fn: (children: ChatMessage[]) => ChatMessage[]) {
    let found = false;
    const next = prev.map((m) => {
      if (found || m.role !== 'tool' || m.toolCallId !== parentId) return m;
      found = true;
      return { ...m, children: fn(m.children ?? []) };
    });
    // The Task row itself never arrived (a replay that starts mid-subagent). Dropping the output would be
    // worse than showing it unattributed, so fall through to the top level.
    return found ? next : null;
  }

  /**
   * Rebuild the nested transcript from the flat `Message[]` a resume replays. The wire format stays flat
   * — officer folds messages in arrival order and stamps each with its `parentToolUseId` — so the nesting
   * is reconstructed here rather than stored twice.
   */
  function rebuildTranscript(messages: Message[]): ChatMessage[] {
    const top: ChatMessage[] = [];
    const byToolCallId = new Map<string, Extract<ChatMessage, { role: 'tool' }>>();

    for (const m of messages) {
      let converted: ChatMessage;
      if (m.role === 'user') {
        converted = { role: 'user', text: m.text || '' };
      } else if (m.role === 'tool') {
        const call: Extract<ChatMessage, { role: 'tool' }> = {
          role: 'tool',
          toolName: m.toolName || '',
          toolInput: m.toolInput || {},
          toolCallId: m.toolCallId || '',
          output: m.output,
          isError: m.isError,
        };
        if (call.toolCallId) byToolCallId.set(call.toolCallId, call);
        converted = call;
      } else {
        converted = { role: 'assistant', id: m.id ?? randomId(), text: m.text || '' };
      }

      // A subagent's output belongs under the Task row that spawned it. If that row is missing (pruned
      // from the replay window) it goes top-level — unattributed beats dropped.
      const parent = m.parentToolUseId ? byToolCallId.get(m.parentToolUseId) : undefined;
      if (parent) parent.children = [...(parent.children ?? []), converted];
      else top.push(converted);
    }

    return top;
  }

  function handleMessage(data: unknown) {
    const msg = data as ServerMessage;

    // Continuity check, before the cursor moves. The writer stamps every durable message with the cursor
    // of the one before it in the same session, so a mismatch against what we last saw means something is
    // missing — pruned by retention, or a write that failed. Surface it: a conversation that silently
    // skips a tool call or half an answer reads as the assistant having done something inexplicable.
    // Only checked once we actually hold a cursor; opening a session from history starts mid-chain by
    // design (events are swept after 7 days, the transcript itself is not).
    const prevSeq = (data as { prevSeq?: number }).prevSeq;
    if (typeof prevSeq === 'number' && cursorRef.current > 0 && prevSeq !== cursorRef.current) {
      setMessages((prev) => [
        ...prev,
        { role: 'error', text: '⚠️ Some output could not be recovered — part of this conversation is missing above.' },
      ]);
    }

    // Advance the resume cursor for any durable (seq-carrying) event.
    const seq = (data as { seq?: number }).seq;
    if (typeof seq === 'number' && seq > cursorRef.current) cursorRef.current = seq;

    switch (msg.type) {
      case 'session:init':
        sessionIdRef.current = msg.sessionId;
        modelRef.current = msg.model;
        cwdRef.current = msg.cwd;
        setSessionId(msg.sessionId);
        setModel(msg.model);
        setCwd(msg.cwd);
        break;

      case 'session:claude':
        // The turn has only just begun and the conversation is already addressable. Everything that makes
        // a refresh survivable hangs off this: the URL is what the reattach handshake sends, so writing it
        // here rather than at `result` is the difference between "refresh mid-turn and lose the turn" and
        // "refresh mid-turn and watch it carry on".
        claudeSessionIdRef.current = msg.claudeSessionId;
        if (replaceUrl) writePermalink(msg.claudeSessionId);
        break;

      case 'assistant:delta':
        // A subagent's deltas are deliberately not streamed. Two speakers cannot share one cursor, and the
        // complete `assistant:text` that follows lands in the Task row a moment later regardless.
        if (msg.parentToolUseId) break;
        streamingRef.current += msg.text;
        flushStreaming();
        break;

      case 'assistant:text': {
        const parent = msg.parentToolUseId;
        if (parent) {
          setMessages(
            (prev) =>
              withChildren(prev, parent, (kids) => [
                ...kids,
                { role: 'assistant', id: randomId(), text: msg.text },
              ]) ?? [...prev, { role: 'assistant', id: randomId(), text: msg.text }],
          );
          break;
        }
        if (streamingRef.current) {
          commitStreaming();
        } else {
          setMessages((prev) => [...prev, { role: 'assistant', id: randomId(), text: msg.text }]);
        }
        break;
      }

      case 'tool:start': {
        toolCallsInTurnRef.current = true;
        const call: ChatMessage = {
          role: 'tool',
          toolName: msg.toolName,
          toolInput: msg.toolInput,
          toolCallId: msg.toolCallId,
        };
        const parent = msg.parentToolUseId;
        setMessages((prev) =>
          parent ? (withChildren(prev, parent, (kids) => [...kids, call]) ?? [...prev, call]) : [...prev, call],
        );
        break;
      }

      case 'tool:result': {
        const patch = (m: ChatMessage): ChatMessage =>
          m.role === 'tool' && m.toolCallId === msg.toolCallId ? { ...m, output: msg.output, isError: msg.isError } : m;
        const parent = msg.parentToolUseId;
        setMessages((prev) =>
          parent ? (withChildren(prev, parent, (kids) => kids.map(patch)) ?? prev.map(patch)) : prev.map(patch),
        );
        break;
      }

      case 'result': {
        // Belt and braces for the permalink: `session:claude` normally got here first, but a harness that
        // never emitted an init (or a turn relayed by another sidecar) still reports the uuid at the end.
        // Deliberately not keyed off officer's `sessionId`, which is a per-connection key
        // (`msg.sessionId || randomUUID()`) that `/chat/sessions/:id` cannot resolve.
        if (msg.claudeSessionId) {
          claudeSessionIdRef.current = msg.claudeSessionId;
          if (replaceUrl) writePermalink(msg.claudeSessionId);
        }
        commitStreaming();
        setMessages((prev) => [
          ...prev,
          {
            role: 'result',
            cost: msg.cost,
          },
        ]);
        setIsGenerating(false);
        const hadTools = toolCallsInTurnRef.current;
        toolCallsInTurnRef.current = false;
        onTurnCompleteRef.current?.(hadTools);
        break;
      }

      case 'sync:live': {
        // Re-bound to a turn that kept running while this page was away. Messages are deliberately absent
        // — the transcript already loaded over HTTP — so this only picks up the live state and lets the
        // rest of the turn arrive through the normal cases below.
        sessionIdRef.current = msg.sessionId;
        setSessionId(msg.sessionId);
        setIsGenerating(msg.isGenerating);
        if (msg.isGenerating) setHasStarted(true);
        // Start from the session's head, not from zero: this socket now knows officer's key, so the next
        // drop replays via `resume-cursor` — and from zero that would re-deliver the whole conversation
        // on top of the transcript already on screen.
        if (msg.cursor > cursorRef.current) cursorRef.current = msg.cursor;
        if (msg.streamingText) {
          streamingRef.current = msg.streamingText;
          flushStreaming();
        }
        // The one thing the transcript genuinely cannot supply — see `sync:live` on the server.
        if (msg.runningTasks?.length) setMessages((prev) => spliceRunningTasks(prev, msg.runningTasks));
        break;
      }

      case 'sync:messages': {
        sessionIdRef.current = msg.sessionId;
        setSessionId(msg.sessionId);
        const transcript = rebuildTranscript(msg.messages);
        setMessages(transcript);
        // A resume replaces the list wholesale with what the server has. All of it is history — including a
        // turn this tab watched arrive before it disconnected, which is now something being read back.
        setHistoricalCount(transcript.length);
        if (transcript.length > 0) setHasStarted(true);
        setIsGenerating(msg.isGenerating);
        if (msg.streamingText) {
          streamingRef.current = msg.streamingText;
          flushStreaming();
        }
        break;
      }

      case 'error':
        commitStreaming();
        setMessages((prev) => [...prev, { role: 'error', text: msg.message }]);
        setIsGenerating(false);
        break;

      case 'stopped':
        // Whatever the agent had said by then stays — it happened — with a line under it saying where
        // you cut it off. Silently settling to idle was indistinguishable from the turn just ending.
        commitStreaming();
        setMessages((prev) => [...prev, { role: 'interrupted' }]);
        setIsGenerating(false);
        break;

      case 'cut-off':
        // The prompt is taken from the transcript rather than remembered separately: this can arrive in a
        // tab that never sent it (a second window on the same session), and the transcript is the one
        // place both of them agree on.
        commitStreaming();
        setMessages((prev) => {
          const lastUser = [...prev].reverse().find((m) => m.role === 'user');
          return [...prev, { role: 'cutoff', prompt: lastUser?.role === 'user' ? lastUser.text : '' }];
        });
        setIsGenerating(false);
        break;

      case 'disconnected':
        // Session torn down server-side (turn killed, in-memory session dropped). Commit any partial
        // stream and settle to idle; the WS stays open so a new prompt starts a fresh session.
        commitStreaming();
        setIsGenerating(false);
        break;

      case 'task:started':
        setMessages((prev) => [
          ...prev,
          { role: 'task', taskId: msg.taskId, description: msg.description, taskType: msg.taskType },
        ]);
        break;

      case 'compact:start':
        // Committed first: compaction interrupts the agent mid-turn, and the text it had already streamed
        // is finished — leaving it in the streaming bubble would park it under the notice until the turn
        // ended, minutes later.
        commitStreaming();
        setMessages((prev) => [...prev, { role: 'compact', trigger: msg.trigger }]);
        break;

      case 'compact:done':
        setMessages((prev) => {
          const at = prev.findLastIndex((m) => m.role === 'compact' && !m.done);
          const resolved: ChatMessage = {
            role: 'compact',
            trigger: msg.trigger,
            preTokens: msg.preTokens,
            durationMs: msg.durationMs,
            done: true,
          };
          // No pending row: a replay whose window starts after `compact:start`, or a harness that
          // compacted without firing the hook. The boundary alone still explains the gap.
          if (at < 0) return [...prev, resolved];
          const next = [...prev];
          next[at] = resolved;
          return next;
        });
        break;

      case 'task:notification': {
        // A background task's completion, arriving after the turn ended — the whole point of the
        // persistent worker. Resolved onto the row that announced it rather than appended as a second
        // bubble: the two used to be minutes and a screenful of scrollback apart, with the taskId that
        // relates them thrown away on both sides.
        setMessages((prev) => {
          const at = prev.findIndex((m) => m.role === 'task' && m.taskId === msg.taskId);
          // No announcing row — a replay that starts after `task:started` was pruned. Stand one up.
          if (at < 0) {
            return [
              ...prev,
              { role: 'task', taskId: msg.taskId, description: msg.summary, status: msg.status, summary: msg.summary },
            ];
          }
          const next = [...prev];
          next[at] = {
            ...(next[at] as Extract<ChatMessage, { role: 'task' }>),
            status: msg.status,
            summary: msg.summary,
          };
          return next;
        });
        break;
      }
    }
  }

  // On every (re)connect, if a session is already established, re-bind + replay via resume-cursor.
  // The first connect (no session yet) no-ops; the first turn establishes the session via session:init.
  // `model`/`cwd` come back from the session:init this client already holds. After an officer restart the
  // server has no memory of either, and it needs both to re-adopt the session rather than leave it
  // orphaned — so the client, which is now the only party that remembers, hands them back.
  // Ask the server to re-bind this socket by Claude's transcript uuid — the refresh case, where officer's
  // key died with the page but the uuid is right there in the URL. Sending it for a chat that turns out
  // not to be running costs one message and a silent no from the server, which is the right trade: the
  // case worth catching is opening a URL whose turn is still going, and that is indistinguishable from
  // the ordinary one until the server has looked.
  const attachedRef = useRef<string | null>(null);
  const sendAttach = useCallback(() => {
    const claudeId = claudeSessionIdRef.current;
    // Officer's own key, once we have one, is strictly better: it addresses the session directly and
    // replays from a cursor. Attach is only ever the fallback for not having it.
    if (!claudeId || sessionIdRef.current || attachedRef.current === claudeId) return;
    attachedRef.current = claudeId;
    sendRef.current({ type: 'attach', claudeSessionId: claudeId });
  }, []);

  const onOpen = useCallback(() => {
    const sid = sessionIdRef.current;
    if (!sid) {
      // Without this the callback returned early and the socket sat idle while the turn ran on unwatched
      // — which is why output "stopped" on refresh and the only way forward was to re-send the prompt.
      sendAttach();
      return;
    }
    sendRef.current({
      type: 'resume-cursor',
      sessionId: sid,
      cursor: cursorRef.current,
      ...(modelRef.current ? { model: modelRef.current } : {}),
      ...(cwdRef.current ? { cwd: cwdRef.current } : {}),
      // Whether we still think a turn is running. If the agent restarted under us there is nothing left
      // to produce output, and this is the only moment anyone can notice — the socket is healthy, the
      // transcript just stops. The server checks the claim with the agent and answers if it's false.
      generating: isGeneratingRef.current,
    });
  }, [sendAttach]);

  const { isConnected, send } = useChatWebSocket({ url: wsUrl, onMessage: handleMessage, onOpen });
  sendRef.current = send;

  // `resumeSessionId` is resolved asynchronously by the panel that owns this hook, so it routinely lands
  // *after* the socket has already opened and `onOpen` has come and gone with nothing to send. Attaching
  // is idempotent, so covering both orders here is simpler than sequencing them.
  useEffect(() => {
    if (resumeSessionId) claudeSessionIdRef.current = resumeSessionId;
    if (isConnected) sendAttach();
  }, [resumeSessionId, isConnected, sendAttach]);

  // Clean up RAF on unmount
  useEffect(() => {
    return () => {
      if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
    };
  }, []);

  function sendPrompt(
    text: string,
    attachmentIds?: string[],
    images?: { filename: string; dataUrl: string }[],
    cwdParam?: { root?: string; path: string },
    groupSlug?: string | null,
    thinking?: string | null,
    displayText?: string,
  ) {
    // Mark session as started on first message
    if (!hasStarted) {
      setHasStarted(true);
    }

    setMessages((prev) => [
      ...prev,
      { role: 'user', text: displayText ?? text, ...(images?.length ? { images } : {}) },
    ]);
    setIsGenerating(true);
    streamingRef.current = '';
    setStreamingText('');
    toolCallsInTurnRef.current = false;

    // Parse dataUrls into { mediaType, data } for the server
    const imageData = images
      ?.map((img) => {
        const match = img.dataUrl.match(/^data:([^;]+);base64,(.+)$/);
        return match ? { mediaType: match[1], data: match[2] } : null;
      })
      .filter((x): x is { mediaType: string; data: string } => x !== null);

    const pendingResumeSummary = resumeSummaryRef.current;
    resumeSummaryRef.current = undefined;

    send({
      type: 'chat',
      prompt: text,
      ...(displayText ? { displayText } : {}),
      sessionId: sessionIdRef.current,
      ...(selectedModel ? { model: selectedModel } : {}),
      ...(cwdParam?.path ? { cwd: cwdParam.path } : {}),
      ...(cwdParam?.root ? { cwdRoot: cwdParam.root } : {}),
      ...(groupSlug !== undefined ? { groupSlug } : {}),
      ...(attachmentIds?.length ? { attachmentIds } : {}),
      ...(imageData?.length ? { images: imageData } : {}),
      ...(resourceChatDir ? { resourceChatDir } : {}),
      ...(taskInfo ? { taskInfo } : {}),
      ...(thinking ? { thinking } : {}),
      ...(context ? { context } : {}),
      ...(contextId ? { contextId } : {}),
      ...(pendingResumeSummary ? { resumeSummary: pendingResumeSummary } : {}),
      ...(resumeSessionId ? { resumeSessionId } : {}),
    });
  }

  function stopGeneration() {
    send({ type: 'stop' });
  }

  // Tear down the whole session server-side (kill any in-flight turn + drop the in-memory session),
  // freeing it so its transcript can be resumed elsewhere. Stronger than stopGeneration (turn only).
  function disconnectSession() {
    send({ type: 'disconnect' });
  }

  return {
    messages,
    historicalCount,
    streamingText,
    isConnected,
    isGenerating,
    sessionId,
    model,
    selectedModel,
    hasStarted,
    cwd,
    thinkingLevel,
    setSelectedModel,
    setThinkingLevel,
    sendPrompt,
    stopGeneration,
    disconnectSession,
    loadOlder,
    hasMoreOlder,
    isLoadingOlder,
  };
}

export type UseChatType = ReturnType<typeof useChat>;
