import { createSignal, For, Show } from 'solid-js'; import { ChatConfig, useChatConfig } from '../primitives/chat-config'; import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor } from './chat-container'; import { Message, MessageAvatar, MessageContent, MessageActionBar } from './message'; import { Reasoning, ReasoningTrigger, ReasoningContent } from './reasoning'; import { Tool } from './tool'; import { Attachments, Attachment, AttachmentPreview, AttachmentInfo, type AttachmentData } from './attachments'; import { ModelSwitcher } from './model-switcher'; import { ScrollButton } from './scroll-button'; import { Context, ContextTrigger, ContextContent, ContextContentHeader, ContextContentBody, ContextContentFooter, ContextInputUsage, ContextOutputUsage, } from './context'; import { DefaultPromptInput } from '../elements/default-input'; import type { SlashCommandItem } from './slash-command'; import type { ChatMessage } from '../elements/chat-types'; import type { ProseSize } from '../primitives/chat-config'; import type { ModelOption } from '../types'; export interface ChatThreadContextUsage { usedTokens: number; maxTokens: number; inputTokens?: number; outputTokens?: number; estimatedCost?: number; } export interface ChatThreadProps { /** Extra classes for the thread root (e.g. `h-full`). */ class?: string; /** The full message thread to render, newest last. Each entry carries its role, * content, and optional reasoning/tools/attachments/actions. Set as a JS * property (`el.messages = [...]`). */ messages: ChatMessage[]; /** Controlled value of the input. When set, the host owns the input text and * must update it on `kc-value-change`; leave unset for uncontrolled behavior. */ value?: string; /** Placeholder text shown in the empty input. */ placeholder?: string; /** When true, shows the loading/streaming state and disables submit (use while * awaiting the assistant's reply). */ loading?: boolean; /** Starter prompts shown above the input when the thread is empty. Clicking one * follows `suggestionMode`. Set as a JS property. */ suggestions?: string[]; /** What clicking a suggestion does: `'submit'` (default) sends it immediately * as if typed and submitted; `'fill'` just places it in the input. */ suggestionMode?: 'submit' | 'fill'; /** Keep suggestions visible after the conversation starts. By default * suggestions are conversation starters and hide once `messages` is * non-empty; set this to keep them always shown. Default false. */ persistSuggestions?: boolean; /** Body/prose font scale for rendered markdown (`'xs' | 'sm' | 'base' | 'lg'`). * Defaults to `'sm'`. */ proseSize?: ProseSize; /** Shiki theme name for syntax-highlighted code blocks (e.g. * `'github-dark-dimmed'`). */ codeTheme?: string; /** Enable Shiki syntax highlighting in code blocks. Turn off to render plain * `
` blocks (lighter, no highlighter load). Default true. */
  codeHighlight?: boolean;
  /** Optional header title shown on the left of the header. */
  chatTitle?: string;
  /** Optional model list. When set (>1 model) a ModelSwitcher is shown in the
   *  header and a `kc-model-change` event fires on selection. */
  models?: ModelOption[];
  /** The currently selected model id (pairs with `models`). */
  currentModel?: string;
  /** Optional context-window token usage. When set, a Context token meter is
   *  shown in the header. */
  context?: ChatThreadContextUsage;
  /** Show the scroll-to-bottom button inside the scroll area. Default true. */
  scrollButton?: boolean;
  /** Whether the host has `slot="header-start"` content (left of the title) —
   *  set by the `` facade so a custom control forces the header open. */
  headerStart?: boolean;
  /** Whether the host has `slot="header-end"` content (right of the controls). */
  headerEnd?: boolean;
  /** Show a Search (Globe) button in the input toolbar; fires a `search` event. */
  search?: boolean;
  /** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
  voice?: boolean;
  /** Slash commands — when set, typing `/` in the input opens the command
   *  palette and fires `kc-slash-select`. Set as a JS property. */
  slashCommands?: SlashCommandItem[];
  /** Command ids to highlight as active in the palette. */
  slashActiveIds?: string[];
  /** Single-line palette rows. */
  slashCompact?: boolean;
  /** Whether each message's action bar is always visible (`'always'`, default)
   *  or only revealed on hover of that message row (`'hover'`). */
  actionsReveal?: 'always' | 'hover';
  // callbacks (the facade maps these to dispatch())
  onValueChange?: (value: string) => void;
  onSubmit?: (detail: { value: string; attachments: AttachmentData[] }) => void;
  onSuggestionClick?: (value: string) => void;
  onModelChange?: (modelId: string) => void;
  onMessageAction?: (detail: { messageId: string; action: string }) => void;
  onSearch?: () => void;
  onVoice?: () => void;
  onSlashSelect?: (command: SlashCommandItem) => void;
}

export function ChatThread(props: ChatThreadProps) {
  const outer = useChatConfig();
  const reveal = () => (props.actionsReveal === 'hover' ? 'hover' : 'always');
  const [internal, setInternal] = createSignal(props.value ?? '');
  const [attachments, setAttachments] = createSignal([]);
  const current = () => props.value ?? internal();
  const handleChange = (v: string) => { setInternal(v); props.onValueChange?.(v); };
  const handleSubmit = () => { props.onSubmit?.({ value: current(), attachments: attachments() }); setAttachments([]); };
  const handleSuggestionClick = (v: string) => {
    if ((props.suggestionMode ?? 'submit') === 'fill') { handleChange(v); props.onSuggestionClick?.(v); }
    else { props.onSubmit?.({ value: v, attachments: attachments() }); setAttachments([]); }
  };
  const showHeader = () => !!(props.chatTitle || props.models || props.context || props.headerStart || props.headerEnd);
  // Suggestions are conversation starters: show only on an empty thread unless
  // the host opts into persisting them.
  const visibleSuggestions = () =>
    props.persistSuggestions || props.messages.length === 0 ? props.suggestions : undefined;
  const showScrollButton = () => props.scrollButton !== false;

  return (
    
      
{/* Consumer-injected leading controls (sidebar-toggle, compose, a popover title-button). Projects light-DOM `slot="header-start"` children of ; inert outside a shadow root. */}
{props.chatTitle}
props.onModelChange?.(modelId)} />
{/* Consumer-injected trailing controls (share, settings, …). Projects light-DOM `slot="header-end"` children of . */}
{(m) => { const body = ( <> {m.reasoning!.label ?? 'Reasoning'} {m.reasoning!.text} {(tp) => } {(att) => ()} {m.content} props.onMessageAction?.({ messageId: m.id, action })} /> ); const rowGroup = reveal() === 'hover' ? 'group ' : ''; return ( {body} } > {(av) => (
{body}
)}
); }}
props.onSearch?.()} onVoice={() => props.onVoice?.()} onSlashSelect={(command) => props.onSlashSelect?.(command)} />
); }